diff options
141 files changed, 11948 insertions, 4408 deletions
@@ -1,8 +1,5 @@ *.pyc *.swp *.log -settings_local.py nbproject settings_local.py -log -*.log @@ -1,10 +1,27 @@ -PRE-REQUIREMENTS: +CONTENTS +------------------ +A. PREREQUISITES +B. INSTALLATION + 1. Settings file + 2. Database + 3. Running CNPROG in the development server + 4. Installation under Apache/WSGI + 5. Full text search + 6. Email subscriptions + 7. Sitemap + 8. Miscellaneous +C. CONFIGURATION PARAMETERS (settings_local.py) +D. CUSTOMIZATION + + +A. PREREQUISITES ----------------------------------------------- 0. We recommend you to use python-setuptools to install pre-requirement libraries. If you haven't installed it, please try to install it first. e.g, sudo apt-get install python-setuptools 1. Python2.5/2.6, MySQL, Django v1.0/1.1 +Note: email subscription sender job requires Django 1.1, everything else works with 1.0 Make sure mysql for python provider has been installed. sudo easy_install mysql-python @@ -12,9 +29,6 @@ sudo easy_install mysql-python http://openidenabled.com/python-openid/ sudo easy_install python-openid -3. django-authopenid(Included in project already) -http://code.google.com/p/django-authopenid/ - 4. html5lib http://code.google.com/p/html5lib/ Used for HTML sanitizer @@ -27,24 +41,257 @@ sudo easy_install markdown2 6. Django Debug Toolbar http://github.com/robhudson/django-debug-toolbar/tree/master -INSTALL STEPS: +7. djangosphinx (optional - for full text questions+answer+tag) +http://github.com/dcramer/django-sphinx/tree/master/djangosphinx + +8. sphinx search engine (optional, works together with djangosphinx) +http://sphinxsearch.com/downloads.html + +NOTES: django_authopenid is included into CNPROG code +and is significantly modified. http://code.google.com/p/django-authopenid/ +no need to install this library + +B. INSTALLATION ----------------------------------------------- 0. Make sure you have all above python libraries installed. -1. Copy settings_local.py.dist to settings_local.py and + make cnprog installation server-readable on Linux command might be: + chown -R yourlogin:apache /path/to/CNPROG + + directories templates/upfiles and log must be server writable + + on Linux type chmod + chmod -R g+w /path/to/CNPROG/upfiles + chmod -R g+w /path/to/log + + above it is assumed that webserver runs under group named "apache" + +1. Settings file + +Copy settings_local.py.dist to settings_local.py and update all your settings. Check settings.py and update it as well if necessory. +Section C explains configuration paramaters. -2. Prepare your database by using the same database/account +2. Database + +Prepare your database by using the same database/account configuration from above. e.g, create database cnprog DEFAULT CHARACTER SET UTF8 COLLATE utf8_general_ci; grant all on cnprog.* to 'cnprog'@'localhost'; And then run "python manage.py syncdb" to synchronize your database. -3. Run "python manager.py runserver" to startup django +3. Running CNPROG on the development server + +Run "python manage.py runserver" to startup django development environment. +(Under Linux you can use command "python manage.py runserver `hostname -i`:8000", +where you can use any other available number for the port) + +you might want to have DEBUG=True in the beginning of settings.py +when using the test server + +4. Installation under Apache/WSGI + +4.1 Prepare wsgi script + +Make a file readable by your webserver with the following content: + +--------- +import os +import sys + +sys.path.insert(0,'/one/level/above') #insert to make sure that forum will be found +sys.path.append('/one/level/above/CNPROG') #maybe this is not necessary +os.environ['DJANGO_SETTINGS_MODULE'] = 'CNPROG.settings' +import django.core.handlers.wsgi +application = django.core.handlers.wsgi.WSGIHandler() +----------- + +insert method is used for path because if the forum directory name +is by accident the same as some other python module +you wull see strange errors - forum won't be found even though +it's in the python path. for example using name "test" is +not a good idea - as there is a module with such name + + +4.2 Configure webserver +Settings below are not perfect but may be a good starting point + +--------- +WSGISocketPrefix /path/to/socket/sock #must be readable and writable by apache +WSGIPythonHome /usr/local #must be readable by apache +WSGIPythonEggs /var/python/eggs #must be readable and writable by apache + +#NOTE: all urs below will need to be adjusted if +#settings.FORUM_SCRIPT_ALIAS !='' (e.g. = 'forum/') +#this allows "rooting" forum at http://example.com/forum, if you like +<VirtualHost ...your ip...:80> + ServerAdmin forum@example.com + DocumentRoot /path/to/cnprog + ServerName example.com + + #run mod_wsgi process for django in daemon mode + #this allows avoiding confused timezone settings when + #another application runs in the same virtual host + WSGIDaemonProcess CNPROG + WSGIProcessGroup CNPROG + + #force all content to be served as static files + #otherwise django will be crunching images through itself wasting time + Alias /content/ /path/to/cnprog/templates/content/ + AliasMatch /([^/]*\.css) /path/to/cnprog/templates/content/style/$1 + <Directory /path/to/cnprog/templates/content> + Order deny,allow + Allow from all + </Directory> + + #this is your wsgi script described in the prev section + WSGIScriptAlias / /path/to/cnprog/cnprog.wsgi -4. There are some demo scripts under sql_scripts folder, + #this will force admin interface to work only + #through https (optional) + #"nimda" is the secret spelling of "admin" ;) + <Location "/nimda"> + RewriteEngine on + RewriteRule /nimda(.*)$ https://example.com/nimda$1 [L,R=301] + </Location> + CustomLog /var/log/httpd/CNPROG/access_log common + ErrorLog /var/log/httpd/CNPROG/error_log +</VirtualHost> +#(optional) run admin interface under https +<VirtualHost ..your ip..:443> + ServerAdmin forum@example.com + DocumentRoot /path/to/cnrpog + ServerName example.com + SSLEngine on + SSLCertificateFile /path/to/ssl-certificate/server.crt + SSLCertificateKeyFile /path/to/ssl-certificate/server.key + WSGIScriptAlias / /path/to/cnprogcnprog.wsgi + CustomLog /var/log/httpd/CNPROG/access_log common + ErrorLog /var/log/httpd/CNPROG/error_log + DirectoryIndex index.html +</VirtualHost> +------------- + +5. Full text search (using sphinx search) + Currently full text search works only with sphinx search engine + Sphinx at this time supports only MySQL and PostgreSQL databases + to enable this, install sphinx search engine and djangosphinx + + configure sphinx, sample configuration can be found in + sphinx/sphinx.conf file usually goes somewhere in /etc tree + + build cnprog index first time manually + + % indexer --config /path/to/sphinx.conf --index cnprog + + setup cron job to rebuild index periodically with command + your crontab entry may be something like + + 0 9,15,21 * * * /usr/local/bin/indexer --config /etc/sphinx/sphinx.conf --all --rotate >/dev/null 2>&1 + adjust it as necessary this one will reindex three times a day at 9am 3pm and 9pm + + if your forum grows very big ( good luck with that :) you'll + need to two search indices one diff index and one main + please refer to online sphinx search documentation for the information + on the subject http://sphinxsearch.com/docs/ + + in settings_local.py set + USE_SPHINX_SEARCH=True + adjust other settings that have SPHINX_* prefix accordingly + remember that there must be trailing comma in parentheses for + SHPINX_SEARCH_INDICES tuple - particlarly with just one item! + +6. Email subscriptions + + This function at the moment requires Django 1.1 + + edit paths in the file cron/send_email_alerts + set up a cron job to call cron/send_email_alerts once or twice a day + subscription sender may be tested manually in shell + by calling cron/send_email_alerts + +7. Sitemap +Sitemap will be available at /<settings_local.FORUM_SCRIPT_ALIAS>sitemap.xml +e.g yoursite.com/forum/sitemap.xml + +google will be pinged each time question, answer or +comment is saved or a question deleted + +for this to be useful - do register you sitemap with Google at +https://www.google.com/webmasters/tools/ + +8. Miscellaneous + +There are some demo scripts under sql_scripts folder, including badges and test accounts for CNProg.com. You don't need them to run your sample. + +C. CONFIGURATION PARAMETERS + +#the only parameter that needs to be touched in settings.py is +DEBUG=False #set to True to enable debug mode + +#all forum parameters are set in file settings_local.py + +LOG_FILENAME = 'cnprog.log' #where logging messages should go +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' #mysql, etc +SERVER_EMAIL = '' +DEFAULT_FROM_EMAIL = '' +EMAIL_HOST_USER = '' +EMAIL_HOST_PASSWORD = '' #not necessary if mailserver is run on local machine +EMAIL_SUBJECT_PREFIX = '[CNPROG] ' +EMAIL_HOST='cnprog.com' +EMAIL_PORT='25' +EMAIL_USE_TLS=False +TIME_ZONE = 'America/Tijuana' +APP_TITLE = u'CNPROG Q&A Forum' #title of your forum +APP_KEYWORDS = u'CNPROG,forum,community' #keywords for search engines +APP_DESCRIPTION = u'Ask and answer questions.' #site description for searche engines +APP_INTRO = u'<p>Ask and answer questions, make the world better!</p>' #slogan that goes to front page in logged out mode +APP_COPYRIGHT = '' #copyright message + +#if you set FORUM_SCRIPT_ALIAS= 'forum/' +#then CNPROG will run at url http://example.com/forum +#FORUM_SCRIPT_ALIAS cannot have leading slash, otherwise it can be set to anything +FORUM_SCRIPT_ALIAS = '' #no leading slash, default = '' empty string + +LANGUAGE_CODE = 'en' #forum language (see language instructions on the wiki) +EMAIL_VALIDATION = 'off' #string - on|off +MIN_USERNAME_LENGTH = 1 +EMAIL_UNIQUE = False #if True, email addresses must be unique in all accounts +APP_URL = 'http://cnprog.com' #used by email notif system and RSS +GOOGLE_SITEMAP_CODE = '' #code for google site crawler (look up google webmaster tools) +GOOGLE_ANALYTICS_KEY = '' #key to enable google analytics on this site +BOOKS_ON = False #if True - books tab will be on +WIKI_ON = True #if False - community wiki feature is disabled + +#experimental - allow password login through external site +#must implement django_authopenid/external_login.py +#included prototype external_login works with Mediawiki +USE_EXTERNAL_LEGACY_LOGIN = True #if false CNPROG uses it's own login/password +EXTERNAL_LEGACY_LOGIN_HOST = 'login.cnprog.com' +EXTERNAL_LEGACY_LOGIN_PORT = 80 +EXTERNAL_LEGACY_LOGIN_PROVIDER_NAME = '<span class="orange">CNPROG</span>' + +FEEDBACK_SITE_URL = None #None or url +LOGIN_URL = '/%s%s%s' % (FORUM_SCRIPT_ALIAS,'account/','signin/') + +DJANGO_VERSION = 1.1 #must be either 1.0 or 1.1 +RESOURCE_REVISION=4 #increment when you update media files - clients will be forced to load new version + +D. Customization + +Other than settings_local.py the following will most likely need customization: +* locale/*/django.po - language files that may also contain your site-specific messages + if you want to start with english messages file - look for words like "forum" and + "CNPROG" in the msgstr lines +* templates/header.html and templates/footer.html may contain extra links +* templates/about.html - a place to explain for is your forum for +* templates/faq.html - put answers to users frequent questions +* templates/content/style/style.css - modify style sheet to add disctinctive look to your forum @@ -0,0 +1,3 @@ +* per-tag email subscriptions +* make sorting tabs work in question search +* allow multiple logins to the same account diff --git a/cnprog.wsgi b/cnprog.wsgi new file mode 100644 index 00000000..bd3745ee --- /dev/null +++ b/cnprog.wsgi @@ -0,0 +1,8 @@ +#example wsgi setup script +import os +import sys +sys.path.append('/path/above_forum') +sys.path.append('/path/above_forum/forum_dir') +os.environ['DJANGO_SETTINGS_MODULE'] = 'forum_dir.settings' +import django.core.handlers.wsgi +application = django.core.handlers.wsgi.WSGIHandler() @@ -11,6 +11,10 @@ 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, + 'USE_SPHINX_SEARCH':settings.USE_SPHINX_SEARCH, } return {'settings':my_settings} diff --git a/cron/send_email_alerts b/cron/send_email_alerts new file mode 100644 index 00000000..e9e433be --- /dev/null +++ b/cron/send_email_alerts @@ -0,0 +1,4 @@ +PYTHONPATH=/dir/just/above_forum +export PYTHONPATH +APP_ROOT=$PYTHONPATH/CNPROG +/usr/local/bin/python $APP_ROOT/manage.py send_email_alerts >> $APP_ROOT/log/django.lanai.log diff --git a/development.log b/development.log index 5310f70b..abe1aac0 100644 --- a/development.log +++ b/development.log @@ -1,5 +1,5 @@ ==Aug 5, 2009 Evgeny== -===Interface changes=== +====Interface changes=== Merged in my code that: * allows anonymous posting of Q&A and then login * per-question email notifications via 'send_email_alerts' command 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 (<i>required</i>)'))) 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 3382434a..112cbbe1 100644 --- a/django_authopenid/urls.py +++ b/django_authopenid/urls.py @@ -7,11 +7,12 @@ 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('^%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 @@ -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<id>\d+)/(?P<key>[\dabcdef]{32})/$' % (_('email/'), _('verify/')), 'verifyemail', name='user_verifyemail'), + url(r'^%s$' % _('openid/'), 'changeopenid', name='user_changeopenid'), url(r'^%s$' % _('delete/'), 'delete', name='user_delete'), ) diff --git a/django_authopenid/util.py b/django_authopenid/util.py index 11afe53b..2b9d44a2 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 fa94c4e5..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,18 +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) @@ -322,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_), @@ -338,13 +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_ != None and user_.is_authenticated(): - return HttpResponseRedirect('/') + 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('/') @@ -366,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): @@ -377,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, @@ -396,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): @@ -444,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) @@ -495,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() @@ -509,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) @@ -540,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]) @@ -580,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 @@ -589,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 @@ -611,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)) @@ -666,7 +786,6 @@ def changeemail(request): return output - def emailopenid_success(request, identity_url, openid_response): openid_ = from_openid_response(openid_response) @@ -840,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) @@ -848,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): """ @@ -859,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: @@ -878,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() @@ -915,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 new file mode 100644 index 00000000..1e55cb1f --- /dev/null +++ b/drop-all-tables.sh @@ -0,0 +1,4 @@ +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..eb81f853 100644 --- a/forum/auth.py +++ b/forum/auth.py @@ -1,4 +1,4 @@ -""" + """ Authorisation related functions. The actions a User is authorised to perform are dependent on their reputation @@ -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..76fd4a24 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')), @@ -49,6 +49,7 @@ TYPE_ACTIVITY_MARK_OFFENSIVE=14 TYPE_ACTIVITY_UPDATE_TAGS=15 TYPE_ACTIVITY_FAVORITE=16 TYPE_ACTIVITY_USER_FULL_UPDATED = 17 +TYPE_ACTIVITY_QUESTION_EMAIL_UPDATE_SENT = 18 #TYPE_ACTIVITY_EDIT_QUESTION=17 #TYPE_ACTIVITY_EDIT_ANSWER=18 @@ -70,6 +71,7 @@ TYPE_ACTIVITY = ( (TYPE_ACTIVITY_UPDATE_TAGS, _('updated tags')), (TYPE_ACTIVITY_FAVORITE, _('selected favorite')), (TYPE_ACTIVITY_USER_FULL_UPDATED, _('completed user profile')), + (TYPE_ACTIVITY_QUESTION_EMAIL_UPDATE_SENT, _('email update sent to user')), ) TYPE_RESPONSE = { @@ -85,3 +87,6 @@ CONST = { 'default_version' : _('initial version'), 'retagged' : _('retagged'), } + +#how to filter questions by tags in email digests? +TAG_EMAIL_FILTER_CHOICES = (('ignored', _('exclude ignored tags')),('interesting',_('allow only selected tags'))) diff --git a/forum/feed.py b/forum/feed.py index 22a075a5..59983161 100644 --- a/forum/feed.py +++ b/forum/feed.py @@ -16,7 +16,7 @@ from models import Question import settings class RssLastestQuestionsFeed(Feed): title = settings.APP_TITLE + _(' - ')+ _('latest questions') - link = settings.APP_URL + '/' + _('questions/') + link = settings.APP_URL + '/' + _('question/') description = settings.APP_DESCRIPTION #ttl = 10 copyright = settings.APP_COPYRIGHT @@ -34,7 +34,7 @@ class RssLastestQuestionsFeed(Feed): return item.added_at def items(self, item): - return Question.objects.filter(deleted=False).order_by('-added_at')[:30] + return Question.objects.filter(deleted=False).order_by('-last_activity_at')[:30] def main(): pass diff --git a/forum/forms.py b/forum/forms.py index 98ae3cbb..ad8a676a 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, UserNameField +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 @@ -174,6 +196,7 @@ class EditAnswerForm(forms.Form): class EditUserForm(forms.Form): email = forms.EmailField(label=u'Email', help_text=_('this email does not have to be linked to gravatar'), required=False, max_length=255, widget=forms.TextInput(attrs={'size' : 35})) + username = UserNameField(label=_('Screen name')) realname = forms.CharField(label=_('Real name'), required=False, max_length=255, widget=forms.TextInput(attrs={'size' : 35})) website = forms.URLField(label=_('Website'), required=False, max_length=255, widget=forms.TextInput(attrs={'size' : 35})) city = forms.CharField(label=_('Location'), required=False, max_length=255, widget=forms.TextInput(attrs={'size' : 35})) @@ -182,6 +205,7 @@ class EditUserForm(forms.Form): def __init__(self, user, *args, **kwargs): super(EditUserForm, self).__init__(*args, **kwargs) + self.fields['username'].initial = user.username self.fields['email'].initial = user.email self.fields['realname'].initial = user.real_name self.fields['website'].initial = user.website @@ -208,3 +232,87 @@ 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 TagFilterSelectionForm(forms.ModelForm): + tag_filter_setting = forms.ChoiceField(choices=TAG_EMAIL_FILTER_CHOICES, #imported from forum/const.py + initial='ignored', + label=_('Choose email tag filter'), + widget=forms.RadioSelect) + class Meta: + model = User + fields = ('tag_filter_setting',) + + def save(self): + before = self.instance.tag_filter_setting + super(TagFilterSelectionForm, self).save() + after = self.instance.tag_filter_setting #User.objects.get(pk=self.instance.id).tag_filter_setting + if before != after: + return True + return False + +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', + } + 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')) + all_questions = forms.ChoiceField(choices=DWN,initial='w', + widget=forms.RadioSelect, + label=_('Entire forum (tag filtered)'),) + + 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..283d5683 100644 --- a/forum/management/commands/send_email_alerts.py +++ b/forum/management/commands/send_email_alerts.py @@ -1,10 +1,21 @@ 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 +<<<<<<< HEAD:forum/management/commands/send_email_alerts.py +======= +from forum import const +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:forum/management/commands/send_email_alerts.py from django.core.mail import EmailMessage from django.utils.translation import ugettext as _ +from django.utils.translation import ungettext +import datetime import settings +<<<<<<< HEAD:forum/management/commands/send_email_alerts.py +======= +import logging +from utils.odict import OrderedDict +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:forum/management/commands/send_email_alerts.py class Command(NoArgsCommand): def handle_noargs(self,**options): @@ -15,27 +26,257 @@ class Command(NoArgsCommand): finally: connection.close() + def get_updated_questions_for_user(self,user): +<<<<<<< HEAD:forum/management/commands/send_email_alerts.py + q_sel = [] + q_ask = [] + q_ans = [] + q_all = [] +======= + q_sel = None + q_ask = None + q_ans = None + q_all = None +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:forum/management/commands/send_email_alerts.py + 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 + ) +<<<<<<< HEAD:forum/management/commands/send_email_alerts.py +======= + +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:forum/management/commands/send_email_alerts.py + 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: +<<<<<<< HEAD:forum/management/commands/send_email_alerts.py + Q_set = Q_set1.exclude(last_activity_at__gt=cutoff_time) +======= + Q_set = Q_set1.exclude(last_activity_at__gt=cutoff_time)#report these excluded later +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:forum/management/commands/send_email_alerts.py + 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) +<<<<<<< HEAD:forum/management/commands/send_email_alerts.py + q_sel.cutoff_time = cutoff_time +======= + q_sel.cutoff_time = cutoff_time #store cutoff time per query set +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:forum/management/commands/send_email_alerts.py + 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': +<<<<<<< HEAD:forum/management/commands/send_email_alerts.py + 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 user.tag_filter_setting == 'ignored': + ignored_tags = Tag.objects.filter(user_selections___reason='bad',user_selections__user=user) + q_all = Q_set.exclude( tags__in=ignored_tags ) + else: + selected_tags = Tag.objects.filter(user_selections___reason='good',user_selections__user=user) + q_all = Q_set.filter( tags__in=selected_tags ) + q_all.cutoff_time = cutoff_time + #build list in this order + q_list = OrderedDict() + def extend_question_list(src, dst): + """src is a query set with questions + or an empty list + dst - is an ordered dictionary + """ + if src is None: + return #will not do anything if subscription of this type is not used + cutoff_time = src.cutoff_time + for q in src: + if q in dst: + if cutoff_time < dst[q]['cutoff_time']: + dst[q]['cutoff_time'] = cutoff_time + else: + #initialise a questions metadata dictionary to use for email reporting + dst[q] = {'cutoff_time':cutoff_time} + + extend_question_list(q_sel, q_list) + extend_question_list(q_ask, q_list) + extend_question_list(q_ans, q_list) + extend_question_list(q_all, q_list) + + ctype = ContentType.objects.get_for_model(Question) + EMAIL_UPDATE_ACTIVITY = const.TYPE_ACTIVITY_QUESTION_EMAIL_UPDATE_SENT + for q, meta_data in q_list.items(): + #todo use Activity, but first start keeping more Activity records + #act = Activity.objects.filter(content_type=ctype, object_id=q.id) + #because currently activity is not fully recorded to through + #revision records to see what kind modifications were done on + #the questions and answers + try: + update_info = Activity.objects.get(content_type=ctype, + object_id=q.id, + activity_type=EMAIL_UPDATE_ACTIVITY) + emailed_at = update_info.active_at + except Activity.DoesNotExist: + update_info = Activity(user=user, content_object=q, activity_type=EMAIL_UPDATE_ACTIVITY) + emailed_at = datetime.datetime(1970,1,1)#long time ago + except Activity.MultipleObjectsReturned: + raise Exception('server error - multiple question email activities found per user-question pair') + + q_rev = QuestionRevision.objects.filter(question=q,\ + revised_at__lt=cutoff_time,\ + revised_at__gt=emailed_at) + q_rev = q_rev.exclude(author=user) + meta_data['q_rev'] = len(q_rev) + if len(q_rev) > 0 and q.added_at == q_rev[0].revised_at: + meta_data['q_rev'] = 0 + meta_data['new_q'] = True + else: + meta_data['new_q'] = False + + new_ans = Answer.objects.filter(question=q,\ + added_at__lt=cutoff_time,\ + added_at__gt=emailed_at) + new_ans = new_ans.exclude(author=user) + meta_data['new_ans'] = len(new_ans) + ans_rev = AnswerRevision.objects.filter(answer__question=q,\ + revised_at__lt=cutoff_time,\ + revised_at__gt=emailed_at) + ans_rev = ans_rev.exclude(author=user) + meta_data['ans_rev'] = len(ans_rev) + if len(q_rev) == 0 and len(new_ans) == 0 and len(ans_rev) == 0: + meta_data['nothing_new'] = True + else: + meta_data['nothing_new'] = False + update_info.active_at = now + update_info.save() #save question email update activity + return q_list + + def __action_count(self,string,number,output): +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:forum/management/commands/send_email_alerts.py + 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() - +<<<<<<< HEAD:forum/management/commands/send_email_alerts.py + for user in User.objects.all(): + q_list = self.get_updated_questions_for_user(user) + num_q = len(q_list) +======= + #todo: move this to template + for user in User.objects.all(): + q_list = self.get_updated_questions_for_user(user) + num_q = 0 + num_moot = 0 + for meta_data in q_list.values(): + if meta_data['nothing_new'] == False: + num_q += 1 + else: + num_moot += 1 +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:forum/management/commands/send_email_alerts.py + 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 += '<ul>' +<<<<<<< HEAD:forum/management/commands/send_email_alerts.py + 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 += '<li><a href="%s?sort=latest">%s</a> <font color="#777777">(%s)</font></li>' \ + % (url_prefix + q.get_absolute_url(), q.title, act_token) + text += '</ul>' +======= + for q, meta_data in q_list.items(): + act_list = [] + if meta_data['nothing_new']: + continue + else: + if meta_data['new_q']: + act_list.append(_('new question')) + self.__action_count('%(num)d rev', meta_data['q_rev'],act_list) + self.__action_count('%(num)d ans', meta_data['new_ans'],act_list) + self.__action_count('%(num)d ans rev',meta_data['ans_rev'],act_list) + act_token = ', '.join(act_list) + text += '<li><a href="%s?sort=latest">%s</a> <font color="#777777">(%s)</font></li>' \ + % (url_prefix + q.get_absolute_url(), q.title, act_token) + text += '</ul>' + if num_moot > 0: + text += '<p></p>' + text += ungettext('There is also one question which was recently '\ + +'updated but you might not have seen its latest version.', + 'There are also %(num)d more questions which were recently updated '\ + +'but you might not have seen their latest version.',num_moot) \ + % {'num':num_moot,} + text += _('Perhaps you could look up previously sent forum reminders in your mailbox.') + text += '</p>' + +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:forum/management/commands/send_email_alerts.py + 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 31528428..1504491a 100644 --- a/forum/managers.py +++ b/forum/managers.py @@ -7,25 +7,6 @@ from forum.models import * from urllib import quote, unquote class QuestionManager(models.Manager): - def get_translation_questions(self, orderby, page_size): - questions = self.filter(deleted=False, author__id__in=[28,29]).order_by(orderby)[:page_size] - return questions - - def get_questions_by_pagesize(self, orderby, page_size): - questions = self.filter(deleted=False).order_by(orderby)[:page_size] - return questions - - def get_questions_by_tag(self, tagname, orderby): - questions = self.filter(deleted=False, tags__name = unquote(tagname)).order_by(orderby) - return questions - - def get_unanswered_questions(self, orderby): - questions = self.filter(deleted=False, answer_accepted=False).order_by(orderby) - return questions - - def get_questions(self, orderby): - questions = self.filter(deleted=False).order_by(orderby) - return questions def update_tags(self, question, tagnames, user): """ @@ -70,7 +51,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 +74,11 @@ class QuestionManager(models.Manager): """ #print datetime.datetime.now() from forum.models import Question - questions = list(Question.objects.filter(tagnames = question.tagnames).exclude(id=question.id).all()) + questions = list(self.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).exclude(id=question.id)[:50] + extend_questions = self.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 +91,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 3d752db0..3e1e6543 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.http import urlquote as django_urlquote from django.utils.html import strip_tags from django.core.urlresolvers import reverse @@ -12,33 +12,66 @@ 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 +from django.contrib.sitemaps import ping_google import django.dispatch import settings +import logging + +if settings.USE_SPHINX_SEARCH == True: + from djangosphinx.models import SphinxSearch 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) @@ -46,7 +79,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) @@ -70,6 +102,14 @@ class Comment(models.Model): class Meta: ordering = ('-added_at',) db_table = u'comment' + + def save(self,**kwargs): + super(Comment,self).save(**kwargs) + try: + ping_google() + except Exception: + logging.debug('problem pinging google did you register you sitemap with google?') + def __unicode__(self): return self.comment @@ -137,6 +177,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) @@ -156,10 +197,23 @@ class Question(models.Model): comments = generic.GenericRelation(Comment) votes = generic.GenericRelation(Vote) flagged_items = generic.GenericRelation(FlaggedItem) - email_feeds = generic.GenericRelation(EmailFeed) + + if settings.USE_SPHINX_SEARCH == True: + search = SphinxSearch( + index=' '.join(settings.SPHINX_SEARCH_INDICES), + mode='SPH_MATCH_ALL', + ) + logging.debug('have sphinx search') objects = QuestionManager() + def delete(self): + super(Question, self).delete() + try: + ping_google() + except Exception: + logging.debug('problem pinging google did you register you sitemap with google?') + def save(self, **kwargs): """ Overridden to manually manage addition of tags when the object @@ -170,6 +224,10 @@ class Question(models.Model): """ initial_addition = (self.id is None) super(Question, self).save(**kwargs) + try: + ping_google() + except Exception: + logging.debug('problem pinging google did you register you sitemap with google?') if initial_addition: tags = Tag.objects.get_or_create_multiple(self.tagname_list(), self.author) @@ -184,7 +242,7 @@ class Question(models.Model): return u','.join([unicode(tag) for tag in self.tagname_list()]) def get_absolute_url(self): - return '%s%s' % (reverse('question', args=[self.id]), django_urlquote(self.title.replace(' ', '-'))) + return '%s%s' % (reverse('question', args=[self.id]), django_urlquote(slugify(self.title))) def has_favorite_by_user(self, user): if not user.is_authenticated(): @@ -212,17 +270,23 @@ class Question(models.Model): def get_latest_revision(self): return self.revisions.all()[0] - - def get_user_votes_in_answers(self, user): - content_type = ContentType.objects.get_for_model(Answer) - query_set = Vote.objects.extra( - tables = ['question', 'answer'], - where = ['question.id = answer.question_id AND question.id = %s AND vote.object_id = answer.id AND vote.content_type_id = %s AND vote.user_id = %s'], - params = [self.id, content_type.id, user.id] - ) - - return query_set - + + 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: @@ -251,7 +315,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}) @@ -285,6 +349,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) @@ -295,6 +364,12 @@ class FavoriteQuestion(models.Model): def __unicode__(self): return '[%s] favorited at %s' %(self.user, self.added_at) +class MarkedTag(models.Model): + TAG_MARK_REASONS = (('good',_('interesting')),('bad',_('ignored'))) + tag = models.ForeignKey(Tag, related_name='user_selections') + user = models.ForeignKey(User, related_name='tag_selections') + reason = models.CharField(max_length=16, choices=TAG_MARK_REASONS) + class QuestionRevision(models.Model): """A revision of a Question.""" question = models.ForeignKey(Question, related_name='revisions') @@ -314,7 +389,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.""" @@ -394,6 +470,16 @@ class Answer(models.Model): objects = AnswerManager() + get_comments = get_object_comments + get_last_update_info = post_get_last_update_info + + def save(self,**kwargs): + super(Answer,self).save(**kwargs) + try: + ping_google() + except Exception: + logging.debug('problem pinging google did you register you sitemap with google?') + def get_user_vote(self, user): votes = self.votes.filter(user=user) if votes.count() > 0: @@ -408,7 +494,7 @@ class Answer(models.Model): return self.question.title def get_absolute_url(self): - return '%s%s#%s' % (reverse('question', args=[self.question.id]), django_urlquote(self.question.title), self.id) + return '%s%s#%s' % (reverse('question', args=[self.question.id]), django_urlquote(slugify(self.question.title)), self.id) class Meta: db_table = u'answer' @@ -426,7 +512,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 @@ -549,7 +635,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=[django_urlquote(self.short_name)]) + return reverse('book', args=[django_urlquote(slugify(self.short_name))]) def __unicode__(self): return self.title @@ -588,7 +674,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 = ( @@ -597,11 +682,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')) @@ -619,6 +723,16 @@ 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) +User.add_to_class('hide_ignored_questions', models.BooleanField(default=False)) +User.add_to_class('tag_filter_setting', + models.CharField( + max_length=16, + choices=TAG_EMAIL_FILTER_CHOICES, + default='ignored' + ) + ) # custom signal tags_updated = django.dispatch.Signal(providing_args=["question"]) @@ -641,7 +755,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'<a href="%s">%s</a>' % (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/sitemap.py b/forum/sitemap.py new file mode 100644 index 00000000..dc97a009 --- /dev/null +++ b/forum/sitemap.py @@ -0,0 +1,11 @@ +from django.contrib.sitemaps import Sitemap +from forum.models import Question + +class QuestionsSitemap(Sitemap): + changefreq = 'daily' + priority = 0.5 + def items(self): + return Question.objects.exclude(deleted=True) + + def lastmod(self, obj): + return obj.last_activity_at diff --git a/forum/templatetags/extra_filters.py b/forum/templatetags/extra_filters.py index d8b8e61f..865cd33d 100644 --- a/forum/templatetags/extra_filters.py +++ b/forum/templatetags/extra_filters.py @@ -1,5 +1,10 @@ from django import template +<<<<<<< HEAD:forum/templatetags/extra_filters.py +======= +from django.core import serializers +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:forum/templatetags/extra_filters.py from forum import auth +import logging register = template.Library() @@ -9,6 +14,10 @@ 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 +26,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): @@ -86,3 +95,10 @@ def cnprog_intword(number): return number except: return number +<<<<<<< HEAD:forum/templatetags/extra_filters.py +======= + +@register.filter +def json_serialize(object): + return serializers.serialize('json',object) +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:forum/templatetags/extra_filters.py diff --git a/forum/templatetags/extra_tags.py b/forum/templatetags/extra_tags.py index 90ebb65b..b2199284 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 = ('<img width="%(size)s" height="%(size)s" ' +GRAVATAR_TEMPLATE = ('<img class="gravatar" width="%(size)s" height="%(size)s" ' 'src="http://www.gravatar.com/avatar/%(gravatar_hash)s' '?s=%(size)s&d=identicon&r=PG" ' 'alt="%(username)s\'s gravatar image" />') @@ -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,21 +236,31 @@ def convert2tagname_list(question): @register.simple_tag def diff_date(date, limen=2): - meses = ['Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun', 'Jul', 'Ago', 'Sept', 'Oct', 'Nov', 'Dic'] - current_time = datetime.datetime(*time.localtime()[0:6]) - diff = current_time - date - diff_days = diff.days - if diff_days > limen: - return "%s %s - %s @ %s:%s" % (meses[date.month-1], date.day, date.year, date.hour, date.minute) + 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, @@ -243,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 = os.path.normpath(out) + '?v=%d' % settings.RESOURCE_REVISION + return out.replace(' ','') + +@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 "<Smart If node>"
+
+ 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..62e70161 --- /dev/null +++ b/forum/urls.py @@ -0,0 +1,91 @@ +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 forum.sitemap import QuestionsSitemap +from django.utils.translation import ugettext as _ + +admin.autodiscover() +feeds = { + 'rss': RssLastestQuestionsFeed +} +sitemaps = { + 'questions': QuestionsSitemap +} + +APP_PATH = os.path.dirname(os.path.dirname(__file__)) +urlpatterns = patterns('', + url(r'^$', app.index, name='index'), + url(r'^sitemap.xml$', 'django.contrib.sitemaps.views.sitemap', {'sitemaps': sitemaps}), + (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<path>.*)$', 'django.views.static.serve', + {'document_root': os.path.join(APP_PATH, 'templates/content').replace('\\','/')} + ), + (r'^%s(?P<path>.*)$' % _('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<id>\d+)/%s$' % (_('answers/'), _('comments/')), app.answer_comments, name='answer_comments'), + url(r'^%s(?P<id>\d+)/%s$' % (_('answers/'), _('edit/')), app.edit_answer, name='edit_answer'), + url(r'^%s(?P<id>\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<id>\d+)/%s$' % (_('questions/'), _('edit/')), app.edit_question, name='edit_question'), + url(r'^%s(?P<id>\d+)/%s$' % (_('questions/'), _('close/')), app.close, name='close'), + url(r'^%s(?P<id>\d+)/%s$' % (_('questions/'), _('reopen/')), app.reopen, name='reopen'), + url(r'^%s(?P<id>\d+)/%s$' % (_('questions/'), _('answer/')), app.answer, name='answer'), + url(r'^%s(?P<id>\d+)/%s$' % (_('questions/'), _('vote/')), app.vote, name='vote'), + url(r'^%s(?P<id>\d+)/%s$' % (_('questions/'), _('revisions/')), app.question_revisions, name='question_revisions'), + url(r'^%s(?P<id>\d+)/%s$' % (_('questions/'), _('comments/')), app.question_comments, name='question_comments'), + url(r'^%s$' % _('command/'), app.ajax_command, name='call_ajax'), + + url(r'^%s(?P<object_id>\d+)/%s(?P<comment_id>\d+)/%s$' % (_('questions/'), _('comments/'),_('delete/')), \ + app.delete_comment, kwargs={'commented_object_type':'question'},\ + name='delete_question_comment'), + + url(r'^%s(?P<object_id>\d+)/%s(?P<comment_id>\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<id>\d+)//*' % _('question/'), app.question, name='question'), + url(r'^%s$' % _('tags/'), app.tags, name='tags'), + url(r'^%s(?P<tag>[^/]+)/$' % _('tags/'), app.tag, name='tag_questions'), + + url(r'^%s%s(?P<tag>[^/]+)/$' % (_('mark-tag/'),_('interesting/')), app.mark_tag, \ + kwargs={'reason':'good','action':'add'}, \ + name='mark_interesting_tag'), + + url(r'^%s%s(?P<tag>[^/]+)/$' % (_('mark-tag/'),_('ignored/')), app.mark_tag, \ + kwargs={'reason':'bad','action':'add'}, \ + name='mark_ignored_tag'), + + url(r'^%s(?P<tag>[^/]+)/$' % _('unmark-tag/'), app.mark_tag, \ + kwargs={'action':'remove'}, \ + name='mark_ignored_tag'), + + url(r'^%s$' % _('users/'),app.users, name='users'), + url(r'^%s(?P<id>\d+)/$' % _('moderate-user/'), app.moderate_user, name='moderate_user'), + url(r'^%s(?P<id>\d+)/%s$' % (_('users/'), _('edit/')), app.edit_user, name='edit_user'), + url(r'^%s(?P<id>\d+)//*' % _('users/'), app.user, name='user'), + url(r'^%s$' % _('badges/'),app.badges, name='badges'), + url(r'^%s(?P<id>\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<url>.*)/$', '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<short_name>[^/]+)/$' % (_('books/'), _('ask/')), app.ask_book, name='ask_book'), + url(r'^%s(?P<short_name>[^/]+)/$' % _('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 6c79bfbd..65b80d0e 100644 --- a/forum/views.py +++ b/forum/views.py @@ -2,20 +2,23 @@ import calendar from django.conf import settings from django.contrib.auth.decorators import login_required -from django.contrib.contenttypes.models import ContentType -from django.core.files.storage import default_storage -from django.core.paginator import EmptyPage -from django.core.paginator import InvalidPage -from django.core.paginator import Paginator -from django.http import Http404 -from django.http import HttpResponse -from django.http import HttpResponseRedirect -from django.shortcuts import get_object_or_404 -from django.shortcuts import render_to_response -from django.template import RequestContext -from django.utils import simplejson +from django.http import HttpResponseRedirect, HttpResponse, HttpResponseForbidden, Http404 +from django.core.paginator import Paginator, EmptyPage, InvalidPage +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.db.models import Count, Q +from django.contrib.contenttypes.models import ContentType from django.utils.translation import ugettext as _ +from django.utils.datastructures import SortedDict +from django.template.defaultfilters import slugify +from django.core.exceptions import PermissionDenied + +from utils.html import sanitize_html +from utils.decorators import ajax_method, ajax_login_required from markdown2 import Markdown import os.path import random @@ -29,7 +32,8 @@ from forum.diff import textDiff as htmldiff from forum.forms import * from forum.models import * from forum.user import * -from utils.html import sanitize_html +from forum import auth +from django_authopenid.util import get_next_url # used in index page INDEX_PAGE_SIZE = 20 @@ -65,42 +69,85 @@ 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 = "-added_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) + "latest":"-last_activity_at", + "hottest":"-answer_count", + "mostvoted":"-score", + } + view_id, orderby = _get_and_remember_questions_sort_method(request, view_dic, 'latest') + + page_size = request.session.get('pagesize', QUESTIONS_PAGE_SIZE) + questions = Question.objects.exclude(deleted=True).order_by(orderby)[:page_size] # RISK - inner join queries questions = questions.select_related() tags = Tag.objects.get_valid_tags(INDEX_TAGS_SIZE) awards = Award.objects.get_recent_awards() + (interesting_tag_names, ignored_tag_names) = (None, None) + if request.user.is_authenticated(): + pt = MarkedTag.objects.filter(user=request.user) + interesting_tag_names = pt.filter(reason='good').values_list('tag__name', flat=True) + ignored_tag_names = pt.filter(reason='bad').values_list('tag__name', flat=True) + + tags_autocomplete = _get_tags_cache_json() + return render_to_response('index.html', { - "questions": questions, - "tab_id": view_id, - "tags": tags, - "awards": awards[:INDEX_AWARD_SIZE], - }, context_instance=RequestContext(request)) + 'interesting_tag_names': interesting_tag_names, + 'tags_autocomplete': tags_autocomplete, + 'ignored_tag_names': ignored_tag_names, + "questions" : questions, + "tab_id" : view_id, + "tags" : tags, + "awards" : awards[:INDEX_AWARD_SIZE], + }, context_instance=RequestContext(request)) 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)) @@ -113,7 +160,7 @@ def questions(request, tagname=None, unanswered=False): List of Questions, Tagged questions, and Unanswered questions. """ # template file - # "questions.html" or "unanswered.html" + # "questions.html" or maybe index.html in the future template_file = "questions.html" # get pagesize from session, if failed then get default value pagesize = request.session.get("pagesize", 10) @@ -122,27 +169,65 @@ 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_dic = {"latest":"-added_at", "active":"-last_activity_at", "hottest":"-answer_count", "mostvoted":"-score" } + view_id, orderby = _get_and_remember_questions_sort_method(request,view_dic,'latest') # check if request is from tagged questions + qs = Question.objects.exclude(deleted=True) + if tagname is not None: - objects = Question.objects.get_questions_by_tag(tagname, orderby) - elif unanswered: - #check if request is from unanswered questions - template_file = "unanswered.html" - objects = Question.objects.get_unanswered_questions(orderby) - else: - objects = Question.objects.get_questions(orderby) + qs = qs.filter(tags__name = unquote(tagname)) - # RISK - inner join queries - objects = objects.select_related(depth=1); - objects_list = Paginator(objects, pagesize) + if unanswered: + qs = qs.exclude(answer_accepted=True) + + author_name = None + #user contributed questions & answers + if 'user' in request.GET: + try: + author_name = request.GET['user'] + u = User.objects.get(username=author_name) + qs = qs.filter(Q(author=u) | Q(answers__author=u)) + except User.DoesNotExist: + author_name = None + + if request.user.is_authenticated(): + uid_str = str(request.user.id) + qs = qs.extra( + select = SortedDict([ + ( + 'interesting_score', + 'SELECT COUNT(1) FROM forum_markedtag, question_tags ' + + 'WHERE forum_markedtag.user_id = %s ' + + 'AND forum_markedtag.tag_id = question_tags.tag_id ' + + 'AND forum_markedtag.reason = "good" ' + + 'AND question_tags.question_id = question.id' + ), + ]), + select_params = (uid_str,), + ) + if request.user.hide_ignored_questions: + ignored_tags = Tag.objects.filter(user_selections__reason='bad', + user_selections__user = request.user) + qs = qs.exclude(tags__in=ignored_tags) + else: + qs = qs.extra( + select = SortedDict([ + ( + 'ignored_score', + 'SELECT COUNT(1) FROM forum_markedtag, question_tags ' + + 'WHERE forum_markedtag.user_id = %s ' + + 'AND forum_markedtag.tag_id = question_tags.tag_id ' + + 'AND forum_markedtag.reason = "bad" ' + + 'AND question_tags.question_id = question.id' + ) + ]), + select_params = (uid_str, ) + ) + + qs = qs.select_related(depth=1).order_by(orderby) + + objects_list = Paginator(qs, pagesize) questions = objects_list.page(page) # Get related tags from this page objects @@ -150,28 +235,41 @@ def questions(request, tagname=None, unanswered=False): related_tags = Tag.objects.get_tags_by_questions(questions.object_list) else: related_tags = None - return render_to_response(template_file, { - "questions": questions, - "tab_id": view_id, - "questions_count": objects_list.count, - "tags": related_tags, - "searchtag": tagname, - "is_unanswered": unanswered, - "context": { - 'is_paginated': True, - 'pages': objects_list.num_pages, - 'page': page, - 'has_previous': questions.has_previous(), - 'has_next': questions.has_next(), - 'previous': questions.previous_page_number(), - 'next': questions.next_page_number(), - 'base_url': request.path + '?sort=%s&' % view_id, - 'pagesize': pagesize - }}, context_instance=RequestContext(request)) + tags_autocomplete = _get_tags_cache_json() + + # get the list of interesting and ignored tags + (interesting_tag_names, ignored_tag_names) = (None, None) + if request.user.is_authenticated(): + pt = MarkedTag.objects.filter(user=request.user) + interesting_tag_names = pt.filter(reason='good').values_list('tag__name', flat=True) + ignored_tag_names = pt.filter(reason='bad').values_list('tag__name', flat=True) -def create_new_answer(question=None, author=None, \ - added_at=None, wiki=False, \ - text='', email_notify=False): + return render_to_response(template_file, { + "questions" : questions, + "author_name" : author_name, + "tab_id" : view_id, + "questions_count" : objects_list.count, + "tags" : related_tags, + "tags_autocomplete" : tags_autocomplete, + "searchtag" : tagname, + "is_unanswered" : unanswered, + "interesting_tag_names": interesting_tag_names, + 'ignored_tag_names': ignored_tag_names, + "context" : { + 'is_paginated' : True, + 'pages': objects_list.num_pages, + 'page': page, + 'has_previous': questions.has_previous(), + 'has_next': questions.has_next(), + 'previous': questions.previous_page_number(), + 'next': questions.next_page_number(), + 'base_url' : request.path + '?sort=%s&' % view_id, + 'pagesize' : pagesize + }}, context_instance=RequestContext(request)) + +def create_new_answer( question=None, author=None,\ + added_at=None, wiki=False,\ + text='', email_notify=False): html = sanitize_html(markdowner.convert(text)) @@ -208,16 +306,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 +361,7 @@ def ask(request): if form.is_valid(): added_at = datetime.datetime.now() - title = strip_tags(form.cleaned_data['title']).strip() + title = strip_tags(form.cleaned_data['title'].strip()) wiki = form.cleaned_data['wiki'] tagnames = form.cleaned_data['tags'].strip() text = form.cleaned_data['text'] @@ -301,28 +395,42 @@ 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() tags = _get_tags_cache_json() return render_to_response('ask.html', { - 'form': form, - 'tags': tags, - }, context_instance=RequestContext(request)) + 'form' : form, + 'tags' : tags, + 'email_validation_faq_url':reverse('faq') + '#validate', + }, context_instance=RequestContext(request)) def question(request, id): try: page = int(request.GET.get('page', '1')) except ValueError: page = 1 - view_id = request.GET.get('sort', 'votes') - view_dic = {"latest":"-added_at", "oldest":"added_at", "votes":"-score"} + + 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): @@ -345,8 +453,11 @@ def question(request, id): vote_value = -1 if vote.is_upvote(): vote_value = 1 - user_answer_votes[vote.object_id] = vote_value - + user_answer_votes[answer.id] = vote_value + + if answers is not None: + answers = answers.order_by("-accepted", orderby) + filtered_answers = [] for answer in answers: if answer.deleted == True: @@ -357,8 +468,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, @@ -472,7 +613,6 @@ def _retag_question(request, question): 'tags': _get_tags_cache_json(), }, context_instance=RequestContext(request)) - def _edit_question(request, question): latest_revision = question.get_latest_revision() revision_form = None @@ -617,6 +757,7 @@ QUESTION_REVISION_TEMPLATE = ('<h1>%(title)s</h1>\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, @@ -625,16 +766,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(['<a class="post-tag">%s</a>' % 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, @@ -644,16 +784,16 @@ ANSWER_REVISION_TEMPLATE = ('<div class="text">%(html)s</div>') 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, @@ -691,8 +831,7 @@ def answer(request, id): ip_addr=request.META['REMOTE_ADDR'], ) anon.save() - return HttpResponseRedirect('/%s%s%s%s' % ( _('account/'), - _('signin/'),'?next=', question.get_absolute_url())) + return HttpResponseRedirect(reverse('user_signin_new_answer')) return HttpResponseRedirect(question.get_absolute_url()) @@ -707,7 +846,7 @@ def tags(request): if request.method == "GET": stag = request.GET.get("q", "").strip() - if len(stag) > 0: + 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 == "used": @@ -721,22 +860,21 @@ def tags(request): tags = objects_list.page(objects_list.num_pages) return render_to_response('tags.html', { - "tags": tags, - "stag": stag, - "tab_id": sortby, - "keywords": stag, - "context": { - 'is_paginated': is_paginated, - 'pages': objects_list.num_pages, - 'page': page, - 'has_previous': tags.has_previous(), - 'has_next': tags.has_next(), - 'previous': tags.previous_page_number(), - 'next': tags.next_page_number(), - 'base_url': '/tags/?sort=%s&' % sortby - } - - }, context_instance=RequestContext(request)) + "tags" : tags, + "stag" : stag, + "tab_id" : sortby, + "keywords" : stag, + "context" : { + 'is_paginated' : is_paginated, + 'pages': objects_list.num_pages, + 'page': page, + 'has_previous': tags.has_previous(), + 'has_next': tags.has_next(), + 'previous': tags.previous_page_number(), + 'next': tags.next_page_number(), + 'base_url' : reverse('tags') + '?sort=%s&' % sortby + } + }, context_instance=RequestContext(request)) def tag(request, tag): return questions(request, tagname=tag) @@ -925,7 +1063,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: @@ -934,13 +1073,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'] += '<br/>' + response_data['message'] = _('email update frequency has been set to daily') #response_data['status'] = 1 #responst_data['allowed'] = 1 else: @@ -950,12 +1095,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.' @@ -967,6 +1108,42 @@ def vote(request, id): data = simplejson.dumps(response_data) return HttpResponse(data, mimetype="application/json") +@ajax_login_required +def mark_tag(request, tag=None, **kwargs): + action = kwargs['action'] + ts = MarkedTag.objects.filter(user=request.user, tag__name=tag) + if action == 'remove': + logging.debug('deleting tag %s' % tag) + ts.delete() + else: + reason = kwargs['reason'] + if len(ts) == 0: + try: + t = Tag.objects.get(name=tag) + mt = MarkedTag(user=request.user, reason=reason, tag=t) + mt.save() + except: + pass + else: + ts.update(reason=reason) + return HttpResponse(simplejson.dumps(''), mimetype="application/json") + +@ajax_login_required +def ajax_toggle_ignored_questions(request): + if request.user.hide_ignored_questions: + new_hide_setting = False + else: + new_hide_setting = True + request.user.hide_ignored_questions = new_hide_setting + request.user.save() + +@ajax_method +def ajax_command(request): + if 'command' not in request.POST: + return HttpResponseForbidden(mimetype="application/json") + if request.POST['command'] == 'toggle-ignored-questions': + return ajax_toggle_ignored_questions(request) + def users(request): is_paginated = True sortby = request.GET.get('sort', 'reputation') @@ -986,11 +1163,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) @@ -998,22 +1175,22 @@ def users(request): users = objects_list.page(objects_list.num_pages) return render_to_response('users.html', { - "users": users, - "suser": suser, - "keywords": suser, - "tab_id": sortby, - "context": { - 'is_paginated': is_paginated, - 'pages': objects_list.num_pages, - 'page': page, - 'has_previous': users.has_previous(), - 'has_next': users.has_next(), - 'previous': users.previous_page_number(), - 'next': users.next_page_number(), - 'base_url': base_url - } - - }, context_instance=RequestContext(request)) + "users" : users, + "suser" : suser, + "keywords" : suser, + "tab_id" : sortby, + "context" : { + 'is_paginated' : is_paginated, + 'pages': objects_list.num_pages, + 'page': page, + 'has_previous': users.has_previous(), + 'has_next': users.has_next(), + 'previous': users.previous_page_number(), + 'next': users.next_page_number(), + 'base_url' : base_url + } + + }, context_instance=RequestContext(request)) def user(request, id): sort = request.GET.get('sort', 'stats') @@ -1023,6 +1200,26 @@ def user(request, id): 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) if request.user != user: @@ -1035,6 +1232,7 @@ def edit_user(request, id): from django_authopenid.views import set_new_email set_new_email(user, new_email) + user.username = sanitize_html(form.cleaned_data['username']) user.real_name = sanitize_html(form.cleaned_data['realname']) user.website = sanitize_html(form.cleaned_data['website']) user.location = sanitize_html(form.cleaned_data['city']) @@ -1052,8 +1250,9 @@ def edit_user(request, id): else: form = EditUserForm(user) return render_to_response('user_edit.html', { - 'form': form, - }, context_instance=RequestContext(request)) + 'form' : form, + 'gravatar_faq_url' : reverse('faq') + '#gravatar', + }, context_instance=RequestContext(request)) def user_stats(request, user_id, user_view): user = get_object_or_404(User, id=user_id) @@ -1097,74 +1296,99 @@ def user_stats(request, user_id, user_view): 'la_user_reputation')[:100] answered_questions = Question.objects.extra( - select={ - 'vote_up_count': 'answer.vote_up_count', - 'vote_down_count': 'answer.vote_down_count', - 'answer_id': 'answer.id', - 'accepted': 'answer.accepted', - 'vote_count': 'answer.score', - 'comment_count': 'answer.comment_count' - }, - tables=['question', 'answer'], - where=['answer.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] - ).distinct().values('comment_count', - 'id', - 'answer_id', - 'title', - 'author_id', - 'accepted', - 'vote_count', - 'answer_count', - 'vote_up_count', - 'vote_down_count')[:100] + select={ + 'vote_up_count' : 'answer.vote_up_count', + 'vote_down_count' : 'answer.vote_down_count', + 'answer_id' : 'answer.id', + 'accepted' : 'answer.accepted', + 'vote_count' : 'answer.score', + 'comment_count' : 'answer.comment_count' + }, + tables=['question', 'answer'], + 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] + ).distinct().values('comment_count', + 'id', + 'answer_id', + 'title', + 'author_id', + 'accepted', + 'vote_count', + 'answer_count', + 'vote_up_count', + 'vote_down_count')[:100] + up_votes = Vote.objects.get_up_vote_count_from_user(user) down_votes = Vote.objects.get_down_vote_count_from_user(user) votes_today = Vote.objects.get_votes_count_today_from_user(user) votes_total = VOTE_RULES['scope_votes_per_user_per_day'] - tags = user.created_tags.all().order_by('-used_count')[:50] + question_id_set = set(map(lambda v: v['id'], list(questions))) \ + | set(map(lambda v: v['id'], list(answered_questions))) + + user_tags = Tag.objects.filter(questions__id__in = question_id_set) try: from django.db.models import Count awards = Award.objects.extra( - select={'id': 'badge.id', 'name':'badge.name', 'description': 'badge.description', 'type': 'badge.type'}, - tables=['award', 'badge'], - order_by=['-awarded_at'], - where=['user_id=%s AND badge_id=badge.id'], - params=[user.id] - ).values('id', 'name', 'description', 'type') + select={'id': 'badge.id', + 'name':'badge.name', + 'description': 'badge.description', + 'type': 'badge.type'}, + tables=['award', 'badge'], + order_by=['-awarded_at'], + where=['user_id=%s AND badge_id=badge.id'], + params=[user.id] + ).values('id', 'name', 'description', 'type') total_awards = awards.count() - awards = awards.annotate(count=Count('badge__id')) + awards = awards.annotate(count = Count('badge__id')) + user_tags = user_tags.annotate(user_tag_usage_count=Count('name')) + except ImportError: awards = Award.objects.extra( - select={'id': 'badge.id', 'count': 'count(badge_id)', 'name':'badge.name', 'description': 'badge.description', 'type': 'badge.type'}, - tables=['award', 'badge'], - order_by=['-awarded_at'], - where=['user_id=%s AND badge_id=badge.id'], - params=[user.id] - ).values('id', 'count', 'name', 'description', 'type') + select={'id': 'badge.id', + 'count': 'count(badge_id)', + 'name':'badge.name', + 'description': 'badge.description', + 'type': 'badge.type'}, + tables=['award', 'badge'], + order_by=['-awarded_at'], + where=['user_id=%s AND badge_id=badge.id'], + params=[user.id] + ).values('id', 'count', 'name', 'description', 'type') total_awards = awards.count() awards.query.group_by = ['badge_id'] + user_tags = user_tags.extra( + select={'user_tag_usage_count': 'COUNT(1)',}, + order_by=['-user_tag_usage_count'], + ) + user_tags.query.group_by = ['name'] - 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, - "questions": questions, - "answered_questions": answered_questions, - "up_votes": up_votes, - "down_votes": down_votes, - "total_votes": up_votes + down_votes, - "votes_today_left": votes_total-votes_today, - "votes_total_per_day": votes_total, - "tags": tags, - "awards": awards, - "total_awards": total_awards, - }, context_instance=RequestContext(request)) + 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, + "view_user" : user, + "questions" : questions, + "answered_questions" : answered_questions, + "up_votes" : up_votes, + "down_votes" : down_votes, + "total_votes": up_votes + down_votes, + "votes_today_left": votes_total-votes_today, + "votes_total_per_day": votes_total, + "user_tags" : user_tags[:50], + "tags" : tags, + "awards": awards, + "total_awards" : total_awards, + }, context_instance=RequestContext(request)) def user_recent(request, user_id, user_view): user = get_object_or_404(User, id=user_id) @@ -1180,8 +1404,10 @@ def user_recent(request, user_id, user_view): self.type_id = type self.title = title self.summary = summary - self.title_link = u'/questions/%s/%s#%s' % (question_id, title, answer_id)\ - if int(answer_id) > 0 else u'/questions/%s/%s' % (question_id, title) + 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 += '#%s' % answer_id class AwardEvent: def __init__(self, time, type, id): @@ -1193,23 +1419,23 @@ def user_recent(request, user_id, user_view): activities = [] # ask questions questions = Activity.objects.extra( - select={ - 'title': 'question.title', - 'question_id': 'question.id', - 'active_at': 'activity.active_at', - 'activity_type': 'activity.activity_type' - }, - 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'], - params=[question_type_id, user_id, TYPE_ACTIVITY_ASK_QUESTION], - order_by=['-activity.active_at'] - ).values( - 'title', - 'question_id', - 'active_at', - 'activity_type' - ) + select={ + 'title' : 'question.title', + 'question_id' : 'question.id', + 'active_at' : 'activity.active_at', + 'activity_type' : 'activity.activity_type' + }, + tables=['activity', 'question'], + where=['activity.content_type_id = %s AND activity.object_id = ' + + '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( + 'title', + 'question_id', + 'active_at', + 'activity_type' + ) if len(questions) > 0: questions = [(Event(q['active_at'], q['activity_type'], q['title'], '', '0', \ q['question_id'])) for q in questions] @@ -1217,25 +1443,26 @@ def user_recent(request, user_id, user_view): # answers answers = Activity.objects.extra( - select={ - 'title': 'question.title', - 'question_id': 'question.id', - 'answer_id': 'answer.id', - 'active_at': 'activity.active_at', - 'activity_type': 'activity.activity_type' - }, - 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'], - params=[answer_type_id, user_id, TYPE_ACTIVITY_ANSWER], - order_by=['-activity.active_at'] - ).values( - 'title', - 'question_id', - 'answer_id', - 'active_at', - 'activity_type' - ) + select={ + 'title' : 'question.title', + 'question_id' : 'question.id', + 'answer_id' : 'answer.id', + 'active_at' : 'activity.active_at', + 'activity_type' : 'activity.activity_type' + }, + tables=['activity', 'answer', 'question'], + where=['activity.content_type_id = %s AND activity.object_id = answer.id AND ' + + '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( + 'title', + 'question_id', + 'answer_id', + 'active_at', + 'activity_type' + ) if len(answers) > 0: answers = [(Event(q['active_at'], q['activity_type'], q['title'], '', q['answer_id'], \ q['question_id'])) for q in answers] @@ -1243,25 +1470,26 @@ def user_recent(request, user_id, user_view): # question comments comments = Activity.objects.extra( - select={ - 'title': 'question.title', - 'question_id': 'comment.object_id', - 'added_at': 'comment.added_at', - 'activity_type': 'activity.activity_type' - }, - tables=['activity', 'question', 'comment'], - - 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'], - params=[comment_type_id, question_type_id, user_id, TYPE_ACTIVITY_COMMENT_QUESTION], - order_by=['-comment.added_at'] - ).values( - 'title', - 'question_id', - 'added_at', - 'activity_type' - ) + select={ + 'title' : 'question.title', + 'question_id' : 'comment.object_id', + 'added_at' : 'comment.added_at', + 'activity_type' : 'activity.activity_type' + }, + tables=['activity', 'question', 'comment'], + + 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 AND ' + + 'question.deleted=0'], + params=[comment_type_id, question_type_id, user_id, TYPE_ACTIVITY_COMMENT_QUESTION], + order_by=['-comment.added_at'] + ).values( + 'title', + 'question_id', + 'added_at', + 'activity_type' + ) if len(comments) > 0: comments = [(Event(q['added_at'], q['activity_type'], q['title'], '', '0', \ @@ -1270,28 +1498,29 @@ def user_recent(request, user_id, user_view): # answer comments comments = Activity.objects.extra( - select={ - 'title': 'question.title', - 'question_id': 'question.id', - 'answer_id': 'answer.id', - 'added_at': 'comment.added_at', - 'activity_type': 'activity.activity_type' - }, - tables=['activity', 'question', 'answer', 'comment'], - - 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'], - params=[comment_type_id, answer_type_id, user_id, TYPE_ACTIVITY_COMMENT_ANSWER], - order_by=['-comment.added_at'] - ).values( - 'title', - 'question_id', - 'answer_id', - 'added_at', - 'activity_type' - ) + select={ + 'title' : 'question.title', + 'question_id' : 'question.id', + 'answer_id' : 'answer.id', + 'added_at' : 'comment.added_at', + 'activity_type' : 'activity.activity_type' + }, + tables=['activity', 'question', 'answer', 'comment'], + + 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 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( + 'title', + 'question_id', + 'answer_id', + 'added_at', + 'activity_type' + ) if len(comments) > 0: comments = [(Event(q['added_at'], q['activity_type'], q['title'], '', q['answer_id'], \ @@ -1300,26 +1529,27 @@ def user_recent(request, user_id, user_view): # question revisions revisions = Activity.objects.extra( - select={ - 'title': 'question_revision.title', - 'question_id': 'question_revision.question_id', - 'added_at': 'activity.active_at', - 'activity_type': 'activity.activity_type', - 'summary': 'question_revision.summary' - }, - tables=['activity', 'question_revision'], - where=['activity.content_type_id = %s AND activity.object_id = question_revision.id 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], - order_by=['-activity.active_at'] - ).values( - 'title', - 'question_id', - 'added_at', - 'activity_type', - 'summary' - ) + select={ + 'title' : 'question_revision.title', + 'question_id' : 'question_revision.question_id', + 'added_at' : 'activity.active_at', + 'activity_type' : 'activity.activity_type', + 'summary' : 'question_revision.summary' + }, + 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], + order_by=['-activity.active_at'] + ).values( + 'title', + 'question_id', + 'added_at', + 'activity_type', + 'summary' + ) if len(revisions) > 0: revisions = [(Event(q['added_at'], q['activity_type'], q['title'], q['summary'], '0', \ @@ -1328,30 +1558,31 @@ def user_recent(request, user_id, user_view): # answer revisions revisions = Activity.objects.extra( - select={ - 'title': 'question.title', - 'question_id': 'question.id', - 'answer_id': 'answer.id', - 'added_at': 'activity.active_at', - 'activity_type': 'activity.activity_type', - 'summary': 'answer_revision.summary' - }, - tables=['activity', 'answer_revision', 'question', 'answer'], - - 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 ' + - 'activity.activity_type=%s'], - params=[answer_revision_type_id, user_id, TYPE_ACTIVITY_UPDATE_ANSWER], - order_by=['-activity.active_at'] - ).values( - 'title', - 'question_id', - 'added_at', - 'answer_id', - 'activity_type', - 'summary' - ) + select={ + 'title' : 'question.title', + 'question_id' : 'question.id', + 'answer_id' : 'answer.id', + 'added_at' : 'activity.active_at', + 'activity_type' : 'activity.activity_type', + 'summary' : 'answer_revision.summary' + }, + tables=['activity', 'answer_revision', 'question', 'answer'], + + 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'] + ).values( + 'title', + 'question_id', + 'added_at', + 'answer_id', + 'activity_type', + 'summary' + ) if len(revisions) > 0: revisions = [(Event(q['added_at'], q['activity_type'], q['title'], q['summary'], \ @@ -1360,24 +1591,25 @@ def user_recent(request, user_id, user_view): # accepted answers accept_answers = Activity.objects.extra( - select={ - 'title': 'question.title', - 'question_id': 'question.id', - 'added_at': 'activity.active_at', - 'activity_type': 'activity.activity_type', - }, - 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.question_id=question.id AND activity.activity_type=%s'], - params=[answer_type_id, user_id, TYPE_ACTIVITY_MARK_ANSWER], - order_by=['-activity.active_at'] - ).values( - 'title', - 'question_id', - 'added_at', - 'activity_type', - ) + select={ + 'title' : 'question.title', + 'question_id' : 'question.id', + 'added_at' : 'activity.active_at', + 'activity_type' : 'activity.activity_type', + }, + 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'] + ).values( + 'title', + 'question_id', + 'added_at', + 'activity_type', + ) if len(accept_answers) > 0: accept_answers = [(Event(q['added_at'], q['activity_type'], q['title'], '', '0', \ q['question_id'])) for q in accept_answers] @@ -1405,13 +1637,13 @@ def user_recent(request, user_id, user_view): activities.sort(lambda x, y: cmp(y.time, x.time)) - 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, - "activities": activities[:user_view.data_size] - }, context_instance=RequestContext(request)) + 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, + "activities" : activities[:user_view.data_size] + }, context_instance=RequestContext(request)) def user_responses(request, user_id, user_view): """ @@ -1421,9 +1653,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, slugify(title), answer_id) + self.titlelink = reverse('question', args=[question_id]) + u'%s#%s' % (slugify(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] @@ -1433,30 +1665,31 @@ def user_responses(request, user_id, user_view): user = get_object_or_404(User, id=user_id) responses = [] answers = Answer.objects.extra( - select={ - 'title': 'question.title', - 'question_id': 'question.id', - 'answer_id': 'answer.id', - 'added_at': 'answer.added_at', - 'html': 'answer.html', - 'username': 'auth_user.username', - 'user_id': 'auth_user.id' - }, - select_params=[user_id], - tables=['answer', 'question', 'auth_user'], - where=['answer.question_id = question.id AND answer.deleted=0 AND question.deleted = 0 AND ' + - 'question.author_id = %s AND answer.author_id <> %s AND answer.author_id=auth_user.id'], - params=[user_id, user_id], - order_by=['-answer.id'] - ).values( - 'title', - 'question_id', - 'answer_id', - 'added_at', - 'html', - 'username', - 'user_id' - ) + select={ + 'title' : 'question.title', + 'question_id' : 'question.id', + 'answer_id' : 'answer.id', + 'added_at' : 'answer.added_at', + 'html' : 'answer.html', + 'username' : 'auth_user.username', + 'user_id' : 'auth_user.id' + }, + select_params=[user_id], + tables=['answer', 'question', 'auth_user'], + where=['answer.question_id = question.id AND answer.deleted=0 AND question.deleted = 0 AND '+ + 'question.author_id = %s AND answer.author_id <> %s AND answer.author_id=auth_user.id'], + params=[user_id, user_id], + order_by=['-answer.id'] + ).values( + 'title', + 'question_id', + 'answer_id', + 'added_at', + 'html', + 'username', + 'user_id' + ) + if len(answers) > 0: answers = [(Response(TYPE_RESPONSE['QUESTION_ANSWERED'], a['title'], a['question_id'], a['answer_id'], a['added_at'], a['username'], a['user_id'], a['html'])) for a in answers] @@ -1465,27 +1698,27 @@ def user_responses(request, user_id, user_view): # question comments comments = Comment.objects.extra( - select={ - 'title': 'question.title', - 'question_id': 'comment.object_id', - 'added_at': 'comment.added_at', - 'comment': 'comment.comment', - 'username': 'auth_user.username', - 'user_id': 'auth_user.id' - }, - tables=['question', 'auth_user', 'comment'], - where=['question.deleted = 0 AND question.author_id = %s AND comment.object_id=question.id AND ' + - 'comment.content_type_id=%s AND comment.user_id <> %s AND comment.user_id = auth_user.id'], - params=[user_id, question_type_id, user_id], - order_by=['-comment.added_at'] - ).values( - 'title', - 'question_id', - 'added_at', - 'comment', - 'username', - 'user_id' - ) + select={ + 'title' : 'question.title', + 'question_id' : 'comment.object_id', + 'added_at' : 'comment.added_at', + 'comment' : 'comment.comment', + 'username' : 'auth_user.username', + 'user_id' : 'auth_user.id' + }, + tables=['question', 'auth_user', 'comment'], + where=['question.deleted = 0 AND question.author_id = %s AND comment.object_id=question.id AND '+ + 'comment.content_type_id=%s AND comment.user_id <> %s AND comment.user_id = auth_user.id'], + params=[user_id, question_type_id, user_id], + order_by=['-comment.added_at'] + ).values( + 'title', + 'question_id', + 'added_at', + 'comment', + 'username', + 'user_id' + ) if len(comments) > 0: comments = [(Response(TYPE_RESPONSE['QUESTION_COMMENTED'], c['title'], c['question_id'], @@ -1719,88 +1952,117 @@ def user_favorites(request, user_id, user_view): "view_user": user }, 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) - 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, - }, context_instance=RequestContext(request)) + if request.method == 'POST': + email_feeds_form = EditUserEmailFeedsForm(request.POST) + tag_filter_form = TagFilterSelectionForm(request.POST, instance=user) + if email_feeds_form.is_valid() and tag_filter_form.is_valid(): + + action_status = None + tag_filter_saved = tag_filter_form.save() + if tag_filter_saved: + action_status = _('changes saved') + if 'save' in request.POST: + feeds_saved = email_feeds_form.save(user) + if feeds_saved: + action_status = _('changes saved') + elif 'stop_email' in request.POST: + email_stopped = email_feeds_form.reset().save(user) + initial_values = EditUserEmailFeedsForm.NO_EMAIL_INITIAL + email_feeds_form = EditUserEmailFeedsForm(initial=initial_values) + if email_stopped: + action_status = _('email updates canceled') + else: + email_feeds_form = EditUserEmailFeedsForm() + email_feeds_form.set_initial_values(user) + tag_filter_form = TagFilterSelectionForm(instance=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, + 'email_feeds_form':email_feeds_form, + 'tag_filter_selection_form':tag_filter_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) - json_comments.append({"id": comment.id, - "object_id": obj.id, - "add_date": comment.added_at.strftime('%Y-%m-%d'), - "text": comment.comment, - "user_display_name": comment_user.username, - "user_url": "/users/%s/%s" % (comment_user.id, comment_user.username), - "delete_url": delete_url - }) + #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, + "comment_age" : diff_date(comment.added_at), + "text" : comment.comment, + "user_display_name" : 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, - }, context_instance=RequestContext(request)) + 'next' : get_next_url(request), + }, context_instance=RequestContext(request)) def badges(request): badges = Badge.objects.all().order_by('type') @@ -1810,9 +2072,10 @@ def badges(request): my_badges.query.group_by = ['badge_id'] return render_to_response('badges.html', { - 'badges': badges, - 'mybadges': my_badges, - }, context_instance=RequestContext(request)) + 'badges' : badges, + 'mybadges' : my_badges, + 'feedback_faq_url' : reverse('feedback'), + }, context_instance=RequestContext(request)) def badge(request, id): badge = get_object_or_404(Badge, id=id) @@ -1863,8 +2126,8 @@ def upload(request): if not file_name_suffix in settings.ALLOW_FILE_TYPES: raise FileTypeNotAllow - # genetate new file name - new_file_name = str(time.time()).replace('.', str(random.randint(0, 100000))) + file_name_suffix + # generate new file name + new_file_name = str(time.time()).replace('.', str(random.randint(0,100000))) + file_name_suffix # use default storage to store file default_storage.save(new_file_name, f) # check file size @@ -1887,7 +2150,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): """ @@ -2010,9 +2273,10 @@ def ask_book(request, short_name): tags = _get_tags_cache_json() return render_to_response('ask.html', { - 'form': form, - 'tags': tags, - }, context_instance=RequestContext(request)) + 'form' : form, + 'tags' : tags, + 'email_validation_faq_url': reverse('faq') + '#validate', + }, context_instance=RequestContext(request)) def search(request): """ @@ -2027,11 +2291,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" @@ -2070,10 +2334,16 @@ def search(request): view_id = "latest" orderby = "-added_at" - objects = Question.objects.filter(deleted=False).extra(where=['title like %s'], params=['%' + keywords + '%']).order_by(orderby) + if settings.USE_SPHINX_SEARCH == True: + #search index is now free of delete questions and answers + #so there is not "antideleted" filtering here + objects = Question.search.query(keywords) + #no related selection either because we're relying on full text search here + else: + objects = Question.objects.filter(deleted=False).extra(where=['title like %s'], params=['%' + keywords + '%']).order_by(orderby) + # RISK - inner join queries + objects = objects.select_related(); - # RISK - inner join queries - objects = objects.select_related(); objects_list = Paginator(objects, pagesize) questions = objects_list.page(page) 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 Binary files differindex cd8de560..502c1075 100644 --- a/locale/en/LC_MESSAGES/django.mo +++ b/locale/en/LC_MESSAGES/django.mo diff --git a/locale/en/LC_MESSAGES/django.po b/locale/en/LC_MESSAGES/django.po index b5e5c549..3f554733 100644 --- a/locale/en/LC_MESSAGES/django.po +++ b/locale/en/LC_MESSAGES/django.po @@ -8,347 +8,331 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2009-08-19 00:41+0000\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"POT-Creation-Date: 2009-12-09 08:54-0800\n" +"PO-Revision-Date: 2009-12-15 16:53-0600\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: 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 "" +#: django_authopenid/forms.py:70 +msgid "choose a username" +msgstr "Choose screen name" -#: urls.py:30 -msgid "about/" +#: django_authopenid/forms.py:76 +msgid "user name is required" msgstr "" -#: urls.py:31 -msgid "faq/" +#: django_authopenid/forms.py:77 +msgid "sorry, this name is taken, please choose another" msgstr "" -#: urls.py:32 -msgid "privacy/" +#: django_authopenid/forms.py:78 +msgid "sorry, this name is not allowed, please choose another" msgstr "" -#: urls.py:33 -msgid "logout/" +#: django_authopenid/forms.py:79 +msgid "sorry, there is no user with this name" msgstr "" -#: urls.py:34 urls.py:35 urls.py:36 urls.py:48 forum/models.py:418 -msgid "answers/" +#: django_authopenid/forms.py:80 +msgid "sorry, we have a serious error - user name is taken by several users" msgstr "" -#: urls.py:34 urls.py:46 -msgid "comments/" +#: django_authopenid/forms.py:81 +msgid "user name can only consist of letters, empty space and underscore" msgstr "" -#: urls.py:35 urls.py:40 urls.py:54 templates/user_info.html:34 -msgid "edit/" -msgstr "" +#: django_authopenid/forms.py:116 +msgid "your email address" +msgstr "Your email <i>(never shared)</i>" -#: urls.py:36 urls.py:45 -msgid "revisions/" +#: django_authopenid/forms.py:117 +msgid "email address is required" 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/" +#: django_authopenid/forms.py:118 +msgid "please enter a valid email address" msgstr "" -#: urls.py:38 urls.py:64 -msgid "ask/" +#: django_authopenid/forms.py:119 +msgid "this email is already used by someone else, please choose another" msgstr "" -#: urls.py:39 -msgid "unanswered/" +#: django_authopenid/forms.py:163 django_authopenid/views.py:118 +msgid "i-names are not supported" msgstr "" -#: urls.py:41 -msgid "close/" +#: django_authopenid/forms.py:219 +msgid "Account with this name already exists on the forum" msgstr "" -#: urls.py:42 -msgid "reopen/" +#: django_authopenid/forms.py:220 +msgid "can't have two logins to the same account yet, sorry." msgstr "" -#: urls.py:43 -msgid "answer/" +#: django_authopenid/forms.py:242 +msgid "Please enter valid username and password (both are case-sensitive)." msgstr "" -#: urls.py:44 -msgid "vote/" +#: django_authopenid/forms.py:245 django_authopenid/forms.py:295 +msgid "This account is inactive." msgstr "" -#: urls.py:47 urls.py:48 django_authopenid/urls.py:29 -msgid "delete/" +#: django_authopenid/forms.py:247 +msgid "Login failed." msgstr "" -#: urls.py:50 -msgid "question/" +#: django_authopenid/forms.py:249 +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:251 +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:253 +msgid "Please enter user name" msgstr "" -#: urls.py:56 urls.py:57 -msgid "badges/" +#: django_authopenid/forms.py:291 +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:313 +msgid "choose password" +msgstr "Password" -#: urls.py:58 -msgid "markread/" +#: django_authopenid/forms.py:314 +msgid "password is required" msgstr "" -#: urls.py:60 -msgid "nimda/" -msgstr "" +#: django_authopenid/forms.py:317 +msgid "retype password" +msgstr "Password <i>(please retype)</i>" -#: urls.py:62 -msgid "upload/" +#: django_authopenid/forms.py:318 +msgid "please, retype your password" msgstr "" -#: urls.py:63 urls.py:64 urls.py:65 -msgid "books/" +#: django_authopenid/forms.py:319 +msgid "sorry, entered passwords did not match, please try again" msgstr "" -#: urls.py:66 -msgid "search/" +#: django_authopenid/forms.py:344 +msgid "Current password" msgstr "" -#: django_authopenid/forms.py:67 django_authopenid/views.py:102 -msgid "i-names are not supported" +#: django_authopenid/forms.py:346 +msgid "New password" msgstr "" -#: django_authopenid/forms.py:102 -msgid "" -"Usernames can only contain letters, numbers and " -"underscores" +#: django_authopenid/forms.py:348 +msgid "Retype new password" msgstr "" -#: django_authopenid/forms.py:109 +#: django_authopenid/forms.py:359 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:371 +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:435 +msgid "Your user name (<i>required</i>)" 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:450 +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:44 forum/urls.py:48 +msgid "delete/" msgstr "" -#: django_authopenid/views.py:108 +#: django_authopenid/views.py:124 #, 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:532 +msgid "Welcome email subject line" +msgstr "Welcome to the Q&A forum" -#: django_authopenid/views.py:508 +#: django_authopenid/views.py:627 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:639 django_authopenid/views.py:645 +#, python-format +msgid "your email needs to be validated see %(details_url)s" msgstr "" "Your email needs to be validated. Please see details <a " -"id='validate_email_alert' href=\"/faq#validate\">here</a>." +"id='validate_email_alert' href='%(details_url)s'>here</a>." + +#: django_authopenid/views.py:666 +msgid "Email verification subject line" +msgstr "Verification Email from Q&A forum" + +#: django_authopenid/views.py:752 +msgid "your email was not changed" +msgstr "" -#: django_authopenid/views.py:682 django_authopenid/views.py:834 +#: django_authopenid/views.py:799 django_authopenid/views.py:951 #, 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:803 django_authopenid/views.py:958 #, python-format msgid "The OpenID %s isn't associated to current user logged in" msgstr "" -#: django_authopenid/views.py:694 +#: django_authopenid/views.py:811 msgid "Email Changed." msgstr "" -#: django_authopenid/views.py:769 +#: django_authopenid/views.py:886 msgid "This OpenID is already associated with another account." msgstr "" -#: django_authopenid/views.py:774 +#: django_authopenid/views.py:891 #, python-format msgid "OpenID %s is now associated with your account." msgstr "" -#: django_authopenid/views.py:844 +#: django_authopenid/views.py:961 msgid "Account deleted." msgstr "" -#: django_authopenid/views.py:884 +#: django_authopenid/views.py:1004 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:1017 +msgid "A new password and the activation link were sent to your email address." msgstr "" -#: django_authopenid/views.py:927 +#: django_authopenid/views.py:1047 #, python-format msgid "" "Could not change password. Confirmation key '%s' is not " "registered." msgstr "" -#: django_authopenid/views.py:936 +#: django_authopenid/views.py:1056 msgid "" "Can not change password. User don't exist anymore in our " "database." msgstr "" -#: django_authopenid/views.py:945 +#: django_authopenid/views.py:1065 #, 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 @@ -375,90 +359,102 @@ msgstr "" msgid "spam or advertising" msgstr "" -#: forum/const.py:56 +#: forum/const.py:57 msgid "question" msgstr "" -#: forum/const.py:57 templates/book.html:110 +#: forum/const.py:58 templates/book.html:110 msgid "answer" msgstr "" -#: forum/const.py:58 +#: forum/const.py:59 msgid "commented question" msgstr "" -#: forum/const.py:59 +#: forum/const.py:60 msgid "commented answer" msgstr "" -#: forum/const.py:60 +#: forum/const.py:61 msgid "edited question" msgstr "" -#: forum/const.py:61 +#: forum/const.py:62 msgid "edited answer" msgstr "" -#: forum/const.py:62 +#: forum/const.py:63 msgid "received award" msgstr "received badge" -#: forum/const.py:63 +#: forum/const.py:64 msgid "marked best answer" msgstr "" -#: forum/const.py:64 +#: forum/const.py:65 msgid "upvoted" msgstr "" -#: forum/const.py:65 +#: forum/const.py:66 msgid "downvoted" msgstr "" -#: forum/const.py:66 +#: forum/const.py:67 msgid "canceled vote" msgstr "" -#: forum/const.py:67 +#: forum/const.py:68 msgid "deleted question" msgstr "" -#: forum/const.py:68 +#: forum/const.py:69 msgid "deleted answer" msgstr "" -#: forum/const.py:69 +#: forum/const.py:70 msgid "marked offensive" msgstr "" -#: forum/const.py:70 +#: forum/const.py:71 msgid "updated tags" msgstr "" -#: forum/const.py:71 +#: forum/const.py:72 msgid "selected favorite" msgstr "" -#: forum/const.py:72 +#: forum/const.py:73 msgid "completed user profile" msgstr "" -#: forum/const.py:83 +#: forum/const.py:74 +msgid "email update sent to user" +msgstr "" + +#: forum/const.py:85 msgid "[closed]" msgstr "" -#: forum/const.py:84 +#: forum/const.py:86 msgid "[deleted]" msgstr "" -#: forum/const.py:85 +#: forum/const.py:87 forum/views.py:849 forum/views.py:868 msgid "initial version" msgstr "" -#: forum/const.py:86 +#: forum/const.py:88 msgid "retagged" msgstr "" +#: forum/const.py:92 +msgid "exclude ignored tags" +msgstr "" + +#: forum/const.py:92 +msgid "allow only interesting tags" +msgstr "" + #: forum/feed.py:18 msgid " - " msgstr "" @@ -467,151 +463,399 @@ 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:52 +msgid "question/" +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:28 templates/header.html.py:62 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:61 templates/index.html.py:73 +#: templates/post_contributor_info.html:7 +#: templates/question_summary_list_roll.html:26 +#: templates/question_summary_list_roll.html:38 templates/questions.html:92 +#: templates/questions.html.py:104 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 "Screen name" +msgstr "" + +#: forum/forms.py:199 msgid "Real name" msgstr "" -#: forum/forms.py:177 +#: forum/forms.py:200 msgid "Website" msgstr "" -#: forum/forms.py:178 +#: forum/forms.py:201 msgid "Location" msgstr "" -#: forum/forms.py:179 +#: forum/forms.py:202 msgid "Date of birth" msgstr "" -#: forum/forms.py:179 +#: forum/forms.py:202 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:203 templates/authopenid/settings.html:21 msgid "Profile" msgstr "" -#: forum/forms.py:207 forum/forms.py:208 +#: forum/forms.py:231 forum/forms.py:232 msgid "this email has already been registered, please use another one" msgstr "" -#: forum/models.py:246 +#: forum/forms.py:238 +msgid "Choose email tag filter" +msgstr "" + +#: forum/forms.py:245 forum/forms.py:246 +msgid "weekly" +msgstr "" + +#: forum/forms.py:245 forum/forms.py:246 +msgid "no email" +msgstr "" + +#: forum/forms.py:246 +msgid "daily" +msgstr "" + +#: forum/forms.py:261 +msgid "Asked by me" +msgstr "" + +#: forum/forms.py:264 +msgid "Answered by me" +msgstr "" + +#: forum/forms.py:267 +msgid "Individually selected" +msgstr "" + +#: forum/forms.py:270 +msgid "Entire forum (tag filtered)" +msgstr "" + +#: forum/models.py:51 +msgid "Entire forum" +msgstr "" + +#: forum/models.py:52 +msgid "Questions that I asked" +msgstr "" + +#: forum/models.py:53 +msgid "Questions that I answered" +msgstr "" + +#: forum/models.py:54 +msgid "Individually selected questions" +msgstr "" + +#: forum/models.py:57 +msgid "Weekly" +msgstr "" + +#: forum/models.py:58 +msgid "Daily" +msgstr "" + +#: forum/models.py:59 +msgid "No email" +msgstr "" + +#: forum/models.py:301 #, python-format msgid "%(author)s modified the question" msgstr "" -#: forum/models.py:250 +#: forum/models.py:305 #, python-format msgid "%(people)s posted %(new_answer_count)s new answers" msgstr "" -#: forum/models.py:255 +#: forum/models.py:310 #, python-format msgid "%(people)s commented the question" msgstr "" -#: forum/models.py:260 +#: forum/models.py:315 #, python-format msgid "%(people)s commented answers" msgstr "" -#: forum/models.py:262 +#: forum/models.py:317 #, python-format msgid "%(people)s commented an answer" msgstr "" -#: forum/models.py:306 forum/models.py:418 -msgid "revisions" +#: forum/models.py:348 +msgid "interesting" +msgstr "" + +#: forum/models.py:348 +msgid "ignored" msgstr "" -#: forum/models.py:441 templates/badges.html:51 +#: forum/models.py:511 templates/badges.html:53 msgid "gold" msgstr "" -#: forum/models.py:442 templates/badges.html:59 +#: forum/models.py:512 templates/badges.html:61 msgid "silver" msgstr "" -#: forum/models.py:443 templates/badges.html:66 +#: forum/models.py:513 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:48 +msgid "answers/" +msgstr "" + +#: forum/urls.py:29 forum/urls.py:41 forum/urls.py:44 forum/urls.py:48 +msgid "comments/" +msgstr "" + +#: forum/urls.py:30 forum/urls.py:35 forum/urls.py:70 +#: templates/user_info.html:45 +msgid "edit/" +msgstr "" + +#: forum/urls.py:31 forum/urls.py:40 +msgid "revisions/" +msgstr "" + +#: 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:44 +msgid "questions/" +msgstr "" + +#: forum/urls.py:33 forum/urls.py:80 +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:42 +msgid "command/" +msgstr "" + +#: forum/urls.py:53 forum/urls.py:54 +msgid "tags/" +msgstr "" + +#: forum/urls.py:56 forum/urls.py:60 +msgid "mark-tag/" +msgstr "" + +#: forum/urls.py:56 +msgid "interesting/" +msgstr "" + +#: forum/urls.py:60 +msgid "ignored/" +msgstr "" + +#: forum/urls.py:64 +msgid "unmark-tag/" +msgstr "" + +#: forum/urls.py:68 forum/urls.py:70 forum/urls.py:71 +msgid "users/" +msgstr "" + +#: forum/urls.py:69 +msgid "moderate-user/" +msgstr "" + +#: forum/urls.py:72 forum/urls.py:73 +msgid "badges/" +msgstr "" +"Your subscription is saved, but email address %(email)s needs to be " +"validated, please see <a href='%(details_url)s'>more details here</a>" + +#: forum/views.py:1022 +msgid "email update frequency has been set to daily" +msgstr "" + +#: 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/urls.py:74 +msgid "messages/" +msgstr "" + +#: forum/urls.py:74 +msgid "markread/" +msgstr "" + +#: forum/urls.py:76 +msgid "nimda/" +msgstr "" + +#: forum/urls.py:78 +msgid "upload/" +msgstr "" + +#: forum/urls.py:79 forum/urls.py:80 forum/urls.py:81 +msgid "books/" +msgstr "" + +#: forum/urls.py:82 +msgid "search/" +msgstr "" +"<p>Please remember that you can always <a href='%(link)s'>adjust</a> frequency of the " +"email updates or turn them off entirely.<br/>If you believe that this message " +"was sent in an error, please email about it the forum administrator at %(email)" +"s.</p>" +"<p>Sincerely,</p><p>Your friendly Q&A forum server.</p>" + +#: forum/urls.py:83 +msgid "feedback/" +msgstr "" + +#: forum/urls.py:84 +msgid "account/" +msgstr "" + #: forum/user.py:16 templates/user_tabs.html:7 msgid "overview" msgstr "" @@ -648,17 +892,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,59 +928,154 @@ 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:141 +msgid "Q&A forum feedback" +msgstr "" + +#: forum/views.py:142 +msgid "Thanks for the feedback!" +msgstr "" + +#: forum/views.py:150 +msgid "We look forward to hearing your feedback! Please, give it next time :)" msgstr "" -#: forum/views.py:948 +#: forum/views.py:1150 #, 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 <a href='/faq#validate'>more details here</a>" +"validated, please see <a href='%(details_url)s'>more details here</a>" -#: forum/views.py:1874 -msgid "uploading images is limited to users with >60 reputation points" +#: forum/views.py:1158 +msgid "email update frequency has been set to daily" msgstr "" -#: forum/views.py:1876 +#: forum/views.py:2032 +msgid "changes saved" +msgstr "" + +#: forum/views.py:2038 +msgid "email updates canceled" +msgstr "" + +#: forum/views.py:2207 +msgid "uploading images is limited to users with >60 reputation points" +msgstr "sorry, file uploading requires karma >60" + +#: forum/views.py:2209 msgid "allowed file types are 'jpg', 'jpeg', 'gif', 'bmp', 'png', 'tiff'" msgstr "" -#: forum/views.py:1878 +#: forum/views.py:2211 #, python-format msgid "maximum upload file size is %sK" msgstr "" -#: forum/views.py:1880 +#: forum/views.py:2213 #, 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:160 +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:161 +#, 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] "" +"<p>Dear %(name)s,</p></p>The following question has been updated on the Q&A " +"forum:</p>" +msgstr[1] "" +"<p>Dear %(name)s,</p><p>The following %(num)d questions have been updated on " +"the Q&A forum:</p>" + +#: forum/management/commands/send_email_alerts.py:172 +msgid "new question" +msgstr "" + +#: forum/management/commands/send_email_alerts.py:182 +#, python-format +msgid "There is also one question which was recently " +msgid_plural "" +"There are also %(num)d more questions which were recently updated " +msgstr[0] "" +msgstr[1] "" + +#: forum/management/commands/send_email_alerts.py:187 +msgid "" +"Perhaps you could look up previously sent forum reminders in your mailbox." +msgstr "" + +#: forum/management/commands/send_email_alerts.py:191 +#, python-format +msgid "" +"go to %(link)s to change frequency of email updates or %(email)s " +"administrator" +msgstr "" +"<p>Please remember that you can always <a href='%(link)s'>adjust</a> " +"frequency of the email updates or turn them off entirely.<br/>If you believe " +"that this message was sent in an error, please email about it the forum " +"administrator at %(email)s.</p><p>Sincerely,</p><p>Your friendly Q&A forum " +"server.</p>" + +#: forum/templatetags/extra_tags.py:163 forum/templatetags/extra_tags.py:192 +#: templates/header.html:33 msgid "badges" msgstr "" -#: forum/templatetags/extra_tags.py:144 forum/templatetags/extra_tags.py:171 +#: forum/templatetags/extra_tags.py:164 forum/templatetags/extra_tags.py:191 msgid "reputation points" +msgstr "karma" + +#: forum/templatetags/extra_tags.py:247 +msgid "%b %d at %H:%M" +msgstr "" + +#: forum/templatetags/extra_tags.py:249 +msgid "%b %d '%y at %H:%M" msgstr "" -#: forum/templatetags/extra_tags.py:225 -msgid " ago" +#: forum/templatetags/extra_tags.py:251 +msgid "2 days ago" +msgstr "" + +#: forum/templatetags/extra_tags.py:253 +msgid "yesterday" +msgstr "" + +#: forum/templatetags/extra_tags.py:255 +#, python-format +msgid "%(hr)d hour ago" +msgid_plural "%(hr)d hours ago" +msgstr[0] "" +msgstr[1] "" + +#: forum/templatetags/extra_tags.py:257 +#, python-format +msgid "%(min)d min ago" +msgid_plural "%(min)d mins ago" +msgstr[0] "" +msgstr[1] "" + +#: middleware/anon_user.py:33 +#, python-format +msgid "first time greeting with %(url)s" msgstr "" #: templates/404.html:24 @@ -780,6 +1119,11 @@ msgstr "" #: templates/404.html:43 msgid "see all tags" msgstr "" +"<span class=\"strong big\">You are welcome to start submitting your question " +"anonymously</span>. 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/500.html:22 msgid "sorry, system error" @@ -796,6 +1140,11 @@ msgstr "" #: templates/500.html:28 msgid "see latest questions" msgstr "" +"<span class='strong big'>Looks like your email address, %(email)s has not " +"yet been validated.</span> To post messages you must verify your email, " +"please see <a href='%(email_validation_faq_url)s'>more details here</a>." +"<br>You can submit your question now and validate email after that. Your " +"question will saved as pending meanwhile. " #: templates/500.html:29 msgid "see tags" @@ -805,55 +1154,90 @@ msgstr "" msgid "About" msgstr "" -#: templates/answer_edit.html:4 templates/answer_edit.html.py:47 +#: templates/about.html:21 +msgid "" +"<strong>CNPROG <span class=\"orange\">Q&A</span></strong> is a " +"collaboratively edited question\n" +" and answer site created for the <strong>CNPROG</strong> " +"community.\n" +" " +msgstr "" + +#: templates/about.html:25 +msgid "" +"Here you can <strong>ask</strong> and <strong>answer</strong> questions, " +"<strong>comment</strong>\n" +" and <strong>vote</strong> for the questions of others and their answers. " +"Both questions and answers\n" +" <strong>can be revised</strong> and improved. Questions can be " +"<strong>tagged</strong> with\n" +" the relevant keywords to simplify future access and organize the " +"accumulated material." +msgstr "" + +#: templates/about.html:31 +msgid "" +"This <span class=\"orange\">Q&A</span> site is moderated by its members, " +"hopefully - including yourself!\n" +" Moderation rights are gradually assigned to the site users based on the " +"accumulated <strong>\"reputation\"</strong>\n" +" points. These points are added to the users account when others vote for " +"his/her questions or answers.\n" +" These points (very) roughly reflect the level of trust of the community." +msgstr "" + +#: 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:25 +#: templates/question_edit.html.py: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 "" +"If your questions and answers are highly voted, your contribution to this " +"Q&A community will be recognized with the variety of badges." -#: templates/answer_edit.html:47 templates/question_edit.html:65 -#: templates/question_retag.html:52 templates/revisions_answer.html:36 -#: templates/revisions_question.html:36 +#: templates/answer_edit.html:48 templates/question_edit.html:66 +#: templates/question_retag.html:53 templates/revisions_answer.html:38 +#: templates/revisions_question.html:38 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:52 templates/revisions_question.html:52 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:87 templates/authopenid/changeemail.html:40 msgid "Cancel" msgstr "" @@ -877,80 +1261,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 "" "<span class=\"strong big\">You are welcome to start submitting your question " -"anonymously</span> - 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</span>. 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 "" "<span class='strong big'>Looks like your email address, %(email)s has not " "yet been validated.</span> To post messages you must verify your email, " -"please see <a href='/faq#validate'>more details here</a>.<br>You can submit " -"your question now and validate email after that. Your question will saved as " -"pending meanwhile. " +"please see <a href='%(email_validation_faq_url)s'>more details here</a>." +"<br>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 +1352,7 @@ msgstr "" msgid "Badges summary" msgstr "" -#: templates/badges.html:17 templates/user_stats.html:73 +#: templates/badges.html:17 msgid "Badges" msgstr "" @@ -977,38 +1363,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: <strong>gold</strong>, " "<strong>silver</strong> and <strong>bronze</strong> (their meanings are " "described on the right). In the future there will be many types of badges at " -"each level. <strong>Please give us your <a href=\"/faq#feedback\">feedback</" -"a></strong> - what kinds of badges would you like to see and suggest the " -"activity for which those badges might be awarded." +"each level. <strong>Please give us your <a href='%(feedback_faq_url)" +"s'>feedback</a></strong> - 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 +1469,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:49 +#: templates/question_summary_list_roll.html:14 templates/questions.html:80 +#: templates/unanswered.html:39 templates/users_questions.html:32 msgid "votes" msgstr "" @@ -1089,15 +1479,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:50 +#: templates/question_summary_list_roll.html:15 templates/questions.html:81 +#: 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:105 +#: templates/question.html:480 templates/question_summary_list_roll.html:52 +#: templates/questions.html:136 templates/tags.html:49 +#: templates/unanswered.html:95 templates/unanswered.html.py:122 +#: templates/users_questions.html:52 msgid "using tags" msgstr "" @@ -1105,7 +1497,7 @@ msgstr "" msgid "subscribe to book RSS feed" msgstr "" -#: templates/book.html:147 templates/index.html:118 +#: templates/book.html:147 templates/index.html:156 msgid "subscribe to the questions feed" msgstr "" @@ -1189,16 +1581,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." @@ -1213,7 +1607,7 @@ msgid "" "type of moderation task." msgstr "" -#: templates/faq.html:53 templates/user_votes.html:14 +#: templates/faq.html:53 templates/user_votes.html:15 msgid "upvote" msgstr "" @@ -1225,7 +1619,7 @@ msgstr "" msgid "add comments" msgstr "" -#: templates/faq.html:66 templates/user_votes.html:16 +#: templates/faq.html:66 templates/user_votes.html:17 msgid "downvote" msgstr "" @@ -1237,51 +1631,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 "" -"<form style='margin:0;padding:0;' action='/email/sendkey/'><p><span class=" -"\"bigger strong\">How?</span> If you have just set or changed your email " -"address - <strong>check your email and click the included link</strong>." -"<br>The link contains a key generated specifically for you. You can also " -"<button style='display:inline' type='submit'><strong>get a new key</strong></" -"button> and check your email again.</p></form><span class=\"bigger strong" -"\">Why?</span> Email validation is required to make sure that <strong>only " -"you can post messages</strong> on your behalf and to <strong>minimize spam</" -"strong> posts.<br>With email you can <strong>subscribe for updates</strong> " -"on the most interesting questions. Also, when you sign up for the first time " -"- create a unique <a href='/faq#gravatar'><strong>gravatar</strong></a> " -"personal image.</p>" - -#: templates/faq.html:106 +#: templates/faq.html:104 +#, python-format +msgid "" +"how to validate email info with %(send_email_key_url)s %(gravatar_faq_url)s" +msgstr "" +"<form style='margin:0;padding:0;' action='%(send_email_key_url)s'><p><span " +"class=\"bigger strong\">How?</span> If you have just set or changed your " +"email address - <strong>check your email and click the included link</" +"strong>.<br>The link contains a key generated specifically for you. You can " +"also <button style='display:inline' type='submit'><strong>get a new key</" +"strong></button> and check your email again.</p></form><span class=\"bigger " +"strong\">Why?</span> Email validation is required to make sure that " +"<strong>only you can post messages</strong> on your behalf and to " +"<strong>minimize spam</strong> posts.<br>With email you can " +"<strong>subscribe for updates</strong> on the most interesting questions. " +"Also, when you sign up for the first time - create a unique <a href='%" +"(gravatar_faq_url)s'><strong>gravatar</strong></a> personal image.</p>" + +#: 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 "" "<strong>Gravatar</strong> means <strong>g</strong>lobally <strong>r</" @@ -1292,238 +1688,308 @@ msgstr "" "your image</strong> at <a href='http://gravatar.com'><strong>gravatar.com</" "strong></a>" -#: 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 <a href=\"/questions/ask\">ask</a> your question, help make our " +"Please <a href='%(ask_question_url)s'>ask</a> 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:27 templates/header.html.py:61 msgid "questions" msgstr "" -#: templates/faq.html:126 templates/index.html:123 +#: templates/faq.html:128 templates/index.html:161 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" +" <span class='big strong'>Dear %(user_name)s</span>, we look " +"forward to hearing your feedback. \n" +" Please type and send us your message below.\n" +" " +msgstr "" + +#: templates/feedback.html:24 +msgid "" +"\n" +" <span class='big strong'>Dear visitor</span>, 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:13 templates/index.html:119 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:14 templates/index.html:120 +#: templates/question_edit_tips.html:17 msgid "faq" msgstr "" #: templates/footer.html:10 -msgid "wiki" -msgstr "" - -#: templates/footer.html:11 msgid "blog" msgstr "" -#: templates/footer.html:12 +#: templates/footer.html:11 msgid "contact us" msgstr "" -#: templates/footer.html:13 +#: templates/footer.html:12 msgid "privacy policy" msgstr "" -#: templates/footer.html:14 +#: templates/footer.html:21 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:21 msgid "back to home page" msgstr "" -#: templates/header.html:31 templates/header.html.py:65 +#: templates/header.html:29 templates/header.html.py:63 msgid "users" msgstr "" -#: templates/header.html:33 +#: templates/header.html:31 msgid "books" msgstr "" -#: templates/header.html:36 +#: templates/header.html:34 msgid "unanswered questions" msgstr "unanswered" -#: templates/header.html:40 +#: templates/header.html:38 msgid "my profile" msgstr "" -#: templates/header.html:44 +#: templates/header.html:42 msgid "ask a question" msgstr "" -#: templates/header.html:59 +#: templates/header.html:57 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:25 templates/questions.html:8 msgid "Questions" msgstr "" -#: templates/index.html:24 +#: templates/index.html:27 msgid "last updated questions" msgstr "" -#: templates/index.html:24 templates/questions.html:25 -#: templates/unanswered.html:20 +#: templates/index.html:27 templates/questions.html:47 +#: templates/unanswered.html:21 msgid "newest" msgstr "" -#: templates/index.html:25 templates/questions.html:27 +#: templates/index.html:28 templates/questions.html:49 msgid "hottest questions" msgstr "" -#: templates/index.html:25 templates/questions.html:27 +#: templates/index.html:28 templates/questions.html:49 msgid "hottest" msgstr "" -#: templates/index.html:26 templates/questions.html:28 +#: templates/index.html:29 templates/questions.html:50 msgid "most voted questions" msgstr "" -#: templates/index.html:26 templates/questions.html:28 +#: templates/index.html:29 templates/questions.html:50 msgid "most voted" msgstr "" -#: templates/index.html:27 +#: templates/index.html:30 msgid "all questions" msgstr "" -#: templates/index.html:47 templates/questions.html:45 -#: templates/unanswered.html:36 templates/users_questions.html:35 +#: templates/index.html:48 templates/question_summary_list_roll.html:13 +#: templates/questions.html:79 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:80 templates/index.html.py:94 +#: templates/questions.html:111 templates/questions.html.py:125 +#: templates/unanswered.html:70 templates/unanswered.html.py:84 +msgid "Posted:" +msgstr "" + +#: templates/index.html:83 templates/index.html.py:88 +#: templates/questions.html:114 templates/questions.html.py:119 +#: templates/unanswered.html:73 templates/unanswered.html.py:78 +msgid "Updated:" +msgstr "" + +#: templates/index.html:105 templates/question.html:480 +#: templates/question_summary_list_roll.html:52 templates/questions.html:136 +#: 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:116 msgid "welcome to website" -msgstr "" +msgstr "Welcome to Q&A forum" -#: templates/index.html:89 +#: templates/index.html:127 msgid "Recent tags" msgstr "" -#: templates/index.html:94 templates/question.html:125 +#: templates/index.html:132 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:135 templates/index.html.py:161 msgid "popular tags" msgstr "tags" -#: templates/index.html:102 +#: templates/index.html:140 msgid "Recent awards" msgstr "Recent badges" -#: templates/index.html:108 +#: templates/index.html:146 msgid "given to" msgstr "" -#: templates/index.html:113 +#: templates/index.html:151 msgid "all awards" msgstr "all badges" -#: templates/index.html:118 +#: templates/index.html:156 msgid "subscribe to last 30 questions by RSS" msgstr "" -#: templates/index.html:123 +#: templates/index.html:161 msgid "Still looking for more? See" msgstr "" -#: templates/index.html:123 +#: templates/index.html:161 msgid "complete list of questions" msgstr "list of all questions" -#: templates/index.html:123 +#: templates/index.html:161 templates/authopenid/signup.html:18 msgid "or" msgstr "" -#: templates/index.html:123 +#: templates/index.html:161 msgid "Please help us answer" msgstr "" -#: templates/index.html:123 +#: templates/index.html:161 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 <strong>Logout</strong> will log you out from the Q&A forum.</" -"p><p>If you wish to sign off completely - please make sure to log out from " -"your OpenID provider." +"Clicking <strong>Logout</strong> will log you out from the forumbut will not " +"sign you off from your OpenID provider.</p><p>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 +2017,35 @@ 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 +msgid "asked" +msgstr "" + +#: templates/post_contributor_info.html:22 +msgid "answered" +msgstr "" + +#: templates/post_contributor_info.html:24 +msgid "posted" +msgstr "" + +#: templates/post_contributor_info.html:45 +msgid "updated" +msgstr "" + #: templates/privacy.html:6 templates/privacy.html.py:11 msgid "Privacy policy" msgstr "" @@ -1558,6 +2053,9 @@ 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 +2064,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 +2075,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 +2086,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 +2103,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:58 templates/revisions_question.html:58 msgid "edit" msgstr "" -#: templates/question.html:138 templates/question.html.py:332 -msgid "delete" -msgstr "" - -#: templates/question.html:143 +#: templates/question.html:145 msgid "reopen" msgstr "" -#: templates/question.html:148 +#: templates/question.html:149 msgid "close" msgstr "" -#: templates/question.html:154 templates/question.html.py:345 +#: templates/question.html:155 templates/question.html.py:299 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:156 templates/question.html.py:300 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" -msgstr "" - -#: templates/question.html:216 templates/question.html.py:402 -#: templates/revisions_answer.html:63 templates/revisions_question.html:63 -msgid "asked" +#: templates/question.html:164 templates/question.html.py:311 +msgid "delete" msgstr "" -#: templates/question.html:246 templates/question.html.py:429 -msgid "comments" +#: templates/question.html:182 templates/question.html.py:331 +msgid "delete this comment" msgstr "" -#: templates/question.html:247 templates/question.html.py:430 +#: templates/question.html:193 templates/question.html.py:342 msgid "add comment" -msgstr "" +msgstr "post a comment" + +#: templates/question.html:197 +#, python-format +msgid "" +"\n" +" see <strong>one</strong> more \n" +" " +msgid_plural "" +"\n" +" see <strong>%(counter)s</strong> " +"more\n" +" " +msgstr[0] "" +msgstr[1] "" + +#: templates/question.html:203 +#, python-format +msgid "" +"\n" +" see <strong>one</strong> more " +"comment\n" +" " +msgid_plural "" +"\n" +" see <strong>%(counter)s</strong> " +"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:311 msgid "undelete" msgstr "" -#: templates/question.html:339 -msgid "answer permanent link" +#: templates/question.html:346 +#, python-format +msgid "" +"\n" +" see <strong>one</" +"strong> more \n" +" " +msgid_plural "" +"\n" +" see <strong>%" +"(counter)s</strong> more\n" +" " +msgstr[0] "" +msgstr[1] "" + +#: templates/question.html:352 +#, python-format +msgid "" +"\n" +" see <strong>one</" +"strong> more comment\n" +" " +msgid_plural "" +"\n" +" see <strong>%" +"(counter)s</strong> 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 "" +"<strong>Notify me</strong> 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 "" +"<strong>Notify me</strong> 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 <a href='%(profile_url)s?" +"sort=email_subscriptions'>adjust frequency</a> of email updates)" + +#: templates/question.html:396 +msgid "once you sign in you will be able to subscribe for any updates here" +msgstr "" +"<span class='strong'>Here</span> (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 "" -"<span class='strong big'>You are now not logged in</span> but you can answer " -"first and then login" +"<span class='strong big'>Please start posting your answer anonymously</span> " +"- 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 <strong>substantial " +"answer</strong>, for discussions, <strong>please use comments</strong> and " +"<strong>please do remember to vote</strong> (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 "" +"<span class='big strong'>You are welcome to answer your own question</span>, " +"but please make sure to give an <strong>answer</strong>. Remember that you " +"can always <strong>revise your original question</strong>. Please " +"<strong>use comments for discussions</strong> and <strong>please don't " +"forget to vote :)</strong> 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 "" +"<span class='big strong'>Please try to give a substantial answer</span>. If " +"you wanted to comment on the question or answer, just <strong>use the " +"commenting tool</strong>. Please remember that you can always <strong>revise " +"your answers</strong> - no need to answer the same question twice. Also, " +"please <strong>don't forget to vote</strong> - 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,66 +2411,87 @@ 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:29 msgid "Found by tags" msgstr "Tagged questions" -#: templates/questions.html:23 +#: templates/questions.html:33 +msgid "Search results" +msgstr "" + +#: templates/questions.html:35 msgid "Found by title" msgstr "" -#: templates/questions.html:23 +#: templates/questions.html:39 templates/unanswered.html:8 +#: templates/unanswered.html.py:19 +msgid "Unanswered questions" +msgstr "" + +#: templates/questions.html:41 msgid "All questions" msgstr "" -#: templates/questions.html:25 templates/unanswered.html:20 +#: templates/questions.html:47 templates/unanswered.html:21 msgid "most recently asked questions" msgstr "" -#: templates/questions.html:26 +#: templates/questions.html:48 msgid "most recently updated questions" msgstr "" -#: templates/questions.html:26 +#: templates/questions.html:48 msgid "active" msgstr "" -#: templates/questions.html:109 +#: templates/questions.html:144 +msgid "Did not find anything?" +msgstr "" + +#: templates/questions.html:147 +msgid "Did not find what you were looking for?" +msgstr "" + +#: templates/questions.html:149 +msgid "Please, post your question!" +msgstr "" + +#: templates/questions.html:163 #, python-format msgid "" "\n" -"\t\t\thave total %(q_num)s questions tagged %(tagname)s\n" -"\t\t\t" +" have total %(q_num)s questions tagged %(tagname)s\n" +" " msgid_plural "" "\n" -"\t\t\thave total %(q_num)s questions tagged %(tagname)s\n" -"\t\t\t" +" have total %(q_num)s questions tagged %(tagname)s\n" +" " msgstr[0] "" "\n" "<div class=\"questions-count\">%(q_num)s</div><p>question tagged</p><p><span " @@ -1860,16 +2501,41 @@ msgstr[1] "" "<div class=\"questions-count\">%(q_num)s</div><p>questions tagged</p><div " "class=\"tags\"><span class=\"tag\">%(tagname)s</span></div>" -#: templates/questions.html:116 +#: templates/questions.html:171 #, python-format msgid "" "\n" -"\t\t\t\thave total %(q_num)s questions containing %(searchtitle)s\n" -"\t\t\t\t" +" have total %(q_num)s questions containing %(searchtitle)" +"s in full text\n" +" " msgid_plural "" "\n" -"\t\t\t\thave total %(q_num)s questions containing %(searchtitle)s\n" -"\t\t\t\t" +" have total %(q_num)s questions containing %(searchtitle)" +"s in full text\n" +" " +msgstr[0] "" +"\n" +"<div class=\"questions-count\">%(q_num)s</div><p>question " +"containing <strong><span class=\"darkred\">%(searchtitle)s</span></strong></" +"p>" +msgstr[1] "" +"\n" +"<div class=\"questions-count\">%(q_num)s</div><p>questions " +"containing <strong><span class=\"darkred\">%(searchtitle)s</span></strong></" +"p>" + +#: templates/questions.html:177 +#, python-format +msgid "" +"\n" +" have total %(q_num)s questions containing %(searchtitle)" +"s\n" +" " +msgid_plural "" +"\n" +" have total %(q_num)s questions containing %(searchtitle)" +"s\n" +" " msgstr[0] "" "\n" "<div class=\"questions-count\">%(q_num)s</div><p>question with title " @@ -1881,55 +2547,81 @@ msgstr[1] "" "containing <strong><span class=\"darkred\">%(searchtitle)s</span></strong></" "p>" -#: templates/questions.html:122 +#: templates/questions.html:185 #, python-format msgid "" "\n" -"\t\t\t\thave total %(q_num)s questions\n" -"\t\t\t\t" +" have total %(q_num)s unanswered questions\n" +" " msgid_plural "" "\n" -"\t\t\t\thave total %(q_num)s questions\n" -"\t\t\t\t" +" have total %(q_num)s unanswered questions\n" +" " +msgstr[0] "" +"\n" +"<div class=\"questions-count\">%(q_num)s</div><p>question without an " +"accepted answer</p>" +msgstr[1] "" +"\n" +"<div class=\"questions-count\">%(q_num)s</div><p>questions without an " +"accepted answer</p>" + +#: templates/questions.html:191 +#, python-format +msgid "" +"\n" +" have total %(q_num)s questions\n" +" " +msgid_plural "" +"\n" +" have total %(q_num)s questions\n" +" " msgstr[0] "" "\n" "<div class=\"questions-count\">%(q_num)s</div><p>question</p>" msgstr[1] "" "\n" -"<div class=\"questions-count\">%(q_num)s</div><p>questions</p>" -#: templates/questions.html:131 +"<div class=\"questions-count\">%(q_num)s</div><p>questions<p>" + +#: templates/questions.html:201 msgid "latest questions info" msgstr "<strong>Newest</strong> questions are shown first." -#: templates/questions.html:135 +#: templates/questions.html:205 msgid "Questions are sorted by the <strong>time of last update</strong>." msgstr "" -#: templates/questions.html:136 +#: templates/questions.html:206 msgid "Most recently answered ones are shown first." msgstr "<strong>Most recently answered</strong> questions are shown first." -#: templates/questions.html:140 +#: templates/questions.html:210 msgid "Questions sorted by <strong>number of responses</strong>." msgstr "Questions sorted by the <strong>number of answers</strong>." -#: templates/questions.html:141 +#: templates/questions.html:211 msgid "Most answered questions are shown first." msgstr " " -#: templates/questions.html:145 +#: templates/questions.html:215 msgid "Questions are sorted by the <strong>number of votes</strong>." msgstr "" -#: templates/questions.html:146 +#: templates/questions.html:216 msgid "Most voted questions are shown first." msgstr "" -#: templates/questions.html:153 templates/unanswered.html:102 +#: templates/questions.html:224 templates/unanswered.html:118 msgid "Related tags" msgstr "Tags" +#: templates/questions.html:227 templates/tag_selector.html:10 +#: templates/tag_selector.html.py:27 +#, python-format +msgid "see questions tagged '%(tag_name)s'" +msgstr "" + #: templates/reopen.html:6 templates/reopen.html.py:16 msgid "Reopen question" msgstr "" @@ -1958,11 +2650,41 @@ msgstr "" msgid "Reopen this question" msgstr "" -#: templates/revisions_answer.html:7 templates/revisions_answer.html.py:36 -#: templates/revisions_question.html:8 templates/revisions_question.html:36 +#: templates/revisions_answer.html:7 templates/revisions_answer.html.py:38 +#: templates/revisions_question.html:8 templates/revisions_question.html:38 msgid "Revision history" msgstr "" +#: templates/revisions_answer.html:50 templates/revisions_question.html:50 +msgid "click to hide/show revision" +msgstr "" + +#: templates/tag_selector.html:4 +msgid "Interesting tags" +msgstr "" + +#: templates/tag_selector.html:14 +#, python-format +msgid "remove '%(tag_name)s' from the list of interesting tags" +msgstr "" + +#: templates/tag_selector.html:20 templates/tag_selector.html.py:37 +msgid "Add" +msgstr "" + +#: templates/tag_selector.html:21 +msgid "Ignored tags" +msgstr "" + +#: templates/tag_selector.html:31 +#, python-format +msgid "remove '%(tag_name)s' from the list of ignored tags" +msgstr "" + +#: templates/tag_selector.html:40 +msgid "keep ingored questions hidden" +msgstr "" + #: templates/tags.html:6 templates/tags.html.py:30 msgid "Tag list" msgstr "" @@ -1995,16 +2717,12 @@ msgstr "" msgid "Nothing found" msgstr "" -#: templates/unanswered.html:7 templates/unanswered.html.py:18 -msgid "Unanswered questions" -msgstr "" - -#: templates/unanswered.html:97 +#: templates/unanswered.html:114 #, python-format msgid "have %(num_q)s unanswered questions" msgstr "" -"<div class=\"questions-count\">%(num_q)s</div><strong>unanswered</strong> " -"questions" +"<div class=\"questions-count\">%(num_q)s</div>questions <strong>without " +"accepted answers</strong>" #: templates/user_edit.html:6 msgid "Edit user profile" @@ -2019,139 +2737,203 @@ msgid "image associated with your email address" msgstr "" #: templates/user_edit.html:31 -msgid "avatar" -msgstr "<a href='/faq#gravatar'>gravatar</a>" +#, python-format +msgid "avatar, see %(gravatar_faq_url)s" +msgstr "<a href='%(gravatar_faq_url)s'>gravatar</a>" -#: 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:86 templates/user_email_subscriptions.html:20 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 "" +"<span class='big strong'>Adjust frequency of email updates.</span> Receive " +"updates on interesting questions by email, <strong><br/>help the community</" +"strong> by answering questions of your colleagues. If you do not wish to " +"receive emails - select 'no email' on all items below.<br/>Updates are only " +"sent when there is any new activity on selected items." + +#: templates/user_email_subscriptions.html:21 +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" +" <span class=\"count\">1</span> Question\n" +" " +msgid_plural "" +"\n" +" <span class=\"count\">%(counter)s</span> Questions\n" +" " +msgstr[0] "" +msgstr[1] "" -#: templates/user_stats.html:10 -msgid "User questions" -msgstr "" +#: templates/user_stats.html:23 +#, python-format +msgid "" +"\n" +" <span class=\"count\">1</span> Answer\n" +" " +msgid_plural "" +"\n" +" <span class=\"count\">%(counter)s</span> 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" +" <span class=\"count\">1</span> Vote\n" +" " +msgid_plural "" +"\n" +" <span class=\"count\">%(cnt)s</span> 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" +" <span class=\"count\">1</span> Tag\n" +" " +msgid_plural "" +"\n" +" <span class=\"count\">%(counter)s</span> 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' " +msgid "" +"see other questions with %(view_user)s's contributions tagged '%(tag_name)s' " msgstr "" +#: templates/user_stats.html:115 +#, python-format +msgid "" +"\n" +" <span class=\"count\">1</span> Badge\n" +" " +msgid_plural "" +"\n" +" <span class=\"count\">%(counter)s</span> 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 +2943,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 +2980,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 +3002,65 @@ msgstr "" "you'd like to use another email for <strong>update subscriptions</strong>." "<br>Currently you are using <strong>%(email)s</strong>" -#: 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 "" +"<span class='strong big'>Please enter your email address in the box below.</" +"span> Valid email address is required on this Q&A forum. If you like, " +"you can <strong>receive updates</strong> on interesting questions or entire " +"forum via email. Also, your email is used to create a unique <a href='%" +"(gravatar_faq_url)s'><strong>gravatar</strong></a> 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 "" "<strong>Your new Email:</strong> (will <strong>not</strong> be shown to " "anyone, must be valid)" #: templates/authopenid/changeemail.html:31 -#: templates/authopenid/signin.html:154 -msgid "Password" +msgid "Your Email" +msgstr "" +"<strong>Your Email</strong> (<i>must be valid, never shown to others</i>)" + +#: templates/authopenid/changeemail.html:38 +msgid "Save Email" msgstr "" -#: templates/authopenid/changeemail.html:42 +#: 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 "" "<span class=\"strong big\">An email with a validation link has been sent to %" "(email)s.</span> Please <strong>follow the emailed link</strong> with your " "web browser. Email validation is necessary to help insure the proper use of " -"email on <span class=\"orange\">Q&A</span> forum. If you would like " -"to use <strong>another email</strong>, please <a href=\"/email/change/" -"\"><strong>change it again</strong></a>." +"email on <span class=\"orange\">Q&A</span>. If you would like to use " +"<strong>another email</strong>, please <a href='%(change_email_url)" +"s'><strong>change it again</strong></a>." -#: 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 "" "<span class=\"strong big\">Your email address %(email)s has not been changed." "</span> If you decide to change it later - you can always do it by editing " -"it in your user profile or by using the <a href=\"/email/change/" -"\"><strong>previous form</strong></a> again." +"it in your user profile or by using the <a href='%(change_email_url)" +"s'><strong>previous form</strong></a> 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 +3069,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 "" "<span class=\"big strong\">Thank you for verifying your email!</span> Now " @@ -2291,11 +3082,11 @@ msgstr "" "updates</strong> - then will be notified about changes <strong>once a day</" "strong> 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 +3094,12 @@ msgstr "" "validated before</span> so the new key was not sent. You can <a href='%" "(change_link)s'>change</a> 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 +3109,148 @@ 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 "" +"<span class='strong'>To change your password</span> 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 "" "<p><span class=\"big strong\">You are here for the first time with your %" "(provider)s login.</span> Please create your <strong>screen name</strong> " "and save your <strong>email</strong> address. Saved email address will let " "you <strong>subscribe for the updates</strong> on the most interesting " "questions and will be used to create and retrieve your unique avatar image - " -"<a href='/faq#gravatar'><strong>gravatar</strong></a>." +"<a href='%(gravatar_faq_url)s'><strong>gravatar</strong></a>.</p>" -#: 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 "" +"<p><span class='strong big'>Oops... looks like screen name %(username)s is " +"already used in another account.</span></p><p>Please choose another screen " +"name to use with your %(provider)s login. Also, a valid email address is " +"required on the <span class='orange'>Q&A</span> forum. Your email is " +"used to create a unique <a href='%(gravatar_faq_url)s'><strong>gravatar</" +"strong></a> image for your account. If you like, you can <strong>receive " +"updates</strong> on the interesting questions or entire forum by email. " +"Email addresses are never shown or otherwise shared with anybody else.</p>" + +#: templates/authopenid/complete.html:35 +#, python-format +msgid "" +"register new external %(provider)s account info, see %(gravatar_faq_url)s" +msgstr "" +"<p><span class=\"big strong\">You are here for the first time with your %" +"(provider)s login.</span></p><p>You can either keep your <strong>screen " +"name</strong> the same as your %(provider)s login name or choose some other " +"nickname.</p><p>Also, please save a valid <strong>email</strong> address. " +"With the email you can <strong>subscribe for the updates</strong> on the " +"most interesting questions. Email address is also used to create and " +"retrieve your unique avatar image - <a href='%(gravatar_faq_url)" +"s'><strong>gravatar</strong></a>.</p>" + +#: templates/authopenid/complete.html:40 msgid "This account already exists, please use another." msgstr "" +"<p><span class=\"big strong\">You are here for the first time with your %" +"(provider)s login.</span> Please create your <strong>screen name</strong> " +"and save your <strong>email</strong> address. Saved email address will let " +"you <strong>subscribe for the updates</strong> on the most interesting " +"questions and will be used to create and retrieve your unique avatar image - " +"<a href='%(gravatar_faq_url)s'><strong>gravatar</strong></a>.</p>" + -#: 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 "<strong>Screen Name</strong> (<i>will be shown to others</i>)" -#: templates/authopenid/complete.html:48 +#: templates/authopenid/complete.html:83 msgid "Email address label" msgstr "" "<strong>Email Address</strong> (<i>will <strong>not</strong> be shared with " "anyone, must be valid</i>)" -#: templates/authopenid/complete.html:49 +#: templates/authopenid/complete.html:89 templates/authopenid/signup.html:15 +msgid "receive updates motivational blurb" +msgstr "" +"<strong>Receive forum updates by email</strong> - this will help our " +"community grow and become more useful.<br/>By default <span " +"class='orange'>Q&A</span> forum sends up to <strong>one email digest per " +"week</strong> - only when there is anything new.<br/>If you like, please " +"adjust this now or any time later from your user account." + +#: templates/authopenid/complete.html:91 +msgid "Tag filter tool will be your right panel, once you log in." +msgstr "" +"<strong>Receive forum updates by email</strong> - this will help our " +"community grow and become more useful.<br/>By default " +"<span class='orange'>Q&A</span> forum sends up to <strong>one " +"email digest per week</strong> - only when there is anything new.<br/>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:92 msgid "create account" msgstr "Signup" -#: templates/authopenid/complete.html:56 +#: templates/authopenid/complete.html:101 msgid "Existing account" msgstr "" -#: templates/authopenid/complete.html:57 +#: templates/authopenid/complete.html:102 msgid "user name" msgstr "" -#: templates/authopenid/complete.html:58 templates/authopenid/signin.html:128 +#: templates/authopenid/complete.html:103 msgid "password" msgstr "" -#: templates/authopenid/complete.html:61 +#: templates/authopenid/complete.html:108 msgid "Register" msgstr "" -#: templates/authopenid/complete.html:62 templates/authopenid/signin.html:156 +#: templates/authopenid/complete.html:109 templates/authopenid/signin.html:140 msgid "Forgot your password?" msgstr "" @@ -2415,12 +3263,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 +3277,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 +3338,47 @@ msgid "" "for any inconvenience" msgstr "" -#: templates/authopenid/sendpw.html:4 templates/authopenid/sendpw.html.py:8 -msgid "Send new password" +#: templates/authopenid/external_legacy_login_info.html:4 +#: templates/authopenid/external_legacy_login_info.html:7 +msgid "Traditional login information" msgstr "" +"<span class='big strong'>Forgot you password? No problems - just get a new " +"one!</span><br/>Please follow the following steps:<br/>• submit your " +"user name below and check your email<br/>• <strong>follow the " +"activation link</strong> for the new password - sent to you by email and " +"login with the suggested password<br/>• at this you might want to " +"change your password to something you can remember better" -#: templates/authopenid/sendpw.html:12 -msgid "Lost your password? No problem - here you can reset it." -msgstr "" +#: templates/authopenid/sendpw.html:21 +msgid "Reset password" +msgstr "Send me a new password" -#: templates/authopenid/sendpw.html:13 -msgid "" -"Please enter your username below and new password will be sent to your " -"registered e-mail" +#: templates/authopenid/external_legacy_login_info.html:12 +msgid "how to login with password through external login website" msgstr "" -#: templates/authopenid/sendpw.html:28 -msgid "User name" +#: templates/authopenid/sendpw.html:4 templates/authopenid/sendpw.html.py:7 +msgid "Send new password" +msgstr "Recover password" + +#: templates/authopenid/sendpw.html:10 +msgid "password recovery information" msgstr "" +"<span class='big strong'>Forgot you password? No problems - just get a new " +"one!</span><br/>Please follow the following steps:<br/>• submit your " +"user name below and check your email<br/>• <strong>follow the " +"activation link</strong> for the new password - sent to you by email and " +"login with the suggested password<br/>• 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 +3387,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 +3417,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 +3449,120 @@ msgstr "" "strong> %(summary)s...\"</i> <span class=\"strong big\">is saved and will be " "posted once you log in.</span>" -#: templates/authopenid/signin.html:40 +#: templates/authopenid/signin.html:42 msgid "Click to sign in through any of these services." msgstr "" -"<p><span class=\"big strong\">Please select your favorite login method below.</span>" -"</p><p><font color=\"gray\">External login services use <a " -"href=\"http://openid.net\"><b>OpenID</b></a> technology that increases " -"security of your online identity and makes login process simpler. First option " -"requires login name and password.</font></p>" +"<p><span class=\"big strong\">Please select your favorite login method below." +"</span></p><p><font color=\"gray\">External login services use <a href=" +"\"http://openid.net\"><b>OpenID</b></a> 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.</font></p>" -#: templates/authopenid/signin.html:113 +#: templates/authopenid/signin.html:117 msgid "Enter your <span id=\"enter_your_what\">Provider user name</span>" msgstr "" -"<span class=\"big strong\">Enter your <span id=\"enter_your_what\">Provider " -"user name</span></span><br><span class='grey'>(or select another login method above)</span>" +"<span class=\"big strong\">Enter your </span><span id=\"enter_your_what\" " +"class='big strong'>Provider user name</span><br/><span class='grey'>(or " +"select another login method above)</span>" -#: templates/authopenid/signin.html:120 +#: templates/authopenid/signin.html:124 msgid "" "Enter your <a class=\"openid_logo\" href=\"http://openid.net\">OpenID</a> " "web address" msgstr "" "<span class=\"big strong\">Enter your <a class=\"openid_logo\" href=\"http://" -"openid.net\">OpenID</a> web address</span><br><span class='grey'>(or choose another login " -"method above)</span>" +"openid.net\">OpenID</a> web address</span><br/><span class='grey'>(or choose " +"another login method above)</span>" -#: 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 "" -"<span class='big strong'>Enter your forum login and password</span><br/>" -"<span class='grey'>(or select your OpenID provider above)</span>" - -#: templates/authopenid/signin.html:126 -msgid "login name" -msgstr "" +"<span class='big strong'>Enter your CNPROG login and password</span><br/" +"><span class='grey'>(or select your OpenID provider above)</span>" -#: 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." - -#: 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 "" +"<span class='strong big'>If you prefer, create your forum login name and " +"password here. However</span>, please keep in mind that we also support " +"<strong>OpenID</strong> login method. With <strong>OpenID</strong> 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" -msgstr "" - -#: templates/authopenid/signup.html:46 -msgid "Register with your OpenID" +msgid "Create Account" msgstr "" -#: templates/authopenid/signup.html:49 -msgid "Login with your OpenID" +#: templates/authopenid/signup.html:19 +msgid "return to OpenID login" msgstr "" -# -#~ msgid "editing tips" -#~ msgstr "Tips" - -#~ msgid "Newest questions shown first." -#~ msgstr "" -#~ "Questions are sorted by <strong>entry date</strong>.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." +#~ "\n" +#~ "\t\t\t\thave total %(q_num)s questions\n" +#~ "\t\t\t\t" +#~ msgid_plural "" +#~ "\n" +#~ "\t\t\t\thave total %(q_num)s questions\n" +#~ "\t\t\t\t" +#~ msgstr[0] "" +#~ "\n" +#~ "<div class=\"questions-count\">%(q_num)s</div><p>question</p>" +#~ msgstr[1] "" +#~ "\n" +#~ "<div class=\"questions-count\">%(q_num)s</div><p>questions</p>" diff --git a/locale/es/LC_MESSAGES/django.mo b/locale/es/LC_MESSAGES/django.mo Binary files differindex d25949ed..2b514069 100644 --- a/locale/es/LC_MESSAGES/django.mo +++ b/locale/es/LC_MESSAGES/django.mo diff --git a/locale/es/LC_MESSAGES/django.po b/locale/es/LC_MESSAGES/django.po index fe73330c..83ff69bf 100644 --- a/locale/es/LC_MESSAGES/django.po +++ b/locale/es/LC_MESSAGES/django.po @@ -1,1216 +1,1453 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +#, fuzzy msgid "" msgstr "" -"Project-Id-Version: \n" +"Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2009-08-21 17:28+0000\n" -"PO-Revision-Date: 2009-08-21 11:29-0600\n" -"Last-Translator: Bruno Sarlo <bsarlo@gmail.com>\n" +"POT-Creation-Date: 2010-02-09 20:10+0000\n" +"PO-Revision-Date: 2010-02-09 14:11-0600\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: settings.py:12 urls.py:25 forum/views.py:310 forum/views.py:705 -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 -msgid "signin/" -msgstr "ingresar/" - -#: urls.py:22 -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:24 -#: django_authopenid/urls.py:25 -msgid "email/" -msgstr "email/" - -#: urls.py:28 -msgid "change/" -msgstr "cambiar/" - -#: urls.py:29 -msgid "sendkey/" -msgstr "enviarclave/" - -#: urls.py:30 -msgid "verify/" -msgstr "verificar/" - -#: urls.py:31 -msgid "about/" -msgstr "acercadenosotros/" - -#: urls.py:32 -msgid "faq/" -msgstr "preguntasfrecuentes/" - -#: urls.py:33 -msgid "privacy/" -msgstr "códigodeprivacidad/" - -#: urls.py:34 -msgid "logout/" -msgstr "cerrarsesion/" - -#: urls.py:35 urls.py:36 urls.py:37 urls.py:49 forum/models.py:425 -msgid "answers/" -msgstr "respuestas/" - -#: urls.py:35 urls.py:47 -msgid "comments/" -msgstr "comentarios/" - -#: urls.py:36 urls.py:41 urls.py:55 templates/user_info.html:34 -msgid "edit/" -msgstr "editar/" - -#: urls.py:37 urls.py:46 -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:1228 forum/views.py:1230 -#: forum/views.py:1470 -msgid "questions/" -msgstr "preguntas/" - -#: urls.py:39 urls.py:65 -msgid "ask/" -msgstr "preguntar/" +#: django_authopenid/forms.py:70 +msgid "choose a username" +msgstr "" -#: urls.py:40 -msgid "unanswered/" -msgstr "sinrespuesta/" +#: django_authopenid/forms.py:76 +msgid "user name is required" +msgstr "" -#: urls.py:42 -msgid "close/" -msgstr "cerrar/" +#: django_authopenid/forms.py:77 +msgid "sorry, this name is taken, please choose another" +msgstr "" -#: urls.py:43 -msgid "reopen/" -msgstr "reabrir/" +#: django_authopenid/forms.py:78 +msgid "sorry, this name is not allowed, please choose another" +msgstr "" -#: urls.py:44 -msgid "answer/" -msgstr "respuesta/" +#: django_authopenid/forms.py:79 +msgid "sorry, there is no user with this name" +msgstr "" -#: urls.py:45 -msgid "vote/" -msgstr "votar/" +#: django_authopenid/forms.py:80 +msgid "sorry, we have a serious error - user name is taken by several users" +msgstr "" -#: urls.py:48 urls.py:49 django_authopenid/urls.py:27 -msgid "delete/" -msgstr "borrar/" +#: django_authopenid/forms.py:81 +msgid "user name can only consist of letters, empty space and underscore" +msgstr "" -#: urls.py:51 -msgid "question/" -msgstr "pregunta/" +#: django_authopenid/forms.py:116 +msgid "your email address" +msgstr "" -#: urls.py:52 urls.py:53 forum/views.py:747 forum/views.py:2067 -msgid "tags/" -msgstr "etiquetas/" +#: django_authopenid/forms.py:117 +msgid "email address is required" +msgstr "" -#: urls.py:54 urls.py:55 urls.py:56 forum/views.py:1034 forum/views.py:1038 -#: forum/views.py:1472 forum/views.py:1805 forum/views.py:2069 -msgid "users/" -msgstr "usuarios/" +#: django_authopenid/forms.py:118 +msgid "please enter a valid email address" +msgstr "" -#: urls.py:57 urls.py:58 -msgid "badges/" -msgstr "distinciones/" +#: django_authopenid/forms.py:119 +msgid "this email is already used by someone else, please choose another" +msgstr "" -#: urls.py:59 -msgid "messages/" -msgstr "mensajes/" +#: django_authopenid/forms.py:163 django_authopenid/views.py:118 +msgid "i-names are not supported" +msgstr "" -#: urls.py:59 -msgid "markread/" -msgstr "marcarleido/" +#: django_authopenid/forms.py:219 +msgid "Account with this name already exists on the forum" +msgstr "" -#: urls.py:61 -msgid "nimda/" -msgstr "administrador/" +#: django_authopenid/forms.py:220 +msgid "can't have two logins to the same account yet, sorry." +msgstr "" -#: urls.py:63 -msgid "upload/" -msgstr "subir/" +#: django_authopenid/forms.py:242 +msgid "Please enter valid username and password (both are case-sensitive)." +msgstr "" -#: urls.py:64 urls.py:65 urls.py:66 -msgid "books/" -msgstr "libros/" +#: django_authopenid/forms.py:245 django_authopenid/forms.py:295 +msgid "This account is inactive." +msgstr "" -#: urls.py:67 -msgid "search/" -msgstr "buscar/" +#: django_authopenid/forms.py:247 +msgid "Login failed." +msgstr "" -#: django_authopenid/forms.py:67 django_authopenid/views.py:102 -msgid "i-names are not supported" -msgstr "i-names no son soportados" +#: django_authopenid/forms.py:249 +msgid "Please enter username and password" +msgstr "" -#: django_authopenid/forms.py:102 -msgid "" -"Usernames can only contain letters, numbers and " -"underscores" +#: django_authopenid/forms.py:251 +msgid "Please enter your password" msgstr "" -"Los nombres de usuario solo pueden contener letras, números y guión bajo" -#: django_authopenid/forms.py:109 -msgid "" -"This username does not exist in our database. Please " -"choose another." +#: django_authopenid/forms.py:253 +msgid "Please enter user name" msgstr "" -"Este nombre de usuario no existe en nuestra base de datos. Por favor elija " -"otro." -#: django_authopenid/forms.py:126 django_authopenid/forms.py:233 +#: django_authopenid/forms.py:291 msgid "" "Please enter a valid username and password. Note that " "both fields are case-sensitive." msgstr "" -"Por favor ingrese un usuario y contraseña validos. Ambos campos son " -"sensibles a mayúsculas y minúsculas." - -#: django_authopenid/forms.py:130 django_authopenid/forms.py:237 -msgid "This account is inactive." -msgstr "Esta cuenta esta inactiva." - -#: django_authopenid/forms.py:158 django_authopenid/forms.py:210 -msgid "invalid user name" -msgstr "nombre de usuario no valido" -#: django_authopenid/forms.py:160 -msgid "sorry, this name can not be used, please try another" +#: django_authopenid/forms.py:313 +msgid "choose password" msgstr "" -"perdón, pero este nombre de usuario no puede ser usado, intente con otro" - -#: django_authopenid/forms.py:162 -msgid "username too short" -msgstr "nombre de usuario muy corto" -#: django_authopenid/forms.py:170 django_authopenid/forms.py:171 -msgid "this name is already in use - please try anoter" -msgstr "este nombre ya está tomado - por favor intente con otro" +#: django_authopenid/forms.py:314 +msgid "password is required" +msgstr "" -#: django_authopenid/forms.py:185 -msgid "" -"This email is already registered in our database. " -"Please choose another." +#: django_authopenid/forms.py:317 +msgid "retype password" msgstr "" -"Este email ya está registrado en nuestra base de datos. Por favor, intente " -"con otro." -#: django_authopenid/forms.py:216 -msgid "" -"This username don't exist. Please choose another." -msgstr "Este nombre de usuario no existe, por favor ingrese otro." +#: django_authopenid/forms.py:318 +msgid "please, retype your password" +msgstr "" -#: django_authopenid/forms.py:255 -msgid "choose a username" -msgstr "elija un nombre de usuario" +#: django_authopenid/forms.py:319 +msgid "sorry, entered passwords did not match, please try again" +msgstr "" -#: django_authopenid/forms.py:257 templates/authopenid/signup.html:38 -msgid "your email address" -msgstr "su email (correo electrónico)" +#: django_authopenid/forms.py:344 +msgid "Current password" +msgstr "" -#: django_authopenid/forms.py:259 templates/authopenid/signup.html:39 -msgid "choose password" -msgstr "elija una contraseña" +#: django_authopenid/forms.py:346 +msgid "New password" +msgstr "" -#: django_authopenid/forms.py:261 templates/authopenid/signup.html:40 -msgid "retype password" -msgstr "re-ingrese la contraseña" +#: django_authopenid/forms.py:348 +msgid "Retype new password" +msgstr "" -#: django_authopenid/forms.py:335 +#: django_authopenid/forms.py:359 msgid "" "Old password is incorrect. Please enter the correct " "password." -msgstr "La antigua contraseña es incorrecta. Por favor ingrese la correcta" +msgstr "" -#: django_authopenid/forms.py:347 +#: django_authopenid/forms.py:371 msgid "new passwords do not match" -msgstr "la nueva contraseña no coincide" +msgstr "" -#: django_authopenid/forms.py:442 +#: django_authopenid/forms.py:435 +msgid "Your user name (<i>required</i>)" +msgstr "" + +#: django_authopenid/forms.py:450 msgid "Incorrect username." -msgstr "Nombre de usuario incorrecto" +msgstr "" -#: django_authopenid/urls.py:10 forum/views.py:310 forum/views.py:706 +#: django_authopenid/urls.py:9 django_authopenid/urls.py:10 +#: django_authopenid/urls.py:11 django_authopenid/urls.py:13 forum/urls.py:29 +msgid "signin/" +msgstr "" + +#: django_authopenid/urls.py:10 msgid "newquestion/" -msgstr "nuevapregunta/" +msgstr "" #: django_authopenid/urls.py:11 msgid "newanswer/" -msgstr "respuesta-nueva/" +msgstr "" #: django_authopenid/urls.py:12 msgid "signout/" -msgstr "salir/" +msgstr "" #: django_authopenid/urls.py:13 msgid "complete/" -msgstr "completado/" +msgstr "" + +#: django_authopenid/urls.py:15 +msgid "external-login/" +msgstr "" + +#: django_authopenid/urls.py:16 +msgid "register/" +msgstr "" -#: django_authopenid/urls.py:18 +#: django_authopenid/urls.py:17 +msgid "signup/" +msgstr "" + +#: django_authopenid/urls.py:19 msgid "sendpw/" -msgstr "enviarcontrasena/" +msgstr "" -#: django_authopenid/urls.py:19 django_authopenid/urls.py:23 -#, fuzzy +#: django_authopenid/urls.py:20 django_authopenid/urls.py:24 msgid "password/" -msgstr "contraseña" +msgstr "" -#: django_authopenid/urls.py:19 +#: django_authopenid/urls.py:20 msgid "confirm/" msgstr "" -#: django_authopenid/urls.py:22 -#, fuzzy +#: django_authopenid/urls.py:23 msgid "account_settings" -msgstr "preferencias" +msgstr "" + +#: 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:25 msgid "validate/" msgstr "" -#: django_authopenid/views.py:108 +#: django_authopenid/urls.py:26 +msgid "change/" +msgstr "" + +#: django_authopenid/urls.py:27 +msgid "sendkey/" +msgstr "" + +#: django_authopenid/urls.py:28 +msgid "verify/" +msgstr "" + +#: django_authopenid/urls.py:29 +msgid "openid/" +msgstr "" + +#: django_authopenid/urls.py:30 forum/urls.py:49 forum/urls.py:53 +msgid "delete/" +msgstr "" + +#: django_authopenid/views.py:124 #, python-format msgid "OpenID %(openid_url)s is invalid" -msgstr "El OpenID %(openid_url)s no es valido" +msgstr "" -#: django_authopenid/views.py:419 django_authopenid/views.py:546 -msgid "Welcome" -msgstr "Bienvenido" +#: django_authopenid/views.py:532 +msgid "Welcome email subject line" +msgstr "" -#: django_authopenid/views.py:509 +#: django_authopenid/views.py:627 msgid "Password changed." -msgstr "Contraseña modificada" +msgstr "" -#: django_authopenid/views.py:521 django_authopenid/views.py:526 -msgid "your email needs to be validated" -msgstr "su correo electrónico necesita ser validado" +#: django_authopenid/views.py:639 django_authopenid/views.py:645 +#, python-format +msgid "your email needs to be validated see %(details_url)s" +msgstr "" + +#: django_authopenid/views.py:666 +msgid "Email verification subject line" +msgstr "" -#: django_authopenid/views.py:683 django_authopenid/views.py:835 +#: django_authopenid/views.py:752 +msgid "your email was not changed" +msgstr "" + +#: django_authopenid/views.py:799 django_authopenid/views.py:951 #, python-format msgid "No OpenID %s found associated in our database" -msgstr "El OpenID %s no esta asociada en nuestra base de datos" +msgstr "" -#: django_authopenid/views.py:687 django_authopenid/views.py:842 +#: django_authopenid/views.py:803 django_authopenid/views.py:958 #, python-format msgid "The OpenID %s isn't associated to current user logged in" -msgstr "El OpenID %s no esta asociada al usuario actualmente autenticado" +msgstr "" -#: django_authopenid/views.py:695 +#: django_authopenid/views.py:811 msgid "Email Changed." -msgstr "Email modificado" +msgstr "" -#: django_authopenid/views.py:770 +#: django_authopenid/views.py:886 msgid "This OpenID is already associated with another account." -msgstr "Este OpenID ya está asociada a otra cuenta." +msgstr "" -#: django_authopenid/views.py:775 +#: django_authopenid/views.py:891 #, python-format msgid "OpenID %s is now associated with your account." -msgstr "El OpenID %s está ahora asociada con tu cuenta." +msgstr "" -#: django_authopenid/views.py:845 +#: django_authopenid/views.py:961 msgid "Account deleted." -msgstr "Cuenta borrada." +msgstr "" -#: django_authopenid/views.py:885 +#: django_authopenid/views.py:1004 msgid "Request for new password" -msgstr "Pedir nueva contraseña" +msgstr "" -#: django_authopenid/views.py:898 -msgid "A new password has been sent to your email address." -msgstr "Una nueva contraseña ha sido enviada a tu cuenta de Email." +#: django_authopenid/views.py:1017 +msgid "A new password and the activation link were sent to your email address." +msgstr "" -#: django_authopenid/views.py:928 +#: django_authopenid/views.py:1047 #, python-format msgid "" "Could not change password. Confirmation key '%s' is not " "registered." msgstr "" -"No se ha podido modificar la contraseña. La clave de confirmación '%s' no " -"está registrada" -#: django_authopenid/views.py:937 +#: django_authopenid/views.py:1056 msgid "" "Can not change password. User don't exist anymore in our " "database." msgstr "" -"No se puede cambiar la contraseña. El usuario no existe más en nuestra base " -"de datos." -#: django_authopenid/views.py:946 +#: django_authopenid/views.py:1065 #, python-format msgid "Password changed for %s. You may now sign in." -msgstr "Contraseña cambiada por %s. Ahora puedes ingresar." +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 "pregunta duplicada" +msgstr "" #: forum/const.py:9 -msgid "question if off-topic or not relevant" -msgstr "pregunta esta fuera de tema o no es relevante" +msgid "question is off-topic or not relevant" +msgstr "" #: forum/const.py:10 msgid "too subjective and argumentative" -msgstr "demasiado subjetiva o argumentativa" +msgstr "" #: forum/const.py:11 msgid "is not an answer to the question" -msgstr "no es una respuesta a la pregunta" +msgstr "" #: forum/const.py:12 msgid "the question is answered, right answer was accepted" -msgstr "la pregunta esta respondida, se ha aceptado la respuesta correcta" +msgstr "" #: forum/const.py:13 msgid "problem is not reproducible or outdated" -msgstr "el problema no es reproducible o caducó" +msgstr "" #: forum/const.py:15 msgid "question contains offensive inappropriate, or malicious remarks" -msgstr "la pregunta contiene frases ofensivas, inapropiadas o maliciosas." +msgstr "" #: forum/const.py:16 msgid "spam or advertising" -msgstr "spam o publicidad" +msgstr "" -#: forum/const.py:56 +#: forum/const.py:57 msgid "question" -msgstr "pregunta" +msgstr "" -#: forum/const.py:57 templates/book.html:110 +#: forum/const.py:58 templates/book.html:110 msgid "answer" -msgstr "respuesta" +msgstr "" -#: forum/const.py:58 +#: forum/const.py:59 msgid "commented question" -msgstr "pregunta comentada" +msgstr "" -#: forum/const.py:59 +#: forum/const.py:60 msgid "commented answer" -msgstr "respuesta comentada" +msgstr "" -#: forum/const.py:60 +#: forum/const.py:61 msgid "edited question" -msgstr "pregunta editada" +msgstr "" -#: forum/const.py:61 +#: forum/const.py:62 msgid "edited answer" -msgstr "respuesta editada" +msgstr "" -#: forum/const.py:62 +#: forum/const.py:63 msgid "received award" -msgstr "premio recibido" +msgstr "" -#: forum/const.py:63 +#: forum/const.py:64 msgid "marked best answer" -msgstr "marcada como mejor respuesta" +msgstr "" -#: forum/const.py:64 +#: forum/const.py:65 msgid "upvoted" -msgstr "votada positivo" +msgstr "" -#: forum/const.py:65 +#: forum/const.py:66 msgid "downvoted" -msgstr "votada negativo" +msgstr "" -#: forum/const.py:66 +#: forum/const.py:67 msgid "canceled vote" -msgstr "voto cancelado" +msgstr "" -#: forum/const.py:67 +#: forum/const.py:68 msgid "deleted question" -msgstr "pregunta borrada" +msgstr "" -#: forum/const.py:68 +#: forum/const.py:69 msgid "deleted answer" -msgstr "respuesta borrada" +msgstr "" -#: forum/const.py:69 +#: forum/const.py:70 msgid "marked offensive" -msgstr "marcada como ofensiva" +msgstr "" -#: forum/const.py:70 +#: forum/const.py:71 msgid "updated tags" -msgstr "etiquetas actualizadas" +msgstr "" -#: forum/const.py:71 +#: forum/const.py:72 msgid "selected favorite" -msgstr "seleccionada como favorita" +msgstr "" -#: forum/const.py:72 +#: forum/const.py:73 msgid "completed user profile" -msgstr "completó perfil de usuario" +msgstr "" + +#: forum/const.py:74 +msgid "email update sent to user" +msgstr "" -#: forum/const.py:83 +#: forum/const.py:85 msgid "[closed]" -msgstr "[cerrada]" +msgstr "" -#: forum/const.py:84 +#: forum/const.py:86 msgid "[deleted]" -msgstr "[borrada]" +msgstr "" -#: forum/const.py:85 +#: forum/const.py:87 forum/views.py:777 forum/views.py:796 msgid "initial version" -msgstr "versión inicial" +msgstr "" -#: forum/const.py:86 +#: forum/const.py:88 msgid "retagged" -msgstr "re-etiquetada" +msgstr "" + +#: forum/const.py:92 +msgid "exclude ignored tags" +msgstr "" + +#: forum/const.py:92 +msgid "allow only selected tags" +msgstr "" #: forum/feed.py:18 msgid " - " -msgstr " - " +msgstr "" #: forum/feed.py:18 msgid "latest questions" -msgstr "últimas preguntas" +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:57 +msgid "question/" +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 "título" +msgstr "" -#: forum/forms.py:15 +#: forum/forms.py:17 msgid "please enter a descriptive title for your question" -msgstr "ingrese un título descriptivo para su pregunta" +msgstr "" -#: forum/forms.py:20 +#: forum/forms.py:22 msgid "title must be > 10 characters" -msgstr "el título debe tener al menos 10 caracteres" +msgstr "" -#: forum/forms.py:29 +#: forum/forms.py:31 msgid "content" -msgstr "contenido" +msgstr "" -#: forum/forms.py:35 +#: forum/forms.py:37 msgid "question content must be > 10 characters" -msgstr "el contenido de la pregunta debe ser al menos de 10 caracteres" +msgstr "" -#: forum/forms.py:45 templates/header.html:30 templates/header.html.py:64 +#: forum/forms.py:47 templates/header.html:28 templates/header.html.py:62 msgid "tags" -msgstr "etiquetas" +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 "" -"por favor utilice espacio para separar las etiquetas (esto habilitael auto-" -"completado)" -#: forum/forms.py:54 templates/question_retag.html:38 +#: forum/forms.py:56 templates/question_retag.html:39 msgid "tags are required" -msgstr "las etiquetas son requeridas" +msgstr "" -#: forum/forms.py:58 +#: forum/forms.py:62 msgid "please use 5 tags or less" -msgstr "por favor use 5 o menos etiquetas" +msgstr "" -#: forum/forms.py:61 +#: forum/forms.py:65 msgid "tags must be shorter than 20 characters" -msgstr "las etiquetas deben ser menores a 20 caracteres" +msgstr "" -#: forum/forms.py:65 +#: forum/forms.py:69 msgid "" "please use following characters in tags: letters 'a-z', numbers, and " "characters '.-_#'" 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 -#: templates/questions.html.py:70 templates/unanswered.html:48 -#: templates/unanswered.html.py:60 +#: forum/forms.py:79 templates/index.html:62 templates/index.html.py:74 +#: templates/post_contributor_info.html:7 +#: templates/question_summary_list_roll.html:26 +#: templates/question_summary_list_roll.html:38 templates/questions.html:96 +#: templates/questions.html.py:108 templates/unanswered.html:51 +#: templates/unanswered.html.py:63 msgid "community wiki" -msgstr "wiki de comunidad" +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 "" -"si marca la opción 'wiki de comunidad', la pregunta y respuestas no generan " -"puntos y el nombre del autor no será mostrado" -#: forum/forms.py:89 +#: forum/forms.py:96 msgid "update summary:" -msgstr "resumen de modificación" +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 "" -"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:100 +msgid "Automatically accept user's contributions for the email updates" +msgstr "" -#: forum/forms.py:99 forum/forms.py:160 -msgid "Category" -msgstr "Categoría" +#: forum/forms.py:113 +msgid "Your name:" +msgstr "" -#: forum/forms.py:180 +#: forum/forms.py:114 +msgid "Email (not shared with anyone):" +msgstr "" + +#: forum/forms.py:115 +msgid "Your message:" +msgstr "" + +#: forum/forms.py:198 msgid "this email does not have to be linked to gravatar" -msgstr "este email no tiene porque estar asociado a un Gravatar" +msgstr "" -#: forum/forms.py:181 +#: forum/forms.py:199 +msgid "Screen name" +msgstr "" + +#: forum/forms.py:200 msgid "Real name" -msgstr "Nombre real" +msgstr "" -#: forum/forms.py:182 +#: forum/forms.py:201 msgid "Website" -msgstr "Sitio Web" +msgstr "" -#: forum/forms.py:183 +#: forum/forms.py:202 msgid "Location" -msgstr "Ubicación" +msgstr "" -#: forum/forms.py:184 +#: forum/forms.py:203 msgid "Date of birth" -msgstr "Fecha de nacimiento" +msgstr "" -#: forum/forms.py:184 +#: forum/forms.py:203 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" +msgstr "" -#: forum/forms.py:185 templates/authopenid/settings.html:21 +#: forum/forms.py:204 templates/authopenid/settings.html:21 msgid "Profile" -msgstr "Perfil" +msgstr "" -#: forum/forms.py:212 forum/forms.py:213 +#: forum/forms.py:232 forum/forms.py:233 msgid "this email has already been registered, please use another one" -msgstr "este email ya ha sido registrado, por favor use otro" +msgstr "" + +#: forum/forms.py:239 +msgid "Choose email tag filter" +msgstr "" + +#: forum/forms.py:254 forum/forms.py:255 +msgid "weekly" +msgstr "" + +#: forum/forms.py:254 forum/forms.py:255 +msgid "no email" +msgstr "" + +#: forum/forms.py:255 +msgid "daily" +msgstr "" + +#: forum/forms.py:270 +msgid "Asked by me" +msgstr "" + +#: forum/forms.py:273 +msgid "Answered by me" +msgstr "" + +#: forum/forms.py:276 +msgid "Individually selected" +msgstr "" + +#: forum/forms.py:279 +msgid "Entire forum (tag filtered)" +msgstr "" + +#: forum/models.py:52 +msgid "Entire forum" +msgstr "" + +#: forum/models.py:53 +msgid "Questions that I asked" +msgstr "" + +#: forum/models.py:54 +msgid "Questions that I answered" +msgstr "" -#: forum/models.py:253 +#: forum/models.py:55 +msgid "Individually selected questions" +msgstr "" + +#: forum/models.py:58 +msgid "Weekly" +msgstr "" + +#: forum/models.py:59 +msgid "Daily" +msgstr "" + +#: forum/models.py:60 +msgid "No email" +msgstr "" + +#: forum/models.py:321 #, python-format msgid "%(author)s modified the question" -msgstr "%(author)s modificó la pregunta" +msgstr "" -#: forum/models.py:257 +#: forum/models.py:325 #, python-format msgid "%(people)s posted %(new_answer_count)s new answers" -msgstr "%(people)s publicaron %(new_answer_count)s nuevas respuestas" +msgstr "" -#: forum/models.py:262 +#: forum/models.py:330 #, python-format msgid "%(people)s commented the question" -msgstr "%(people)s comentarion la pregunta" +msgstr "" -#: forum/models.py:267 +#: forum/models.py:335 #, python-format msgid "%(people)s commented answers" -msgstr "%(people)s comentaron la respuesta" +msgstr "" -#: forum/models.py:269 +#: forum/models.py:337 #, python-format msgid "%(people)s commented an answer" -msgstr "%(people)s comentaron la respuesta" +msgstr "" + +#: forum/models.py:368 +msgid "interesting" +msgstr "" -#: forum/models.py:313 forum/models.py:425 -msgid "revisions" -msgstr "revisiones/" +#: forum/models.py:368 +msgid "ignored" +msgstr "" -#: forum/models.py:448 templates/badges.html:51 +#: forum/models.py:538 templates/badges.html:53 msgid "gold" -msgstr "oro" +msgstr "" -#: forum/models.py:449 templates/badges.html:59 +#: forum/models.py:539 templates/badges.html:61 msgid "silver" -msgstr "plata" +msgstr "" -#: forum/models.py:450 templates/badges.html:66 +#: forum/models.py:540 templates/badges.html:68 msgid "bronze" -msgstr "bronce" +msgstr "" + +#: forum/urls.py:26 +msgid "upfiles/" +msgstr "" + +#: forum/urls.py:30 +msgid "about/" +msgstr "" + +#: forum/urls.py:31 +msgid "faq/" +msgstr "" + +#: forum/urls.py:32 +msgid "privacy/" +msgstr "" + +#: forum/urls.py:33 +msgid "logout/" +msgstr "" + +#: forum/urls.py:34 forum/urls.py:35 forum/urls.py:36 forum/urls.py:53 +msgid "answers/" +msgstr "" + +#: forum/urls.py:34 forum/urls.py:46 forum/urls.py:49 forum/urls.py:53 +msgid "comments/" +msgstr "" + +#: forum/urls.py:35 forum/urls.py:40 forum/urls.py:75 +#: templates/user_info.html:45 +msgid "edit/" +msgstr "" + +#: forum/urls.py:36 forum/urls.py:45 +msgid "revisions/" +msgstr "" + +#: forum/urls.py:37 forum/urls.py:38 forum/urls.py:39 forum/urls.py:40 +#: forum/urls.py:41 forum/urls.py:42 forum/urls.py:43 forum/urls.py:44 +#: forum/urls.py:45 forum/urls.py:46 forum/urls.py:49 +msgid "questions/" +msgstr "" + +#: forum/urls.py:38 forum/urls.py:85 +msgid "ask/" +msgstr "" + +#: forum/urls.py:39 +msgid "unanswered/" +msgstr "" + +#: forum/urls.py:41 +msgid "close/" +msgstr "" + +#: forum/urls.py:42 +msgid "reopen/" +msgstr "" + +#: forum/urls.py:43 +msgid "answer/" +msgstr "" + +#: forum/urls.py:44 +msgid "vote/" +msgstr "" + +#: forum/urls.py:47 +msgid "command/" +msgstr "" + +#: forum/urls.py:58 forum/urls.py:59 +msgid "tags/" +msgstr "" + +#: forum/urls.py:61 forum/urls.py:65 +msgid "mark-tag/" +msgstr "" + +#: forum/urls.py:61 +msgid "interesting/" +msgstr "" + +#: forum/urls.py:65 +msgid "ignored/" +msgstr "" + +#: forum/urls.py:69 +msgid "unmark-tag/" +msgstr "" + +#: forum/urls.py:73 forum/urls.py:75 forum/urls.py:76 +msgid "users/" +msgstr "" + +#: forum/urls.py:74 +msgid "moderate-user/" +msgstr "" + +#: forum/urls.py:77 forum/urls.py:78 +msgid "badges/" +msgstr "" + +#: forum/urls.py:79 +msgid "messages/" +msgstr "" + +#: forum/urls.py:79 +msgid "markread/" +msgstr "" + +#: forum/urls.py:81 +msgid "nimda/" +msgstr "" + +#: forum/urls.py:83 +msgid "upload/" +msgstr "" + +#: forum/urls.py:84 forum/urls.py:85 forum/urls.py:86 +msgid "books/" +msgstr "" + +#: forum/urls.py:87 +msgid "search/" +msgstr "" + +#: forum/urls.py:88 +msgid "feedback/" +msgstr "" + +#: forum/urls.py:89 +msgid "account/" +msgstr "" #: forum/user.py:16 templates/user_tabs.html:7 msgid "overview" -msgstr "vista general" +msgstr "" #: forum/user.py:17 msgid "user profile" -msgstr "perfil de usuario" +msgstr "" #: forum/user.py:18 msgid "user profile overview" -msgstr "vista general del perfil de usuario" +msgstr "" #: forum/user.py:24 templates/user_tabs.html:9 msgid "recent activity" -msgstr "actividades recientes" +msgstr "" #: forum/user.py:25 msgid "recent user activity" -msgstr "actividades recientes del usuario" +msgstr "" #: forum/user.py:26 msgid "profile - recent activity" -msgstr "perfil - actividades recientes" +msgstr "" #: forum/user.py:33 templates/user_tabs.html:13 msgid "responses" -msgstr "respuestas" +msgstr "" #: forum/user.py:34 templates/user_tabs.html:12 msgid "comments and answers to others questions" -msgstr "comentarios y respuestas a preguntas de otros" +msgstr "" #: forum/user.py:35 msgid "profile - responses" -msgstr "perfil - respuestas" +msgstr "" -#: forum/user.py:42 templates/user_info.html:23 templates/users.html:26 +#: forum/user.py:42 templates/users.html:26 msgid "reputation" -msgstr "reputación" +msgstr "" #: forum/user.py:43 msgid "user reputation in the community" -msgstr "reputación del usuario en la comunidad" +msgstr "" #: forum/user.py:44 msgid "profile - user reputation" -msgstr "perfil - reputación del usuario" +msgstr "" #: forum/user.py:50 msgid "favorite questions" -msgstr "preguntas favoritas" +msgstr "" #: forum/user.py:51 msgid "users favorite questions" -msgstr "preguntas favoritas de los usuarios" +msgstr "" #: forum/user.py:52 msgid "profile - favorite questions" -msgstr "perfil - preguntas favoritas" +msgstr "" #: forum/user.py:59 templates/user_tabs.html:20 msgid "casted votes" -msgstr "votos" +msgstr "" #: forum/user.py:60 templates/user_tabs.html:20 msgid "user vote record" -msgstr "historial de votación" +msgstr "" #: forum/user.py:61 msgid "profile - votes" -msgstr "perfil - votos" +msgstr "" -#: forum/user.py:68 -msgid "preferences" -msgstr "preferencias" +#: 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" -msgstr "preferencias del usuario" +msgid "email subscription settings" +msgstr "" #: forum/user.py:70 -msgid "profile - user preferences" -msgstr "perfil - preferencia de " +msgid "profile - email subscriptions" +msgstr "" -#: forum/views.py:988 +#: forum/views.py:141 +msgid "Q&A forum feedback" +msgstr "" + +#: forum/views.py:142 +msgid "Thanks for the feedback!" +msgstr "" + +#: forum/views.py:150 +msgid "We look forward to hearing your feedback! Please, give it next time :)" +msgstr "" + +#: forum/views.py:1080 #, python-format -msgid "subscription saved, %(email)s needs validation" -msgstr "subscripción guardada, %(email)s necesita validación" +msgid "subscription saved, %(email)s needs validation, see %(details_url)s" +msgstr "" -#: forum/views.py:1914 +#: forum/views.py:1088 +msgid "email update frequency has been set to daily" +msgstr "" + +#: forum/views.py:1965 forum/views.py:1969 +msgid "changes saved" +msgstr "" + +#: forum/views.py:1975 +msgid "email updates canceled" +msgstr "" + +#: forum/views.py:2142 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" +msgstr "" -#: forum/views.py:1916 +#: forum/views.py:2144 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:1918 +#: forum/views.py:2146 #, python-format msgid "maximum upload file size is %sK" -msgstr "tamaño máximo permitido es archivo %sK" +msgstr "" -#: forum/views.py:1920 +#: forum/views.py:2148 #, python-format msgid "" "Error uploading file. Please contact the site administrator. Thank you. %s" msgstr "" -"Error al subir el archivo. Por favor, contacte al administrador. Gracias. %s" -#: forum/management/commands/send_email_alerts.py:35 -msgid "updates from website" -msgstr "actualizaciones del sitio" +#: forum/management/commands/send_email_alerts.py:233 +msgid "email update message subject" +msgstr "" + +#: forum/management/commands/send_email_alerts.py:234 +#, 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] "" +msgstr[1] "" + +#: forum/management/commands/send_email_alerts.py:243 +#: forum/management/commands/send_email_alerts.py:258 +msgid "new question" +msgstr "" + +#: forum/management/commands/send_email_alerts.py:268 +#, python-format +msgid "There is also one question which was recently " +msgid_plural "" +"There are also %(num)d more questions which were recently updated " +msgstr[0] "" +msgstr[1] "" + +#: forum/management/commands/send_email_alerts.py:273 +msgid "" +"Perhaps you could look up previously sent forum reminders in your mailbox." +msgstr "" + +#: forum/management/commands/send_email_alerts.py:278 +#, python-format +msgid "" +"go to %(link)s to change frequency of email updates or %(email)s " +"administrator" +msgstr "" -#: forum/templatetags/extra_tags.py:143 forum/templatetags/extra_tags.py:172 -#: templates/header.html:35 +#: forum/templatetags/extra_tags.py:163 forum/templatetags/extra_tags.py:192 +#: templates/header.html:33 msgid "badges" -msgstr "distinciones" +msgstr "" -#: forum/templatetags/extra_tags.py:144 forum/templatetags/extra_tags.py:171 +#: forum/templatetags/extra_tags.py:164 forum/templatetags/extra_tags.py:191 msgid "reputation points" -msgstr "puntos de reputación" +msgstr "" + +#: forum/templatetags/extra_tags.py:247 +msgid "%b %d at %H:%M" +msgstr "" + +#: forum/templatetags/extra_tags.py:249 +msgid "%b %d '%y at %H:%M" +msgstr "" + +#: forum/templatetags/extra_tags.py:251 +msgid "2 days ago" +msgstr "" + +#: forum/templatetags/extra_tags.py:253 +msgid "yesterday" +msgstr "" + +#: forum/templatetags/extra_tags.py:255 +#, python-format +msgid "%(hr)d hour ago" +msgid_plural "%(hr)d hours ago" +msgstr[0] "" +msgstr[1] "" + +#: forum/templatetags/extra_tags.py:257 +#, python-format +msgid "%(min)d min ago" +msgid_plural "%(min)d mins ago" +msgstr[0] "" +msgstr[1] "" -#: forum/templatetags/extra_tags.py:225 -msgid " ago" -msgstr " atras" +#: middleware/anon_user.py:33 +#, python-format +msgid "first time greeting with %(url)s" +msgstr "" #: templates/404.html:24 msgid "Sorry, could not find the page you requested." -msgstr "Disculpe, no se pudo encontrar la página que solicito." +msgstr "" #: templates/404.html:26 msgid "This might have happened for the following reasons:" -msgstr "Esto puede haber sucedido por alguno de los siguientes motivos:" +msgstr "" #: templates/404.html:28 msgid "this question or answer has been deleted;" -msgstr "esta pregunta o respuesta ha sido borrada;" +msgstr "" #: templates/404.html:29 msgid "url has error - please check it;" -msgstr "la url tiene un error - por favor compruebelo;" +msgstr "" #: templates/404.html:30 msgid "" "the page you tried to visit is protected or you don't have sufficient " "points, see" msgstr "" -"La pagina que intentas acceder esta protegida o no tienes los puntos de " -"reputación suficientes, ver" #: templates/404.html:31 msgid "if you believe this error 404 should not have occured, please" -msgstr "si consideras que este error 404 no debería haber sucedido, por favor" +msgstr "" #: templates/404.html:32 msgid "report this problem" -msgstr "reporta este problema" +msgstr "" #: templates/404.html:41 templates/500.html:27 msgid "back to previous page" -msgstr "volver a la página siguiente" +msgstr "" #: templates/404.html:42 msgid "see all questions" -msgstr "ver todas las preguntas" +msgstr "" #: templates/404.html:43 msgid "see all tags" -msgstr "ver todas las tags" +msgstr "" #: templates/500.html:22 msgid "sorry, system error" -msgstr "lo sentimos, ha habido un error del sistema" +msgstr "" #: templates/500.html:24 msgid "system error log is recorded, error will be fixed as soon as possible" msgstr "" -"el error del sistema ha sido registrado, y será solucionado lo antes postible" #: templates/500.html:25 msgid "please report the error to the site administrators if you wish" -msgstr "por favor reportar el error al administrador de ser posible" +msgstr "" #: templates/500.html:28 msgid "see latest questions" -msgstr "ver ultimas preguntas" +msgstr "" #: templates/500.html:29 msgid "see tags" -msgstr "ver tags" +msgstr "" #: templates/about.html:6 templates/about.html.py:11 msgid "About" -msgstr "Acerca de" +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 "Editar respuesta" +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:25 +#: templates/question_edit.html.py:28 msgid "hide preview" -msgstr "ocultar previsualización" +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 "ver previsualización" +msgstr "" -#: templates/answer_edit.html:47 templates/question_edit.html:65 -#: templates/question_retag.html:52 templates/revisions_answer.html:36 -#: templates/revisions_question.html:36 +#: templates/answer_edit.html:48 templates/question_edit.html:66 +#: templates/question_retag.html:53 templates/revisions_answer.html:38 +#: templates/revisions_question.html:38 msgid "back" -msgstr "volver" +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:52 templates/revisions_question.html:52 msgid "revision" -msgstr "revisión" +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 "seleccionar revisión" +msgstr "" -#: templates/answer_edit.html:62 templates/ask.html:94 -#: templates/question.html:468 templates/question_edit.html:91 +#: templates/answer_edit.html:63 templates/ask.html:97 +#: templates/question.html:442 templates/question_edit.html:92 msgid "Toggle the real time Markdown editor preview" -msgstr "Activar la visualización en tiempo real de Markdown" +msgstr "" -#: templates/answer_edit.html:62 templates/ask.html:94 -#: templates/question.html:468 templates/question_edit.html:91 +#: templates/answer_edit.html:63 templates/ask.html:97 +#: templates/question.html:443 templates/question_edit.html:92 msgid "toggle preview" -msgstr "Activar previsualización" +msgstr "" -#: templates/answer_edit.html:71 templates/question_edit.html:121 -#: templates/question_retag.html:73 +#: templates/answer_edit.html:72 templates/question_edit.html:124 +#: templates/question_retag.html:74 msgid "Save edit" -msgstr "Guardar la edición" +msgstr "" -#: templates/answer_edit.html:72 templates/close.html:29 -#: templates/question_edit.html:122 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:125 +#: templates/question_retag.html:75 templates/reopen.html:30 +#: templates/user_edit.html:87 templates/authopenid/changeemail.html:40 msgid "Cancel" -msgstr "Cancelar" +msgstr "" #: templates/answer_edit_tips.html:4 msgid "answer tips" -msgstr "sugerencias sobre respuestas" +msgstr "" #: templates/answer_edit_tips.html:7 msgid "please make your answer relevant to this community" -msgstr "por favor, haz que tu respuesta sea relevante a esta comunidad" +msgstr "" #: templates/answer_edit_tips.html:10 msgid "try to give an answer, rather than engage into a discussion" -msgstr "intenta dar una respuesta, más que entablar un debate o discusión" +msgstr "" #: templates/answer_edit_tips.html:13 msgid "please try to provide details" -msgstr "por favor, intenta brindar detalles" +msgstr "" #: templates/answer_edit_tips.html:16 templates/question_edit_tips.html:13 msgid "be clear and concise" -msgstr "ser claro y conciso" +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 "ver preguntas frecuentes" +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 "sugerencias de Markdown" +msgstr "" -#: 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 "*itálica* o __itálica__" +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 "**negrita** o __negrita__" +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 "enlace" +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 "texto" +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 "imagen" +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 "lista numerada" +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 "etiquetas básicas de HTML permitidas" +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 "aprender mas sobre 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 "Hacer una pregunta" +msgstr "" -#: templates/ask.html:67 +#: templates/ask.html:68 msgid "login to post question info" msgstr "" -"<span class='strong big'>Puedes comenzar a realizar tu pregunta de forma " -"anonima</span> - actualmente no te encuentras Ingresado. Cuando envíes tu " -"pregunta, serás redireccionado a la página de Ingreso/registro. Tu pregunta " -"será guardada temporalemente y será enviada cuando ingreses. El proceso de " -"Ingreso/Registro es muy simple. Ingresar lleva unos 30 segundos, registrarse " -"por primera vez lleva un minuto." -#: 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 "" -"<span class='strong big'>Parece ser que tu dirección de email, %(email)s no " -"ha sido validada aún.</span> Para enviar mensajes debes verificar tu " -"dirección de email, puedes ver <a href='/faq#validate'>más detalles aquí</" -"a>. <br/> 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:112 templates/ask.html.py:119 +#: templates/question_edit.html:120 msgid "(required)" -msgstr "(requerido)" +msgstr "" -#: templates/ask.html:121 +#: templates/ask.html:126 msgid "Login/signup to post your question" -msgstr "Iniciar sesión/registrarse para publicar su pregunta" +msgstr "" -#: templates/ask.html:123 +#: templates/ask.html:128 msgid "Ask your question" -msgstr "Haz tu pregunta" +msgstr "" #: templates/badge.html:6 templates/badge.html.py:17 msgid "Badge" -msgstr "Distinción" +msgstr "" #: templates/badge.html:26 msgid "The users have been awarded with badges:" -msgstr "Usuarios han sido galardonados con distinciones:" +msgstr "" #: templates/badges.html:6 msgid "Badges summary" -msgstr "Resumen de distinciones" +msgstr "" -#: templates/badges.html:17 templates/user_stats.html:73 +#: templates/badges.html:17 msgid "Badges" -msgstr "Distinciones" +msgstr "" #: templates/badges.html:21 msgid "Community gives you awards for your questions, answers and votes." -msgstr "La comunidad te da distinciones por tus preguntas, respuestas y votos." +msgstr "" #: 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 "" -"Debajo esta la lista de las distinciones disponibles y la cantidad de veces " -"que han sido asignadas." -#: templates/badges.html:48 +#: templates/badges.html:50 msgid "Community badges" -msgstr "Distinciones de la comunidad" +msgstr "" -#: templates/badges.html:54 +#: templates/badges.html:56 msgid "gold badge description" msgstr "" -"Las distinciones de Oro son excepcionales. Para obtenerla debes demostrar un " -"profundo conocimiento y habilidad además de participar activamente en la " -"comunidad. La distinción de Oro es la condecoración máxima en esta comunidad" -#: templates/badges.html:62 +#: templates/badges.html:64 msgid "silver badge description" msgstr "" -"Obtener una distinción de Plata requiere de paciencia. Si has logrado una, " -"quiere decir que haz significativamente aportado a esta comunidad." -#: templates/badges.html:65 +#: templates/badges.html:67 msgid "bronze badge: often given as a special honor" msgstr "" -"distinción de bronce: con frecuencia entregada como reconocimiento especial." -#: templates/badges.html:69 +#: templates/badges.html:71 msgid "bronze badge description" msgstr "" -"Si eres un usuario activo de esta comunidad, recibirás esta distinción - de " -"todas maneras es un honor especial." #: templates/book.html:7 msgid "reading channel" -msgstr "canal de lectura" +msgstr "" #: templates/book.html:26 msgid "[author]" -msgstr "[autor]" +msgstr "" #: templates/book.html:30 msgid "[publisher]" -msgstr "[editorial]" +msgstr "" #: templates/book.html:34 msgid "[publication date]" -msgstr "[fecha de publicación]" +msgstr "" #: templates/book.html:38 msgid "[price]" -msgstr "[precio]" +msgstr "" #: templates/book.html:39 msgid "currency unit" -msgstr "unidad de moneda" +msgstr "" #: templates/book.html:42 msgid "[pages]" -msgstr "[páginas]" +msgstr "" #: templates/book.html:43 msgid "pages abbreviation" -msgstr "abreviación de páginas" +msgstr "" #: templates/book.html:46 msgid "[tags]" -msgstr "[etiquetas]" +msgstr "" #: templates/book.html:56 msgid "author blog" -msgstr "blog del autor" +msgstr "" #: templates/book.html:62 msgid "book directory" -msgstr "directorio del libro" +msgstr "" #: templates/book.html:66 msgid "buy online" -msgstr "comprar en-linea" +msgstr "" #: templates/book.html:79 msgid "reader questions" -msgstr "pregunta de lector" +msgstr "" #: templates/book.html:82 msgid "ask the author" -msgstr "preguntar al autor" +msgstr "" #: templates/book.html:88 templates/book.html.py:93 #: templates/users_questions.html:18 msgid "this question was selected as favorite" -msgstr "esta pregunta ha sido seleccionada como favorita" +msgstr "" #: templates/book.html:88 templates/book.html.py:93 #: templates/users_questions.html:11 templates/users_questions.html.py:18 msgid "number of times" -msgstr "numero de veces" +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:50 +#: templates/question_summary_list_roll.html:14 templates/questions.html:84 +#: templates/unanswered.html:39 templates/users_questions.html:32 msgid "votes" -msgstr "votos" +msgstr "" #: templates/book.html:108 msgid "the answer has been accepted to be correct" -msgstr "la respuesta ha sido aceptada como correcta" +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:51 +#: templates/question_summary_list_roll.html:15 templates/questions.html:85 +#: templates/unanswered.html:40 templates/users_questions.html:40 msgid "views" -msgstr "vistas" +msgstr "" -#: templates/book.html:125 templates/index.html:69 templates/question.html:500 -#: templates/questions.html:84 templates/questions.html.py:157 -#: 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:106 +#: templates/question.html:488 templates/question_summary_list_roll.html:52 +#: templates/questions.html:140 templates/questions.html.py:257 +#: templates/tags.html:49 templates/unanswered.html:95 +#: templates/unanswered.html.py:122 templates/users_questions.html:52 msgid "using tags" -msgstr "usando etiquetas" +msgstr "" #: templates/book.html:147 msgid "subscribe to book RSS feed" -msgstr "suscribirse al RSS del libro" +msgstr "" -#: templates/book.html:147 templates/index.html:118 +#: templates/book.html:147 templates/index.html:157 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" +msgstr "" #: templates/close.html:6 templates/close.html.py:16 msgid "Close question" -msgstr "Cerrar pregunta" +msgstr "" #: templates/close.html:19 msgid "Close the question" -msgstr "Cerrar la pregunta" +msgstr "" #: templates/close.html:25 msgid "Reasons" -msgstr "Razón" +msgstr "" #: templates/close.html:28 msgid "OK to close" -msgstr "OK para cerrar" +msgstr "" #: templates/faq.html:11 msgid "Frequently Asked Questions " -msgstr "Preguntas Frecuentes" +msgstr "" #: templates/faq.html:16 msgid "What kinds of questions can I ask here?" -msgstr "¿Qué clase de preguntas puedo hacer aquí?" +msgstr "" #: templates/faq.html:17 msgid "" "Most importanly - questions should be <strong>relevant</strong> to this " "community." msgstr "" -"Por encima de todo - las preguntas deben ser <strong>relevantes</strong>a " -"esta comunidad." #: templates/faq.html:18 msgid "" "Before asking the question - please make sure to use search to see whether " "your question has alredy been answered." msgstr "" -"Antes de hacer tu pregunta - por favor usa el buscador para asegurarte que " -"la pregunta no este ya hecha." #: templates/faq.html:21 msgid "What questions should I avoid asking?" -msgstr "¿Qué preguntas debería evitar preguntar?" +msgstr "" #: templates/faq.html:22 msgid "" "Please avoid asking questions that are not relevant to this community, too " "subjective and argumentative." msgstr "" -"Evita hacer preguntas que no son relevantes a la comunidad, demasiado " -"subjetivas o argumentativas." #: templates/faq.html:27 msgid "What should I avoid in my answers?" -msgstr "¿Que debo evitar en mis respuestas?" +msgstr "" #: templates/faq.html:28 msgid "" @@ -1218,42 +1455,32 @@ msgid "" "discussions in your answers, comment facility allows some space for brief " "discussions." msgstr "" -"es un sitio de Preguntas y Respuestas, no un grupo de discusión. Por ende, " -"intenta evitar discusiones en tus respuestas. Los comentarios permiten " -"realizar pequeñas discusiones." #: templates/faq.html:32 msgid "Who moderates this community?" -msgstr "¿Quién modera esta comunidad?" +msgstr "" #: templates/faq.html:33 msgid "The short answer is: <strong>you</strong>." -msgstr "La respuesta corta es: <strong>tú</strong>" +msgstr "" #: templates/faq.html:34 msgid "This website is moderated by the users." -msgstr "Este sitio es moderado por los usuarios." +msgstr "" #: templates/faq.html:35 msgid "" "The reputation system allows users earn the authorization to perform a " "variety of moderation tasks." msgstr "" -"El sistema de reputación permite a los usuarios adquirir autorización para " -"realizar diversas tareas de moderación." #: templates/faq.html:40 msgid "How does reputation system work?" -msgstr "¿Cómo funciona el sistema de reputación?" +msgstr "" #: templates/faq.html:41 msgid "Rep system summary" msgstr "" -"Cuando una pregunta o respuesta es votada positivamente, el usuario que la " -"realizo ganará algunos puntos, que llamamos \"puntos de reputación\". Estos " -"puntos sirven a groso modo para medir la confianza que la comunidad le " -"tiene. Diversas tareas de moderación son gradualmente asignadas a los " -"usuarios basado en estos puntos de reputación." #: templates/faq.html:42 msgid "" @@ -1265,659 +1492,804 @@ msgid "" "or answer. The table below explains reputation point requirements for each " "type of moderation task." msgstr "" -"Por ejemplo, si haces una pregunta interesante o das una respuesta útil, tu " -"adición será votada positivamente. Por otro lado, si la respuesta es fuera " -"de lugar - será votada negativamente. Cada voto a favor genera <strong>10</" -"strong> puntos, cada voto en contra restará <strong>2</strong> puntos. Hay " -"un limite de <strong>200</strong> puntos que puedes acumular por pregunta o " -"respuesta. La tabla debajo explica los puntos de reputación requeridos para " -"cada tarea de moderación." -#: templates/faq.html:53 templates/user_votes.html:14 +#: templates/faq.html:53 templates/user_votes.html:15 msgid "upvote" -msgstr "votar positivo" +msgstr "" #: templates/faq.html:57 msgid "use tags" -msgstr "etiquetas usadas" +msgstr "" #: templates/faq.html:62 msgid "add comments" -msgstr "agregar comentarios" +msgstr "" -#: templates/faq.html:66 templates/user_votes.html:16 +#: templates/faq.html:66 templates/user_votes.html:17 msgid "downvote" -msgstr "votar negativo" +msgstr "" #: templates/faq.html:69 msgid "open and close own questions" -msgstr "abrir y cerrar sus propias preguntas" +msgstr "" #: templates/faq.html:73 msgid "retag questions" -msgstr "re-etiquetar preguntas" +msgstr "" -#: templates/faq.html:77 +#: templates/faq.html:78 msgid "edit community wiki questions" -msgstr "editar preguntas de la wiki comunitaria" +msgstr "" -#: templates/faq.html:81 +#: templates/faq.html:83 msgid "edit any answer" -msgstr "editar cualquier pregunta" +msgstr "" -#: templates/faq.html:85 +#: templates/faq.html:87 msgid "open any closed question" -msgstr "abrir cualquier pregunta cerrada" +msgstr "" -#: templates/faq.html:89 +#: templates/faq.html:91 msgid "delete any comment" -msgstr "borrar cualquier comentario" +msgstr "" -#: templates/faq.html:93 +#: templates/faq.html:95 msgid "delete any questions and answers and perform other moderation tasks" msgstr "" -"borrar cualquier pregunta o respuesta y realizar otras tareas de " -"administración." -#: templates/faq.html:100 +#: templates/faq.html:102 msgid "how to validate email title" -msgstr "¿Cómo validar mi correo electrónico?" +msgstr "" -#: templates/faq.html:102 -msgid "how to validate email info" -msgstr "" -"<form style='margin:0;padding:0;' action='/email/sendkey/'><p><span class=" -"\"bigger strong\">¿Cómo?</span> Si acabas de asignar o cambiar tu correo " -"electrónico - <strong>verifica tu casilla de mensajes y clickea en el link " -"incluido</strong>. <br/> El link contiene una clave generada especificamente " -"para ti. <button style='display:inline' type='submit'><strong>get a new key</" -"strong></button> y vuelve a revisar tu casilla de mensajes.</p></form><span " -"class=\"bigger strong\">¿Porqué?</span> La validación del email es requerida " -"para estar seguros the que <strong>solo tu puedes enviar mensajes</strong> " -"bajo tu concentimiento y para <strong>minimizar el spam</strong>.<br/> Con " -"tu email podrás <strong>suscribirte a actualizaciones</strong> en las " -"preguntas mas interesantes. También, cuando te registras por primera vez - " -"se crea un imagen personal única de <a href='/" -"faq#gravatar'><strong>gravatar</strong></a>." - -#: templates/faq.html:106 +#: templates/faq.html:104 +#, python-format +msgid "" +"how to validate email info with %(send_email_key_url)s %(gravatar_faq_url)s" +msgstr "" + +#: templates/faq.html:108 msgid "what is gravatar" -msgstr "¿Qué es gravatar?" +msgstr "" -#: templates/faq.html:107 +#: templates/faq.html:109 msgid "gravatar faq info" msgstr "" -"<strong>Gravatar</strong> significa <strong>g</strong>lobalmente <strong>r</" -"strong>econocido <strong>avatar</strong> - tu imagen única asociada a tu " -"email. Es simplemente una imagen que se muestra junto con tus mensajes en " -"sitios que soportan gravatar. Por defecto gravatar aparece como un cuadrado " -"rellenado con figuras parecidas a copos de nieve. Puedes <strong>seleccionar " -"tu imagen</strong> en <a href='http://gravatar.com'><strong>gravatar.com</" -"strong></a>" -#: templates/faq.html:110 +#: templates/faq.html:112 msgid "To register, do I need to create new password?" -msgstr "¿Para registrarme, debo crearme una cuenta?" +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 "" -"No tienes porqué. Puedes ingresar usando cualquiera de los servicios que " -"soportan OpenID, ej. Google, Yahoo, AOL, MyOpenID, etc." -#: templates/faq.html:112 +#: templates/faq.html:114 msgid "Login now!" -msgstr "Ingresa ahora!" +msgstr "" -#: templates/faq.html:117 +#: templates/faq.html:119 msgid "Why other people can edit my questions/answers?" -msgstr "¿Porqué otras personas pueden editar mis preguntas y respuestas?" +msgstr "" -#: templates/faq.html:118 +#: templates/faq.html:120 msgid "Goal of this site is..." msgstr "" -"El objetivo de este sitio es generar contenido valioso mediante preguntas y " -"respuestas, de forma colaborativa. " -#: 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 "" -"Entonces, las preguntas y respuestas pueden ser editadas como wiki por " -"usuarios con experiencia, y esto mejora la calidad general del conocimiento " -"acumulado." -#: templates/faq.html:119 +#: templates/faq.html:121 msgid "If this approach is not for you, we respect your choice." msgstr "" -"Si esta forma de funcionamiento no es de tu agrado, respetamos tu elección." -#: templates/faq.html:123 +#: templates/faq.html:125 msgid "Still have questions?" -msgstr "¿Aún tienes preguntas?" +msgstr "" -#: templates/faq.html:124 -msgid "Please ask your question, help make our community better!" -msgstr "Por favor haz tu pregunta, ¡ayudanos a mejorar nuestra comunidad!" +#: templates/faq.html:126 +#, python-format +msgid "" +"Please ask your question at %(ask_question_url)s, help make our community " +"better!" +msgstr "" -#: templates/faq.html:126 templates/header.html:29 templates/header.html.py:63 +#: templates/faq.html:128 templates/header.html:27 templates/header.html.py:61 msgid "questions" -msgstr "preguntas" +msgstr "" -#: templates/faq.html:126 templates/index.html:123 +#: templates/faq.html:128 templates/index.html:162 msgid "." -msgstr "." +msgstr "" + +#: 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" +" <span class='big strong'>Dear %(user_name)s</span>, we look " +"forward to hearing your feedback. \n" +" Please type and send us your message below.\n" +" " +msgstr "" + +#: templates/feedback.html:24 +msgid "" +"\n" +" <span class='big strong'>Dear visitor</span>, we look forward to " +"hearing your feedback.\n" +" Please type and send us your message below.\n" +" " +msgstr "" -#: templates/footer.html:7 templates/header.html:14 templates/index.html:83 +#: templates/feedback.html:41 +msgid "(this field is required)" +msgstr "" + +#: templates/feedback.html:49 +msgid "Send Feedback" +msgstr "" + +#: templates/footer.html:8 templates/header.html:13 templates/index.html:120 msgid "about" -msgstr "acerca de nosotros" +msgstr "" -#: templates/footer.html:8 templates/header.html:15 templates/index.html:84 -#: templates/question_edit_tips.html:16 +#: templates/footer.html:9 templates/header.html:14 templates/index.html:121 +#: templates/question_edit_tips.html:17 msgid "faq" -msgstr "preguntas frecuentes" +msgstr "" -#: templates/footer.html:9 +#: templates/footer.html:10 msgid "blog" -msgstr "blog" +msgstr "" -#: templates/footer.html:10 +#: templates/footer.html:11 msgid "contact us" -msgstr "contactenos" +msgstr "" -#: templates/footer.html:11 +#: templates/footer.html:12 msgid "privacy policy" -msgstr "código de privacidad" +msgstr "" -#: templates/footer.html:12 +#: templates/footer.html:21 msgid "give feedback" -msgstr "envía comentarios" - -#: templates/footer.html:18 -msgid "current revision" -msgstr "revisión actual" +msgstr "" -#: templates/header.html:10 +#: templates/header.html:9 msgid "logout" -msgstr "salir" +msgstr "" -#: templates/header.html:12 templates/authopenid/signup.html:41 +#: templates/header.html:11 msgid "login" -msgstr "entrar" +msgstr "" -#: templates/header.html:23 +#: templates/header.html:21 msgid "back to home page" -msgstr "volver página principal" +msgstr "" -#: templates/header.html:31 templates/header.html.py:65 +#: templates/header.html:29 templates/header.html.py:63 msgid "users" -msgstr "usuarios" +msgstr "" -#: templates/header.html:33 +#: templates/header.html:31 msgid "books" -msgstr "libros" +msgstr "" -#: templates/header.html:36 +#: templates/header.html:34 msgid "unanswered questions" -msgstr "sin respuesta" +msgstr "" -#: templates/header.html:40 +#: templates/header.html:38 msgid "my profile" -msgstr "mi perfil" +msgstr "" -#: templates/header.html:44 +#: templates/header.html:42 msgid "ask a question" -msgstr "hacer una pregunta" +msgstr "" -#: templates/header.html:59 +#: templates/header.html:57 msgid "search" -msgstr "buscar" +msgstr "" -#: templates/index.html:7 +#: templates/index.html:8 msgid "Home" -msgstr "Inicio" +msgstr "" -#: templates/index.html:22 templates/questions.html:7 +#: templates/index.html:25 templates/questions.html:8 msgid "Questions" -msgstr "Preguntas" +msgstr "" -#: templates/index.html:24 +#: templates/index.html:27 msgid "last updated questions" -msgstr "ultimas preguntas actualizadas" +msgstr "" -#: templates/index.html:24 templates/questions.html:25 -#: templates/unanswered.html:20 +#: templates/index.html:27 templates/questions.html:51 +#: templates/unanswered.html:21 msgid "newest" -msgstr "más nuevas" +msgstr "" + +#: templates/index.html:28 templates/questions.html:52 +msgid "most recently updated questions" +msgstr "" + +#: templates/index.html:28 templates/questions.html:52 +msgid "active" +msgstr "" -#: templates/index.html:25 templates/questions.html:27 +#: templates/index.html:29 templates/questions.html:53 msgid "hottest questions" -msgstr "preguntas calientes" +msgstr "" -#: templates/index.html:25 templates/questions.html:27 +#: templates/index.html:29 templates/questions.html:53 msgid "hottest" -msgstr "más calientes" +msgstr "" -#: templates/index.html:26 templates/questions.html:28 +#: templates/index.html:30 templates/questions.html:54 msgid "most voted questions" -msgstr "preguntas más votadas" +msgstr "" -#: templates/index.html:26 templates/questions.html:28 +#: templates/index.html:30 templates/questions.html:54 msgid "most voted" -msgstr "más votadas" +msgstr "" -#: templates/index.html:27 +#: templates/index.html:31 msgid "all questions" -msgstr "todas las preguntas" +msgstr "" -#: templates/index.html:47 templates/questions.html:45 -#: templates/unanswered.html:36 templates/users_questions.html:37 +#: templates/index.html:49 templates/question_summary_list_roll.html:13 +#: templates/questions.html:83 templates/unanswered.html:38 +#: templates/users_questions.html:36 msgid "answers" -msgstr "respuestas" +msgstr "" -#: templates/index.html:69 templates/question.html:500 -#: templates/questions.html:84 templates/questions.html.py:157 -#: templates/tags.html:49 templates/unanswered.html:75 -#: templates/unanswered.html.py:106 templates/users_questions.html:54 +#: templates/index.html:81 templates/index.html.py:95 +#: templates/questions.html:115 templates/questions.html.py:129 +#: templates/unanswered.html:70 templates/unanswered.html.py:84 +msgid "Posted:" +msgstr "" + +#: templates/index.html:84 templates/index.html.py:89 +#: templates/questions.html:118 templates/questions.html.py:123 +#: templates/unanswered.html:73 templates/unanswered.html.py:78 +msgid "Updated:" +msgstr "" + +#: templates/index.html:106 templates/question.html:488 +#: templates/question_summary_list_roll.html:52 templates/questions.html:140 +#: templates/questions.html.py:257 templates/tags.html:49 +#: templates/unanswered.html:95 templates/unanswered.html.py:122 +#: templates/users_questions.html:52 msgid "see questions tagged" -msgstr "ver preguntas etiquetadas" +msgstr "" -#: templates/index.html:80 +#: templates/index.html:117 msgid "welcome to website" -msgstr "bienvenido a sitio" +msgstr "" -#: templates/index.html:89 +#: templates/index.html:128 msgid "Recent tags" -msgstr "Etiquetas recientes" +msgstr "" -#: templates/index.html:94 templates/question.html:125 +#: templates/index.html:133 templates/question.html:135 #, python-format msgid "see questions tagged '%(tagname)s'" -msgstr "ver preguntas etiquetadas '%(tagname)s'" +msgstr "" -#: templates/index.html:97 templates/index.html.py:123 +#: templates/index.html:136 templates/index.html.py:162 msgid "popular tags" -msgstr "etiquetas populares" +msgstr "" -#: templates/index.html:102 +#: templates/index.html:141 msgid "Recent awards" -msgstr "Reconocimientos recientes" +msgstr "" -#: templates/index.html:108 +#: templates/index.html:147 msgid "given to" -msgstr "dados a" +msgstr "" -#: templates/index.html:113 +#: templates/index.html:152 msgid "all awards" -msgstr "todos los reconocimientos" +msgstr "" -#: templates/index.html:118 +#: templates/index.html:157 msgid "subscribe to last 30 questions by RSS" -msgstr "suscribirse a las últimas 30 preguntas por RSS" +msgstr "" -#: templates/index.html:123 +#: templates/index.html:162 msgid "Still looking for more? See" -msgstr "¿Aún sigues buscando más? Ver" +msgstr "" -#: templates/index.html:123 +#: templates/index.html:162 msgid "complete list of questions" -msgstr "lista completa de preguntas" +msgstr "" -#: templates/index.html:123 +#: templates/index.html:162 templates/authopenid/signup.html:18 msgid "or" -msgstr "ó" +msgstr "" -#: templates/index.html:123 +#: templates/index.html:162 msgid "Please help us answer" -msgstr "Ayudanos a responder" +msgstr "" -#: templates/index.html:123 +#: templates/index.html:162 msgid "list of unanswered questions" -msgstr "lista de preguntas sin respuesta" +msgstr "" -#: templates/logout.html:6 templates/logout.html.py:17 +#: templates/logout.html:6 templates/logout.html.py:16 msgid "Logout" -msgstr "Salir" +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 "" -"Como usuario registrado puedes ingresar con tu OpenID, salir del sitio o " -"eliminar de forma permanente tu cuenta." -#: templates/logout.html:21 +#: templates/logout.html:20 msgid "Logout now" -msgstr "Salir ahora" +msgstr "" #: templates/pagesize.html:6 msgid "posts per page" -msgstr "entradas por página" +msgstr "" #: templates/paginator.html:6 templates/paginator.html.py:7 msgid "previous" -msgstr "previo" +msgstr "" #: templates/paginator.html:19 msgid "current page" -msgstr "página actúal" +msgstr "" #: templates/paginator.html:22 templates/paginator.html.py:29 msgid "page number " -msgstr "número de página" +msgstr "" #: templates/paginator.html:22 templates/paginator.html.py:29 msgid "number - make blank in english" -msgstr " " +msgstr "" #: templates/paginator.html:33 msgid "next page" -msgstr "próxima página" +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 +msgid "asked" +msgstr "" + +#: templates/post_contributor_info.html:22 +msgid "answered" +msgstr "" + +#: templates/post_contributor_info.html:24 +msgid "posted" +msgstr "" + +#: templates/post_contributor_info.html:45 +msgid "updated" +msgstr "" #: templates/privacy.html:6 templates/privacy.html.py:11 msgid "Privacy policy" -msgstr "Privacidad" +msgstr "" #: templates/privacy.html:15 msgid "general message about privacy" -msgstr "mensaje de privacidad" +msgstr "" #: templates/privacy.html:18 msgid "Site Visitors" -msgstr "Visitantes del Sitio" +msgstr "" #: templates/privacy.html:20 msgid "what technical information is collected about visitors" -msgstr "que información es recolectada sobre los usuarios" +msgstr "" #: templates/privacy.html:23 msgid "Personal Information" -msgstr "Información Personal" +msgstr "" #: templates/privacy.html:25 msgid "details on personal information policies" -msgstr "describir código de manejo de la información personal" +msgstr "" #: templates/privacy.html:28 msgid "Other Services" -msgstr "Otros servicios" +msgstr "" #: templates/privacy.html:30 msgid "details on sharing data with third parties" -msgstr "detalles sobre compartir información con terceros" +msgstr "" #: templates/privacy.html:35 msgid "cookie policy details" -msgstr "uso de cookies" +msgstr "" #: templates/privacy.html:37 msgid "Policy Changes" -msgstr "Cambios de Códigos" +msgstr "" #: templates/privacy.html:38 msgid "how privacy policies can be changed" -msgstr "como pueden ser cambiados los códigos de privacidad" +msgstr "" -#: 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 "Me gusta esta entrada (clickear devuelta para cancelar)" +msgstr "" -#: templates/question.html:75 templates/question.html.py:89 -#: templates/question.html:290 +#: templates/question.html:80 templates/question.html.py:98 +#: templates/question.html:257 msgid "current number of votes" -msgstr "número actual de votos" +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 "No me gusta esta entrada (clickear devuelta para cancelar)" +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 "marcar esta pregunta como favorita (clickear devuelta para cancelar)" +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 "" -"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/revisions_answer.html:53 templates/revisions_question.html:53 +#: templates/question.html:140 templates/question.html.py:294 +#: templates/revisions_answer.html:58 templates/revisions_question.html:58 msgid "edit" -msgstr "editar" - -#: templates/question.html:139 templates/question.html.py:333 -msgid "delete" -msgstr "borrar" +msgstr "" -#: templates/question.html:144 +#: templates/question.html:145 msgid "reopen" -msgstr "re-abrir" +msgstr "" #: templates/question.html:149 msgid "close" -msgstr "cerrar" +msgstr "" -#: templates/question.html:155 templates/question.html.py:346 +#: templates/question.html:155 templates/question.html.py:300 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:156 templates/question.html.py:301 msgid "flag offensive" -msgstr "marcar como ofensivo" - -#: templates/question.html:168 templates/question.html.py:356 -#: templates/revisions_answer.html:65 templates/revisions_question.html:65 -msgid "updated" -msgstr "actualizado" +msgstr "" -#: templates/question.html:217 templates/question.html.py:403 -#: templates/revisions_answer.html:63 templates/revisions_question.html:63 -msgid "asked" -msgstr "preguntado" +#: templates/question.html:164 templates/question.html.py:312 +msgid "delete" +msgstr "" -#: templates/question.html:247 templates/question.html.py:430 -msgid "comments" -msgstr "comentarios" +#: templates/question.html:182 templates/question.html.py:332 +msgid "delete this comment" +msgstr "" -#: templates/question.html:248 templates/question.html.py:431 +#: templates/question.html:193 templates/question.html.py:343 +#: templates/question.html:367 msgid "add comment" -msgstr "agregar comentario" +msgstr "" + +#: templates/question.html:197 +#, python-format +msgid "" +"\n" +" see <strong>one</strong> more \n" +" " +msgid_plural "" +"\n" +" see <strong>%(counter)s</strong> " +"more\n" +" " +msgstr[0] "" +msgstr[1] "" -#: 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:203 +#, python-format +msgid "" +"\n" +" see <strong>one</strong> more " +"comment\n" +" " +msgid_plural "" +"\n" +" see <strong>%(counter)s</strong> " +"more comments\n" +" " +msgstr[0] "" +msgstr[1] "" -#: templates/question.html:261 -msgid "by" -msgstr "por" +#: templates/question.html:219 +#, python-format +msgid "" +"The question has been closed for the following reason \"%(close_reason)s\" by" +msgstr "" -#: templates/question.html:263 -msgid "close date " -msgstr "fecha de cierre" +#: templates/question.html:221 +#, python-format +msgid "close date %(closed_at)s" +msgstr "" -#: templates/question.html:270 templates/user_stats.html:13 -msgid "Answers" -msgstr "Respuestas" +#: 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:272 +#: templates/question.html:237 msgid "oldest answers will be shown first" -msgstr "la respuesta mas vieja será mostrada primero" +msgstr "" -#: templates/question.html:272 +#: templates/question.html:237 msgid "oldest answers" -msgstr "pregunta más vieja" +msgstr "" -#: templates/question.html:273 +#: templates/question.html:239 msgid "newest answers will be shown first" -msgstr "preguntas más nuevas serán mostradas primero" +msgstr "" -#: templates/question.html:273 +#: templates/question.html:239 msgid "newest answers" -msgstr "más nuevas" +msgstr "" -#: templates/question.html:274 +#: templates/question.html:241 msgid "most voted answers will be shown first" -msgstr "las preguntas más votadas serán mostradas primero" +msgstr "" -#: templates/question.html:274 +#: templates/question.html:241 msgid "popular answers" -msgstr "respuestas populares" +msgstr "" -#: templates/question.html:288 templates/question.html.py:289 +#: templates/question.html:255 templates/question.html.py:256 msgid "i like this answer (click again to cancel)" -msgstr "me gusta esta respuesta (clickear devuelta para cancelar)" +msgstr "" -#: templates/question.html:295 templates/question.html.py:296 +#: templates/question.html:262 templates/question.html.py:263 msgid "i dont like this answer (click again to cancel)" -msgstr "no me gusta esta respuesta (clickear devuelta para cancelar)" +msgstr "" -#: templates/question.html:301 templates/question.html.py:302 +#: templates/question.html:268 templates/question.html.py:269 msgid "mark this answer as favorite (click again to undo)" -msgstr "marcar esta respuesta como favorita (clickear devuelta para deshacer)" +msgstr "" -#: templates/question.html:307 templates/question.html.py:308 +#: templates/question.html:274 templates/question.html.py:275 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 -msgid "undelete" -msgstr "deshacer eliminar" +msgstr "" -#: templates/question.html:340 +#: templates/question.html:288 msgid "answer permanent link" -msgstr "enlace permanente a respuesta" +msgstr "" -#: templates/question.html:341 +#: templates/question.html:289 msgid "permanent link" -msgstr "enlace permanente" +msgstr "" + +#: templates/question.html:312 +msgid "undelete" +msgstr "" + +#: templates/question.html:347 +#, python-format +msgid "" +"\n" +" see <strong>one</" +"strong> more \n" +" " +msgid_plural "" +"\n" +" see <strong>%" +"(counter)s</strong> more\n" +" " +msgstr[0] "" +msgstr[1] "" + +#: templates/question.html:353 +#, python-format +msgid "" +"\n" +" see <strong>one</" +"strong> more comment\n" +" " +msgid_plural "" +"\n" +" see <strong>%" +"(counter)s</strong> more comments\n" +" " +msgstr[0] "" +msgstr[1] "" + +#: templates/question.html:366 +msgid "comments" +msgstr "" -#: templates/question.html:454 +#: templates/question.html:386 templates/question.html.py:389 +msgid "Notify me once a day when there are any new answers" +msgstr "" + +#: templates/question.html:392 +msgid "Notify me weekly when there are any new answers" +msgstr "" + +#: templates/question.html:397 +#, python-format +msgid "" +"\n" +" You can always adjust frequency of email updates from your %" +"(profile_url)s\n" +" " +msgstr "" + +#: templates/question.html:404 +msgid "once you sign in you will be able to subscribe for any updates here" +msgstr "" + +#: templates/question.html:415 msgid "Your answer" -msgstr "Tu respuesta" +msgstr "" -#: templates/question.html:457 +#: templates/question.html:417 +msgid "Be the first one to answer this question!" +msgstr "" + +#: templates/question.html:423 msgid "you can answer anonymously and then login" -msgstr "puedes responder de forma anónima y luego ingresar" +msgstr "" -#: templates/question.html:480 -msgid "Answer the question" -msgstr "Responde la pregunta" +#: templates/question.html:427 +msgid "answer your own question only to give an answer" +msgstr "" -#: templates/question.html:482 -msgid "Notify me daily if there are any new answers." -msgstr "Notificarme diariamente si hay nuevas respuestas." +#: templates/question.html:429 +msgid "please only give an answer, no discussions" +msgstr "" -#: templates/question.html:484 -msgid "once you sign in you will be able to subscribe for any updates here" +#: templates/question.html:465 +msgid "Login/Signup to Post Your Answer" +msgstr "" + +#: templates/question.html:468 +msgid "Answer Your Own Question" +msgstr "" + +#: templates/question.html:470 +msgid "Answer the question" msgstr "" -"una vez que hayas ingresado podrás suscribirte a cualquiera de las " -"actualizaciones aquí." -#: templates/question.html:495 +#: templates/question.html:483 msgid "Question tags" -msgstr "Tags de la pregunta" +msgstr "" -#: templates/question.html:505 +#: templates/question.html:493 msgid "question asked" -msgstr "pregunta preguntada" - -#: templates/question.html:505 templates/question.html.py:511 -#: templates/user_info.html:51 -msgid "ago" -msgstr " atras" +msgstr "" -#: templates/question.html:508 +#: templates/question.html:496 msgid "question was seen" -msgstr "la pregunta fue vista" +msgstr "" -#: templates/question.html:508 +#: templates/question.html:496 msgid "times" -msgstr "veces" +msgstr "" -#: templates/question.html:511 +#: templates/question.html:499 msgid "last updated" -msgstr "última vez actualizada" +msgstr "" -#: templates/question.html:516 +#: templates/question.html:504 msgid "Related questions" -msgstr "Preguntas relacionadas" +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 "Editar pregunta" +msgstr "" #: templates/question_edit_tips.html:4 msgid "question tips" -msgstr "sugerencias sobre pregunta" +msgstr "" #: templates/question_edit_tips.html:7 msgid "please ask a relevant question" -msgstr "por favor hacer preguntas relevantes" +msgstr "" #: templates/question_edit_tips.html:10 msgid "please try provide enough details" -msgstr "intente proveer suficientes detalles" +msgstr "" -#: 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 "Cambiar etiquetas" +msgstr "" -#: templates/question_retag.html:39 +#: templates/question_retag.html:40 msgid "up to 5 tags, less than 20 characters each" -msgstr "hasta 5 etiquetas, menos de 20 caracteres cada una" +msgstr "" -#: templates/question_retag.html:82 +#: templates/question_retag.html:83 msgid "Why use and modify tags?" -msgstr "¿Porqué usar y modificar etiquetas?" +msgstr "" -#: templates/question_retag.html:85 +#: templates/question_retag.html:86 msgid "tags help us keep Questions organized" -msgstr "las etiquetas nos permiten mantener las Preguntas organizadas" +msgstr "" -#: templates/question_retag.html:91 +#: templates/question_retag.html:94 msgid "tag editors receive special awards from the community" msgstr "" -"los editores de etiquetas reciben distinciones especiales de la comunidad" -#: templates/questions.html:23 +#: templates/questions.html:28 templates/questions.html.py:32 msgid "Found by tags" -msgstr "Encontradas por etiqueta" +msgstr "" -#: templates/questions.html:23 +#: templates/questions.html:28 templates/questions.html.py:38 msgid "Found by title" -msgstr "Encontradas por título" +msgstr "" -#: templates/questions.html:23 +#: templates/questions.html:28 templates/questions.html.py:44 msgid "All questions" -msgstr "Todas las preguntas" +msgstr "" + +#: templates/questions.html:36 +msgid "Search results" +msgstr "" -#: templates/questions.html:25 templates/unanswered.html:20 +#: templates/questions.html:42 templates/unanswered.html:8 +#: templates/unanswered.html.py:19 +msgid "Unanswered questions" +msgstr "" + +#: templates/questions.html:51 templates/unanswered.html:21 msgid "most recently asked questions" -msgstr "preguntas hechas más recientemente" +msgstr "" -#: templates/questions.html:26 -msgid "most recently updated questions" -msgstr "preguntas actualizadas más recientemente" +#: templates/questions.html:143 +msgid "Category: " +msgstr "" -#: templates/questions.html:26 -msgid "active" -msgstr "actividad" +#: templates/questions.html:149 +msgid "Did not find anything?" +msgstr "" + +#: templates/questions.html:152 +msgid "Did not find what you were looking for?" +msgstr "" -#: templates/questions.html:110 +#: templates/questions.html:154 +msgid "Please, post your question!" +msgstr "" + +#: templates/questions.html:169 #, python-format msgid "" "\n" @@ -1928,15 +2300,9 @@ msgid_plural "" "\t\t\thave total %(q_num)s questions tagged %(tagname)s\n" "\t\t\t" msgstr[0] "" -"\n" -"\t\t\ttiene un total de %(q_num)s preguntas etiquetadas con %(tagname)s\n" -"\t\t\t" msgstr[1] "" -"\n" -"\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:176 #, python-format msgid "" "\n" @@ -1947,16 +2313,10 @@ msgid_plural "" "\t\t\t\thave total %(q_num)s questions containing %(searchtitle)s\n" "\t\t\t\t" msgstr[0] "" -"\n" -"\t\t\thay un total de %(q_num)s preguntas que contienen %(searchtitle)s\n" -"\t\t\t" msgstr[1] "" -"\n" -"\t\t\thay un total de %(q_num)s pregunta que contiene %(searchtitle)s\n" -"\t\t\t" -#: templates/questions.html:123 -#, fuzzy, python-format +#: templates/questions.html:182 +#, python-format msgid "" "\n" "\t\t\t\thave total %(q_num)s questions\n" @@ -1965,303 +2325,458 @@ msgid_plural "" "\n" "\t\t\t\thave total %(q_num)s questions\n" "\t\t\t\t" -msgstr[0] "ver preguntas etiquetadas '%(tagname)s'" -msgstr[1] "ver pregunta etiquetada '%(tagname)s'" +msgstr[0] "" +msgstr[1] "" + +#: templates/questions.html:191 +#, python-format +msgid "" +"\n" +" have total %(q_num)s questions tagged %(tagname)s\n" +" " +msgid_plural "" +"\n" +" have total %(q_num)s questions tagged %(tagname)s\n" +" " +msgstr[0] "" +msgstr[1] "" + +#: templates/questions.html:199 +#, python-format +msgid "" +"\n" +" have total %(q_num)s questions containing %(searchtitle)" +"s in full text\n" +" " +msgid_plural "" +"\n" +" have total %(q_num)s questions containing %(searchtitle)" +"s in full text\n" +" " +msgstr[0] "" +msgstr[1] "" + +#: templates/questions.html:205 +#, python-format +msgid "" +"\n" +" have total %(q_num)s questions containing %(searchtitle)" +"s\n" +" " +msgid_plural "" +"\n" +" have total %(q_num)s questions containing %(searchtitle)" +"s\n" +" " +msgstr[0] "" +msgstr[1] "" + +#: templates/questions.html:213 +#, python-format +msgid "" +"\n" +" have total %(q_num)s unanswered questions\n" +" " +msgid_plural "" +"\n" +" have total %(q_num)s unanswered questions\n" +" " +msgstr[0] "" +msgstr[1] "" -#: templates/questions.html:132 +#: templates/questions.html:219 +#, python-format +msgid "" +"\n" +" have total %(q_num)s questions\n" +" " +msgid_plural "" +"\n" +" have total %(q_num)s questions\n" +" " +msgstr[0] "" +msgstr[1] "" + +#: templates/questions.html:230 msgid "latest questions info" -msgstr "<strong>Más recientes</strong> preguntas son mostradas primero." +msgstr "" -#: templates/questions.html:136 +#: templates/questions.html:234 msgid "Questions are sorted by the <strong>time of last update</strong>." msgstr "" -"Las preguntas estan ordenadas por <strong>fecha de último update</strong>." -#: templates/questions.html:137 +#: templates/questions.html:235 msgid "Most recently answered ones are shown first." -msgstr "Las más recientemente respondidas son mostradas primero." +msgstr "" -#: templates/questions.html:141 +#: templates/questions.html:239 msgid "Questions sorted by <strong>number of responses</strong>." -msgstr "Preguntas ordenadas por <strong>número de respuestas</strong>." +msgstr "" -#: templates/questions.html:142 +#: templates/questions.html:240 msgid "Most answered questions are shown first." -msgstr "Preguntas más respondidas aparecen primero." +msgstr "" -#: templates/questions.html:146 +#: templates/questions.html:244 msgid "Questions are sorted by the <strong>number of votes</strong>." -msgstr "Las preguntas son ordenadas por el <strong>número de votos</strong>." +msgstr "" -#: templates/questions.html:147 +#: templates/questions.html:245 msgid "Most voted questions are shown first." -msgstr "Las preguntas más votadas son mostradas primero." +msgstr "" -#: templates/questions.html:154 templates/unanswered.html:102 +#: templates/questions.html:253 templates/unanswered.html:118 msgid "Related tags" -msgstr "Etiquetas relacionadas" +msgstr "" + +#: templates/questions.html:263 templates/tag_selector.html:10 +#: templates/tag_selector.html.py:27 +#, python-format +msgid "see questions tagged '%(tag_name)s'" +msgstr "" #: templates/reopen.html:6 templates/reopen.html.py:16 msgid "Reopen question" -msgstr "Re-abrir pregunta" +msgstr "" #: templates/reopen.html:19 msgid "Open the previously closed question" -msgstr "Abrir pregunta previamente cerrada" +msgstr "" #: templates/reopen.html:22 msgid "The question was closed for the following reason " -msgstr "La pregunta fue cerrada por el siguiente motivo " +msgstr "" #: templates/reopen.html:22 msgid "reason - leave blank in english" -msgstr "razón - " +msgstr "" #: templates/reopen.html:22 msgid "on " -msgstr "el " +msgstr "" #: templates/reopen.html:22 msgid "date closed" -msgstr "fecha cerrada" +msgstr "" #: templates/reopen.html:29 msgid "Reopen this question" -msgstr "Re-abrir esta pregunta" +msgstr "" -#: templates/revisions_answer.html:7 templates/revisions_answer.html.py:36 -#: templates/revisions_question.html:8 templates/revisions_question.html:36 +#: templates/revisions_answer.html:7 templates/revisions_answer.html.py:38 +#: templates/revisions_question.html:8 templates/revisions_question.html:38 msgid "Revision history" -msgstr "Historial de revisiones" +msgstr "" + +#: templates/revisions_answer.html:50 templates/revisions_question.html:50 +msgid "click to hide/show revision" +msgstr "" + +#: templates/tag_selector.html:4 +msgid "Interesting tags" +msgstr "" + +#: templates/tag_selector.html:14 +#, python-format +msgid "remove '%(tag_name)s' from the list of interesting tags" +msgstr "" + +#: templates/tag_selector.html:20 templates/tag_selector.html.py:37 +msgid "Add" +msgstr "" + +#: templates/tag_selector.html:21 +msgid "Ignored tags" +msgstr "" + +#: templates/tag_selector.html:31 +#, python-format +msgid "remove '%(tag_name)s' from the list of ignored tags" +msgstr "" + +#: templates/tag_selector.html:40 +msgid "keep ingored questions hidden" +msgstr "" #: templates/tags.html:6 templates/tags.html.py:30 msgid "Tag list" -msgstr "Lista de etiquetas" +msgstr "" #: templates/tags.html:32 msgid "sorted alphabetically" -msgstr "ordenar alfabéticamente" +msgstr "" #: templates/tags.html:32 msgid "by name" -msgstr "por nombre" +msgstr "" #: templates/tags.html:33 msgid "sorted by frequency of tag use" -msgstr "ordenar por frecuencia de uso de la etiqueta" +msgstr "" #: templates/tags.html:33 msgid "by popularity" -msgstr "por popularidad" +msgstr "" #: templates/tags.html:39 msgid "All tags matching query" -msgstr "Todas las etiquetas que coincidan con la busqueda" +msgstr "" #: templates/tags.html:39 msgid "all tags - make this empty in english" -msgstr "todas las tags" +msgstr "" -#: templates/unanswered.html:7 templates/unanswered.html.py:18 -msgid "Unanswered questions" -msgstr "Preguntas sin respuesta" +#: templates/tags.html:42 +msgid "Nothing found" +msgstr "" -#: templates/unanswered.html:97 +#: templates/unanswered.html:114 #, python-format msgid "have %(num_q)s unanswered questions" msgstr "" -"<div class=\"questions-count\">%(num_q)s</div> preguntas <strong>sin " -"respuesta</strong> " #: templates/user_edit.html:6 msgid "Edit user profile" -msgstr "Editar perfil de usuario" +msgstr "" #: templates/user_edit.html:19 msgid "edit profile" -msgstr "editar perfil" +msgstr "" #: templates/user_edit.html:31 msgid "image associated with your email address" -msgstr "imagen asociada con tu email" +msgstr "" #: templates/user_edit.html:31 -msgid "avatar" -msgstr "avatar" +#, python-format +msgid "avatar, see %(gravatar_faq_url)s" +msgstr "" -#: templates/user_edit.html:36 templates/user_info.html:31 +#: templates/user_edit.html:36 templates/user_info.html:56 msgid "Registered user" -msgstr "Usuario registrado" +msgstr "" -#: templates/user_edit.html:82 +#: templates/user_edit.html:86 templates/user_email_subscriptions.html:23 msgid "Update" -msgstr "Actualización" +msgstr "" + +#: templates/user_email_subscriptions.html:8 +msgid "Email subscription settings" +msgstr "" + +#: templates/user_email_subscriptions.html:9 +msgid "email subscription settings info" +msgstr "" + +#: templates/user_email_subscriptions.html:24 +msgid "Stop sending email" +msgstr "" + +#: templates/user_info.html:22 +msgid "karma" +msgstr "" + +#: templates/user_info.html:32 +msgid "Moderate this user" +msgstr "" -#: templates/user_info.html:34 +#: templates/user_info.html:45 msgid "update profile" -msgstr "actualizar perfil de usuario" +msgstr "" -#: templates/user_info.html:40 +#: templates/user_info.html:60 msgid "real name" -msgstr "nombre real" +msgstr "" -#: templates/user_info.html:45 +#: templates/user_info.html:65 msgid "member for" -msgstr "miembro de" +msgstr "" -#: templates/user_info.html:50 +#: templates/user_info.html:70 msgid "last seen" -msgstr "última vez visto" +msgstr "" -#: templates/user_info.html:56 +#: templates/user_info.html:76 msgid "user website" -msgstr "sitio web del usuario" +msgstr "" -#: templates/user_info.html:62 +#: templates/user_info.html:82 msgid "location" -msgstr "ubicación" +msgstr "" -#: templates/user_info.html:69 +#: templates/user_info.html:89 msgid "age" -msgstr "edad" +msgstr "" -#: templates/user_info.html:70 +#: templates/user_info.html:90 msgid "age unit" -msgstr "unidad de edad" +msgstr "" -#: templates/user_info.html:76 +#: templates/user_info.html:96 msgid "todays unused votes" -msgstr "votos de hoy no usados" +msgstr "" -#: templates/user_info.html:77 +#: templates/user_info.html:97 msgid "votes left" -msgstr "votos restantes" - -#: templates/user_preferences.html:10 -msgid "Connect with Twitter" -msgstr "Conectar con Twitter" - -#: templates/user_preferences.html:13 -msgid "Twitter account name:" -msgstr "Nombre de usuario en Twitter:" - -#: templates/user_preferences.html:15 -msgid "Twitter password:" -msgstr "Contraseña de Twitter:" - -#: templates/user_preferences.html:17 -msgid "Send my Questions to Twitter" -msgstr "Enviar mis preguntas a Twitter" - -#: templates/user_preferences.html:18 -msgid "Send my Answers to Twitter" -msgstr "Enviar mis respuestas a Twitter" +msgstr "" -#: templates/user_preferences.html:19 -msgid "Save" -msgstr "Guardar" +#: templates/user_stats.html:12 +#, python-format +msgid "" +"\n" +" <span class=\"count\">1</span> Question\n" +" " +msgid_plural "" +"\n" +" <span class=\"count\">%(counter)s</span> Questions\n" +" " +msgstr[0] "" +msgstr[1] "" -#: templates/user_stats.html:10 -msgid "User questions" -msgstr "Preguntas del usuario" +#: templates/user_stats.html:23 +#, python-format +msgid "" +"\n" +" <span class=\"count\">1</span> Answer\n" +" " +msgid_plural "" +"\n" +" <span class=\"count\">%(counter)s</span> 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 "la respuesta ha sido votada %(vote_count)s veces" +msgstr "" -#: templates/user_stats.html:20 +#: templates/user_stats.html:36 msgid "this answer has been selected as correct" -msgstr "esta respuesta ha sido seleccionada como correcta" +msgstr "" -#: 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" +msgid "" +"\n" +" (one comment)\n" +" " +msgid_plural "" +"\n" +" the answer has been commented %(comment_count)s times\n" +" " +msgstr[0] "" +msgstr[1] "" -#: templates/user_stats.html:36 -#, fuzzy -msgid "Votes" -msgstr "votos" +#: templates/user_stats.html:61 +#, python-format +msgid "" +"\n" +" <span class=\"count\">1</span> Vote\n" +" " +msgid_plural "" +"\n" +" <span class=\"count\">%(cnt)s</span> 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 "el usuario ha votado positivo esta cantidad de veces" +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 "el usuario voto negativo esta cantidad de veces" +msgstr "" -#: templates/user_stats.html:54 -msgid "Tags" -msgstr "Etiquetas" +#: templates/user_stats.html:87 +#, python-format +msgid "" +"\n" +" <span class=\"count\">1</span> Tag\n" +" " +msgid_plural "" +"\n" +" <span class=\"count\">%(counter)s</span> Tags\n" +" " +msgstr[0] "" +msgstr[1] "" -#: templates/user_stats.html:61 +#: templates/user_stats.html:100 +#, python-format +msgid "" +"see other questions with %(view_user)s's contributions tagged '%(tag_name)s' " +msgstr "" + +#: templates/user_stats.html:115 #, python-format -msgid "see other questions tagged '%(tag)s' " -msgstr "ver otras preguntas etiqueteadas '%(tag)s'" +msgid "" +"\n" +" <span class=\"count\">1</span> Badge\n" +" " +msgid_plural "" +"\n" +" <span class=\"count\">%(counter)s</span> Badges\n" +" " +msgstr[0] "" +msgstr[1] "" #: templates/user_tabs.html:7 msgid "User profile" -msgstr "Perfil de usuario" +msgstr "" #: templates/user_tabs.html:16 msgid "graph of user reputation" -msgstr "gráfica de la reputación del usuario" +msgstr "" #: templates/user_tabs.html:17 msgid "reputation history" -msgstr "historial de reputación" +msgstr "" #: templates/user_tabs.html:23 -#, fuzzy msgid "questions that user selected as his/her favorite" -msgstr "esta pregunta ha sido seleccionada como favorita" +msgstr "" #: templates/user_tabs.html:24 msgid "favorites" -msgstr "favoritos" - -#: templates/user_tabs.html:28 -msgid "settings" -msgstr "preferencias" +msgstr "" #: templates/users.html:6 templates/users.html.py:24 msgid "Users" -msgstr "Usuarios" +msgstr "" #: templates/users.html:27 msgid "recent" -msgstr "reciente" +msgstr "" #: templates/users.html:28 msgid "oldest" -msgstr "más viejo" +msgstr "" #: templates/users.html:29 msgid "by username" -msgstr "por nombre de usuario" +msgstr "" #: templates/users.html:35 #, python-format msgid "users matching query %(suser)s:" -msgstr "usuarios que coincidan con la busqueda %(suser)s:" +msgstr "" #: templates/users.html:39 msgid "Nothing found." -msgstr "Nada encontrado." +msgstr "" #: templates/users_questions.html:11 msgid "this questions was selected as favorite" -msgstr "esta pregunta ha sido seleccionada como favorita" +msgstr "" #: templates/users_questions.html:12 msgid "thumb-up on" @@ -2271,276 +2786,289 @@ 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 "esta respuesta ha sido aceptada como correcta" +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 "Cambiar dirección email" +msgstr "" + +#: templates/authopenid/changeemail.html:11 +msgid "Save your email address" +msgstr "" -#: templates/authopenid/changeemail.html:10 +#: templates/authopenid/changeemail.html:16 #, python-format msgid "change %(email)s info" -msgstr "Cambiar información del correo electrónico %(email)s" +msgstr "" -#: 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:" -msgstr "Por favor corrija los errores debajo: " +#: templates/authopenid/changeemail.html:18 +#, python-format +msgid "here is why email is required, see %(gravatar_faq_url)s" +msgstr "" -#: templates/authopenid/changeemail.html:30 +#: templates/authopenid/changeemail.html:31 msgid "Your new Email" -msgstr "Tu nuevo Email" +msgstr "" #: templates/authopenid/changeemail.html:31 -#: templates/authopenid/signin.html:143 -msgid "Password" -msgstr "Contraseña" +msgid "Your Email" +msgstr "" -#: templates/authopenid/changeemail.html:42 -#, fuzzy +#: templates/authopenid/changeemail.html:38 +msgid "Save Email" +msgstr "" + +#: templates/authopenid/changeemail.html:49 msgid "Validate email" -msgstr "Cambiar dirección email" +msgstr "" -#: templates/authopenid/changeemail.html:45 +#: templates/authopenid/changeemail.html:52 #, python-format -msgid "validate %(email)s info" -msgstr "validar información de %(email)s " +msgid "validate %(email)s info or go to %(change_email_url)s" +msgstr "" -#: templates/authopenid/changeemail.html:50 +#: templates/authopenid/changeemail.html:57 msgid "Email not changed" -msgstr "Email no modificado." +msgstr "" -#: templates/authopenid/changeemail.html:53 +#: templates/authopenid/changeemail.html:60 #, python-format -msgid "old %(email)s kept" -msgstr "se ha conservado el viejo email %(email)s " +msgid "old %(email)s kept, if you like go to %(change_email_url)s" +msgstr "" -#: templates/authopenid/changeemail.html:58 +#: templates/authopenid/changeemail.html:65 msgid "Email changed" -msgstr "Email modificado." +msgstr "" -#: templates/authopenid/changeemail.html:61 +#: templates/authopenid/changeemail.html:68 #, python-format msgid "your current %(email)s can be used for this" -msgstr "tu email actual %(email)s puede ser usado para esto" +msgstr "" -#: templates/authopenid/changeemail.html:66 +#: templates/authopenid/changeemail.html:73 msgid "Email verified" -msgstr "Email verificado" +msgstr "" -#: templates/authopenid/changeemail.html:69 +#: templates/authopenid/changeemail.html:76 msgid "thanks for verifying email" -msgstr "gracias por verificar su correo" +msgstr "" -#: templates/authopenid/changeemail.html:74 +#: templates/authopenid/changeemail.html:81 msgid "email key not sent" -msgstr "llave de correo no enviada" +msgstr "" -#: 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 "email no enviado %(email)s cambiar email aquí %(change_link)s" +msgstr "" + +#: 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 "Cuenta: cambiar la URL de OpenID" +msgstr "" #: templates/authopenid/changeopenid.html:12 msgid "" "This is where you can change your OpenID URL. Make sure you remember it!" -msgstr "Aquí es donde puedes cambiar tu OpenID URL. Asegurate de recordarla!" +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 "URL de OpenID:" +msgstr "" -#: templates/authopenid/changeopenid.html:30 -#: templates/authopenid/settings.html:34 -msgid "Change OpenID" -msgstr "Cambiar 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 "Cuenta: cambiar contraseña" +msgstr "" -#: 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 "Aquí es donde puedes cambiar tu contraseña. Asegurate de recordarlo!" - -#: templates/authopenid/changepw.html:27 -msgid "Current password" -msgstr "Contraseña actual" - -#: templates/authopenid/changepw.html:28 -msgid "New password" -msgstr "Nueva contraseña" - -#: templates/authopenid/changepw.html:29 -msgid "New password again" -msgstr "Nueva contraseña nuevamente" - -#: templates/authopenid/changepw.html:30 templates/authopenid/settings.html:29 -msgid "Change password" -msgstr "Cambiar contraseña" +msgstr "" -#: templates/authopenid/complete.html:5 +#: templates/authopenid/complete.html:19 msgid "Connect your OpenID with this site" -msgstr "Vincular tu OpenID con este sitio" +msgstr "" -#: templates/authopenid/complete.html:8 +#: templates/authopenid/complete.html:22 msgid "Connect your OpenID with your account on this site" -msgstr "Vincular tu OpenID con tu cuenta en este sitio" +msgstr "" + +#: templates/authopenid/complete.html:27 +#, python-format +msgid "register new %(provider)s account info, see %(gravatar_faq_url)s" +msgstr "" + +#: 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 "" -#: templates/authopenid/complete.html:12 +#: templates/authopenid/complete.html:35 #, python-format -msgid "register new %(provider)s account info" -msgstr "Registrar una nueva cuenta %(provider)s." +msgid "" +"register new external %(provider)s account info, see %(gravatar_faq_url)s" +msgstr "" -#: templates/authopenid/complete.html:14 +#: templates/authopenid/complete.html:40 msgid "This account already exists, please use another." -msgstr "Esta cuenta ya existe, por favor usar otra." +msgstr "" -#: templates/authopenid/complete.html:19 templates/authopenid/complete.html:32 -#: templates/authopenid/sendpw.html:16 templates/authopenid/signin.html:130 +#: templates/authopenid/complete.html:55 msgid "Sorry, looks like we have some errors:" -msgstr "Ups, parece que hay errores:" +msgstr "" -#: templates/authopenid/complete.html:47 +#: templates/authopenid/complete.html:76 msgid "Screen name label" -msgstr "Nombre de Usuario" +msgstr "" -#: templates/authopenid/complete.html:48 +#: templates/authopenid/complete.html:83 msgid "Email address label" -msgstr "Su email (correo electrónico)" +msgstr "" -#: templates/authopenid/complete.html:49 +#: templates/authopenid/complete.html:89 templates/authopenid/signup.html:15 +msgid "receive updates motivational blurb" +msgstr "" + +#: templates/authopenid/complete.html:93 +msgid "Tag filter tool will be your right panel, once you log in." +msgstr "" + +#: templates/authopenid/complete.html:95 msgid "create account" -msgstr "crear cuenta" +msgstr "" -#: templates/authopenid/complete.html:56 +#: templates/authopenid/complete.html:104 msgid "Existing account" -msgstr "Cuenta existente" +msgstr "" -#: templates/authopenid/complete.html:57 +#: templates/authopenid/complete.html:105 msgid "user name" -msgstr "nombre de usuario" +msgstr "" -#: templates/authopenid/complete.html:58 +#: templates/authopenid/complete.html:106 msgid "password" -msgstr "contraseña" +msgstr "" -#: templates/authopenid/complete.html:61 +#: templates/authopenid/complete.html:111 msgid "Register" -msgstr "Registrarse" +msgstr "" -#: templates/authopenid/complete.html:62 +#: templates/authopenid/complete.html:112 templates/authopenid/signin.html:140 msgid "Forgot your password?" -msgstr "¿Olvidaste tu contraseña?" +msgstr "" -#: templates/authopenid/delete.html:9 +#: templates/authopenid/delete.html:4 templates/authopenid/settings.html:38 +msgid "Delete account" +msgstr "" + +#: templates/authopenid/delete.html:8 msgid "Account: delete account" -msgstr "Cuenta: borrar cuenta" +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 "" -"Nota: Luego de borrar tu cuenta, cualquiera va a poder registrarse con este " -"nombre de usuario." -#: templates/authopenid/delete.html:17 +#: templates/authopenid/delete.html:16 msgid "Check confirm box, if you want delete your account." -msgstr "Marca caja de confirmación, si deseas borrar tu cuenta." +msgstr "" -#: templates/authopenid/delete.html:20 +#: templates/authopenid/delete.html:19 msgid "Password:" -msgstr "Contraseña" +msgstr "" -#: templates/authopenid/delete.html:32 +#: templates/authopenid/delete.html:31 msgid "I am sure I want to delete my account." -msgstr "Estoy seguro que quiero borrar mi cuenta." +msgstr "" -#: templates/authopenid/delete.html:33 +#: templates/authopenid/delete.html:32 msgid "Password/OpenID URL" -msgstr "Contraseña/OpenID URL" +msgstr "" -#: templates/authopenid/delete.html:33 +#: templates/authopenid/delete.html:32 msgid "(required for your security)" -msgstr "(requerido por tu seguridad)" +msgstr "" -#: templates/authopenid/delete.html:35 +#: templates/authopenid/delete.html:34 msgid "Delete account permanently" -msgstr "Borrar la cuenta de forma permanente" +msgstr "" -#: templates/authopenid/sendpw.html:4 templates/authopenid/sendpw.html.py:8 -msgid "Send new password" -msgstr "Enviar nueva contraseña" +#: 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:12 -msgid "Lost your password? No problem - here you can reset it." -msgstr "¿Haz perdido tu contraseña? No hay problema - aquí puedes re-crearla." +#: templates/authopenid/external_legacy_login_info.html:17 +msgid "how to login with password through external login website" +msgstr "" -#: templates/authopenid/sendpw.html:13 -msgid "" -"Please enter your username below and new password will be sent to your " -"registered e-mail" +#: templates/authopenid/sendpw.html:4 templates/authopenid/sendpw.html.py:7 +msgid "Send new password" msgstr "" -"Por favor, ingresa tu nombre de usuario y una nueva contraseña será enviada " -"a la dirección de email registrada." -#: templates/authopenid/sendpw.html:28 -msgid "User name" -msgstr "Nombre de usuario" +#: templates/authopenid/sendpw.html:10 +msgid "password recovery information" +msgstr "" -#: templates/authopenid/sendpw.html:30 +#: templates/authopenid/sendpw.html:21 msgid "Reset password" -msgstr "Re-crear contraseña" +msgstr "" -#: templates/authopenid/sendpw.html:30 +#: templates/authopenid/sendpw.html:22 msgid "return to login" -msgstr "volver a 'Ingresar'" +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" +#: templates/authopenid/settings.html:4 +msgid "Account functions" 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/settings.html:30 msgid "Give your account a new password." -msgstr "Crea una nueva contraseña para tu cuenta." +msgstr "" #: templates/authopenid/settings.html:31 msgid "Change email " -msgstr "Cambiar email " +msgstr "" #: templates/authopenid/settings.html:32 msgid "Add or update the email address associated with your account." -msgstr "Agrega o actualiza el email asociado a tu cuenta." +msgstr "" #: templates/authopenid/settings.html:35 msgid "Change openid associated to your account" -msgstr "Cambia el OpenID asociado a tu cuenta" - -#: templates/authopenid/settings.html:38 -msgid "Delete account" -msgstr "Eliminar cuenta" +msgstr "" #: templates/authopenid/settings.html:39 msgid "Erase your username and all your data from website" -msgstr "Eliminar tu nombre de usuario y toda tu información del sitio" +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 "Ingreso de usuario" +msgstr "" #: templates/authopenid/signin.html:28 #, python-format @@ -2550,10 +3078,6 @@ msgid "" "log in\n" " " msgstr "" -"\n" -" Tu respuesta a %(title)s %(summary)s será publicada una vez " -"que ingreses \n" -" " #: templates/authopenid/signin.html:35 #, python-format @@ -2562,150 +3086,85 @@ msgid "" " %(title)s %(summary)s will be posted once you log in\n" " " msgstr "" -"Tu pregunta \n" -" %(title)s %(summary)s será publicada una vez que ingreses\n" -" " #: templates/authopenid/signin.html:42 msgid "Click to sign in through any of these services." -msgstr "Clickea para entrar por cualquiera de estos servicios." +msgstr "" -#: templates/authopenid/signin.html:115 +#: templates/authopenid/signin.html:117 msgid "Enter your <span id=\"enter_your_what\">Provider user name</span>" -msgstr "Ingresa tu <span id=\"enter_your_what\">nombre de usuario</span>" +msgstr "" -#: templates/authopenid/signin.html:122 +#: templates/authopenid/signin.html:124 msgid "" "Enter your <a class=\"openid_logo\" href=\"http://openid.net\">OpenID</a> " "web address" msgstr "" -"Ingresa tu dirección (URL) de <a class=\"openid_logo\" href=\"http://openid." -"net\">OpenID</a>" -#: templates/authopenid/signin.html:124 templates/authopenid/signin.html:146 +#: templates/authopenid/signin.html:126 templates/authopenid/signin.html:138 msgid "Login" -msgstr "Ingresar" +msgstr "" -#: templates/authopenid/signin.html:127 +#: templates/authopenid/signin.html:129 msgid "Enter your login name and password" -msgstr "Introdusca su nombre de usuario y contraseña" +msgstr "" -#: templates/authopenid/signin.html:141 +#: templates/authopenid/signin.html:133 msgid "Login name" -msgstr "Nombre de usuario" +msgstr "" -#: templates/authopenid/signin.html:147 -msgid "Create account" -msgstr "Crear cuenta" +#: templates/authopenid/signin.html:135 +msgid "Password" +msgstr "" -#: templates/authopenid/signin.html:148 -msgid "I forgot my password" -msgstr "¿Olvidaste tu contraseña?" +#: templates/authopenid/signin.html:139 +msgid "Create account" +msgstr "" -#: templates/authopenid/signin.html:157 +#: templates/authopenid/signin.html:149 msgid "Why use OpenID?" -msgstr "¿Porqué usar OpenID?" +msgstr "" -#: templates/authopenid/signin.html:160 +#: templates/authopenid/signin.html:152 msgid "with openid it is easier" -msgstr "Con OpenID no necesitas crear un nuevo nombre de usuario y contraseña." +msgstr "" -#: templates/authopenid/signin.html:163 +#: templates/authopenid/signin.html:155 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:166 +#: templates/authopenid/signin.html:158 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:169 +#: templates/authopenid/signin.html:161 msgid "openid is supported open standard" msgstr "" -"OpenID es basado en un standard abierto, apoyado por muchas organizaciones." -#: templates/authopenid/signin.html:174 +#: templates/authopenid/signin.html:166 msgid "Find out more" -msgstr "Averigua más" +msgstr "" -#: templates/authopenid/signin.html:175 +#: templates/authopenid/signin.html:167 msgid "Get OpenID" -msgstr "Adquiere una OpenID" +msgstr "" -#: templates/authopenid/signup.html:4 templates/authopenid/signup.html.py:8 +#: templates/authopenid/signup.html:4 msgid "Signup" -msgstr "Registrate" +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 "" -"Soportamos dos formas de registro de usuario: convencional usuario/" -"contraseña, y" -#: templates/authopenid/signup.html:12 -msgid "the OpenID method" -msgstr "OpenID" +#: templates/authopenid/signup.html:10 +msgid "Traditional signup info" +msgstr "" #: templates/authopenid/signup.html:17 -msgid "Sorry, looks like we have some errors" -msgstr "Ups, parece que hay errores." - -#: templates/authopenid/signup.html:35 -msgid "Conventional registration" -msgstr "Registro clásico" - -#: templates/authopenid/signup.html:36 -msgid "choose a user name" -msgstr "elije un nombre de usuario" - -#: templates/authopenid/signup.html:42 -msgid "back to login" -msgstr "volver al ingreso de usuario" - -#: templates/authopenid/signup.html:46 -msgid "Register with your OpenID" -msgstr "Registrate con tu OpenID" - -#: templates/authopenid/signup.html:49 -msgid "Login with your OpenID" -msgstr "Ingresar con tu OpenID" - -#~ msgid "register/" -#~ msgstr "registrarse/" - -#~ msgid "we support two login modes" -#~ msgstr "soportamos dos tipos de ingreso" - -#~ msgid "Create new account" -#~ msgstr "Crear cuenta nueva" - -#, 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" - -#~ msgid "Have a total of" -#~ msgstr "Hay un total de" - -#~ msgid "/account/" -#~ msgstr "/cuenta/" +msgid "Create Account" +msgstr "" -#~ msgid "content/" -#~ msgstr "contenido/" +#: templates/authopenid/signup.html:19 +msgid "return to OpenID login" +msgstr "" diff --git a/log/cnprog.log b/middleware/__init__.py~HEAD index e69de29b..e69de29b 100644 --- a/log/cnprog.log +++ b/middleware/__init__.py~HEAD diff --git a/middleware/anon_user.py b/middleware/anon_user.py new file mode 100644 index 00000000..8422d89b --- /dev/null +++ b/middleware/anon_user.py @@ -0,0 +1,34 @@ +from django.http import HttpResponseRedirect +from django_authopenid.util import get_next_url +from django.utils.translation import ugettext as _ +from user_messages import create_message, get_and_delete_messages +import settings +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 + + #also set the first greeting one time per session only + if 'greeting_set' not in request.session: + request.session['greeting_set'] = True + msg = _('first time greeting with %(url)s') % {'url':settings.GREETING_URL} + request.user.message_set.create(message=msg) 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 86b075e4..3bce2879 100644 --- a/settings.py +++ b/settings.py @@ -1,28 +1,10 @@ # encoding:utf-8 # Django settings for lanai project. import os.path -from django.utils.translation import ugettext as _ - -#DEBUG SETTINGS -DEBUG = True -TEMPLATE_DEBUG = DEBUG -INTERNAL_IPS = ('127.0.0.1',) - -#for OpenID auth -ugettext = lambda s: s -#LOGIN_URL = '/%s%s' % (ugettext('account/'), ugettext('signin/')) -LOGIN_URL = '/%s%s' % (_('account/'), _('signin/')) -#LOGIN_URL = '/cuenta/ingresar/' - -#EMAIL AND ADMINS -ADMINS = ( - ('CNProg team', 'team@cnprog.com'), -) -MANAGERS = ADMINS - +import sys 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 = ( @@ -39,16 +21,18 @@ MIDDLEWARE_CLASSES = ( 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.middleware.transaction.TransactionMiddleware', #'django.middleware.sqlprint.SqlPrintingMiddleware', + 'middleware.anon_user.ConnectToSessionMessagesMiddleware', 'middleware.pagesize.QuestionsPageSizeMiddleware', + 'middleware.cancel.CancelActionMiddleware', #'debug_toolbar.middleware.DebugToolbarMiddleware', ) 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' @@ -74,9 +58,12 @@ INSTALLED_APPS = ( 'django.contrib.sites', 'django.contrib.admin', 'django.contrib.humanize', + 'django.contrib.sitemaps', 'forum', 'django_authopenid', + 'djangosphinx', #'debug_toolbar' , + 'user_messages', ) import django DJANGO_VERSION = django.get_version() diff --git a/settings_local.py.dist b/settings_local.py.dist index 003c7491..136d4bdd 100644 --- a/settings_local.py.dist +++ b/settings_local.py.dist @@ -1,17 +1,39 @@ # encoding:utf-8 import os.path +<<<<<<< HEAD:settings_local.py.dist +======= +from django.utils.translation import ugettext as _ + +>>>>>>> evgenyfadeev/master:settings_local.py.dist SITE_SRC_ROOT = os.path.dirname(__file__) LOG_FILENAME = 'django.lanai.log' #for logging import logging logging.basicConfig(filename=os.path.join(SITE_SRC_ROOT, 'log', LOG_FILENAME), level=logging.DEBUG,) +<<<<<<< HEAD:settings_local.py.dist DATABASE_NAME = '' # Or path to database file if using sqlite3. DATABASE_USER = '' # Not used with sqlite3. DATABASE_PASSWORD = '' # Not used with sqlite3. DATABASE_ENGINE = '' #mysql, etc <<<<<<< HEAD:settings_local.py.dist +======= + +#ADMINS and MANAGERS +ADMINS = (('Forum Admin', 'forum@example.com'),) +MANAGERS = ADMINS + +#DEBUG SETTINGS +DEBUG = False +TEMPLATE_DEBUG = DEBUG +INTERNAL_IPS = ('127.0.0.1',) + +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' #mysql, etc +>>>>>>> evgenyfadeev/master:settings_local.py.dist #Moved from settings.py for better organization. (please check it up to clean up settings.py) @@ -22,18 +44,33 @@ DATABASE_ENGINE = '' #mysql, etc >>>>>>> 6214863f362fd0702af79abaade0de6736d12e96:settings_local.py.dist #email server settings SERVER_EMAIL = '' -DEFAULT_FROM_EMAIL = 'team@cnprog.com' +DEFAULT_FROM_EMAIL = '' EMAIL_HOST_USER = '' EMAIL_HOST_PASSWORD = '' +<<<<<<< HEAD:settings_local.py.dist EMAIL_SUBJECT_PREFIX = '[cnprog.com]' EMAIL_HOST='smtp.gmail.com' EMAIL_PORT='587' EMAIL_USE_TLS=True <<<<<<< HEAD:settings_local.py.dist +======= +EMAIL_SUBJECT_PREFIX = '[CNPROG] ' +EMAIL_HOST='cnprog.com' +EMAIL_PORT='25' +EMAIL_USE_TLS=False +>>>>>>> evgenyfadeev/master:settings_local.py.dist #LOCALIZATIONS -TIME_ZONE = 'Asia/Chongqing Asia/Chungking' -# LANGUAGE_CODE = 'en-us' +TIME_ZONE = 'America/Tijuana' + +########################### +# +# this will allow running your forum with url like http://site.com/forum +# +# FORUM_SCRIPT_ALIAS = 'forum/' +# +FORUM_SCRIPT_ALIAS = '' #no leading slash, default = '' empty string + ======= @@ -43,26 +80,58 @@ TIME_ZONE = 'Asia/Chongqing Asia/Chungking' >>>>>>> 6214863f362fd0702af79abaade0de6736d12e96:settings_local.py.dist #OTHER SETTINGS +<<<<<<< HEAD:settings_local.py.dist APP_TITLE = u'CNProg.com 程序员问答社区' APP_KEYWORDS = u'技术问答社区,中国程序员,编程技术社区,程序员社区,程序员论坛,程序员wiki,程序员博客' APP_DESCRIPTION = u'中国程序员的编程技术问答社区。我们做专业的、可协作编辑的技术问答社区。' APP_INTRO = u' <p>CNProg是一个<strong>面向程序员</strong>的可协作编辑的<strong>开放源代码问答社区</strong>。</p><p> 您可以在这里提问各类<strong>程序技术问题</strong> - 问题不分语言和平台。 同时也希望您对力所能及的问题,给予您的宝贵答案。</p>' APP_COPYRIGHT = 'Copyright CNPROG.COM 2009' <<<<<<< HEAD:settings_local.py.dist +======= +APP_TITLE = u'CNPROG Q&A Forum' +APP_KEYWORDS = u'CNPROG,forum,community' +APP_DESCRIPTION = u'Ask and answer questions.' +APP_INTRO = u'<p>Ask and answer questions, make the world better!</p>' +APP_COPYRIGHT = 'Copyright CNPROG, 2009. Some rights reserved under creative commons license.' +LOGIN_URL = '/%s%s%s' % (FORUM_SCRIPT_ALIAS,'account/','signin/') +GREETING_URL = LOGIN_URL #may be url of "faq" page or "about", etc +>>>>>>> evgenyfadeev/master:settings_local.py.dist ======= >>>>>>> 6214863f362fd0702af79abaade0de6736d12e96:settings_local.py.dist 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=' <<<<<<< HEAD:settings_local.py.dist GOOGLE_ANALYTICS_KEY = '' +<<<<<<< HEAD:settings_local.py.dist BOOKS_ON = True ======= GOOGLE_ANALYTICS_KEY = '' >>>>>>> 6214863f362fd0702af79abaade0de6736d12e96:settings_local.py.dist +======= +BOOKS_ON = False +WIKI_ON = True +USE_EXTERNAL_LEGACY_LOGIN = False +EXTERNAL_LEGACY_LOGIN_HOST = 'login.cnprog.com' +EXTERNAL_LEGACY_LOGIN_PORT = 80 +EXTERNAL_LEGACY_LOGIN_PROVIDER_NAME = '<span class="orange">CNPROG</span>' +FEEDBACK_SITE_URL = None #None or url + +DJANGO_VERSION = 1.1 +RESOURCE_REVISION=4 + +USE_SPHINX_SEARCH = True #if True all SPHINX_* settings are required +#also sphinx search engine and djangosphinxs app must be installed +#sample sphinx configuration file is /sphinx/sphinx.conf +SPHINX_API_VERSION = 0x113 #refer to djangosphinx documentation +SPHINX_SEARCH_INDICES=('cnprog',) #a tuple of index names remember about a comma after the +#last item, especially if you have just one :) +SPHINX_SERVER='localhost' +SPHINX_PORT=3312 +>>>>>>> evgenyfadeev/master:settings_local.py.dist diff --git a/sphinx/sphinx.conf b/sphinx/sphinx.conf new file mode 100644 index 00000000..bf4bdc8b --- /dev/null +++ b/sphinx/sphinx.conf @@ -0,0 +1,127 @@ +#if you have many posts, it's best to configure another index for new posts and +#periodically merge the diff index to the main +#this is not important until you get to hundreds of thousands posts + +source src_cnprog +{ + # data source + type = mysql + sql_host = localhost + sql_user = cnprog #replace with your db username + sql_pass = secret #replace with your db password + sql_db = cnprog #replace with your db name + # these two are optional + #sql_port = 3306 + #sql_sock = /var/lib/mysql/mysql.sock + + # pre-query, executed before the main fetch query + sql_query_pre = SET NAMES utf8 + + # main document fetch query - change the table names if you are using a prefix + # this query creates a flat document from each question that includes only latest + # revisions of the question and all of it's answers + sql_query = SELECT q.id as id, q.title AS title, q.tagnames as tags, qr.text AS text, answers_combined.text AS answers \ + FROM question AS q \ + INNER JOIN \ + ( \ + SELECT MAX(id) as id, question_id \ + FROM question_revision \ + GROUP BY question_id \ + ) \ + AS mqr \ + ON q.id=mqr.question_id \ + INNER JOIN question_revision AS qr ON qr.id=mqr.id \ + LEFT JOIN \ + ( \ + SELECT GROUP_CONCAT(answer_current.text SEPARATOR '. ') AS text, \ + question_id \ + FROM \ + ( \ + SELECT a.question_id as question_id, ar.text as text \ + FROM answer AS a \ + INNER JOIN \ + ( \ + SELECT MAX(id) as id, answer_id \ + FROM answer_revision \ + GROUP BY answer_id \ + ) \ + AS mar \ + ON mar.answer_id = a.id \ + INNER JOIN answer_revision AS ar ON ar.id=mar.id \ + WHERE a.deleted=0 \ + ) \ + AS answer_current \ + GROUP BY question_id \ + ) \ + AS answers_combined ON q.id=answers_combined.question_id \ + WHERE q.deleted=0; + + # optional - used by command-line search utility to display document information + sql_query_info = SELECT title, id FROM question WHERE id=$id +} + +index cnprog { + # which document source to index + source = src_cnprog + + # this is path and index file name without extension + # you may need to change this path or create this folder + path = /var/data/sphinx/cnprog_main + + # docinfo (ie. per-document attribute values) storage strategy + docinfo = extern + + # morphology + morphology = stem_en + + # stopwords file + #stopwords = /var/data/sphinx/stopwords.txt + + # minimum word length + min_word_len = 1 + + # uncomment next 2 lines to allow wildcard (*) searches + #min_infix_len = 1 + #enable_star = 1 + + # charset encoding type + charset_type = utf-8 +} + +# indexer settings +indexer +{ + # memory limit (default is 32M) + mem_limit = 64M +} + +# searchd settings +searchd +{ + # IP address on which search daemon will bind and accept + # optional, default is to listen on all addresses, + # ie. address = 0.0.0.0 + address = 127.0.0.1 + + # port on which search daemon will listen + port = 3312 + + # searchd run info is logged here - create or change the folder + log = /var/log/sphinx/searchd.log + + # all the search queries are logged here + query_log = /var/log/sphinx/query.log + + # client read timeout, seconds + read_timeout = 5 + + # maximum amount of children to fork + max_children = 30 + + # a file which will contain searchd process ID + pid_file = /var/log/sphinx/searchd.pid + + # maximum amount of matches this daemon would ever retrieve + # from each index and serve to client + max_matches = 1000 +} 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/sql_scripts/091208_upgrade_evgeny.sql b/sql_scripts/091208_upgrade_evgeny.sql new file mode 100644 index 00000000..d9c4289a --- /dev/null +++ b/sql_scripts/091208_upgrade_evgeny.sql @@ -0,0 +1 @@ +ALTER TABLE `auth_user` add column hide_ignored_questions tinyint(1) not NULL; diff --git a/sql_scripts/091208_upgrade_evgeny_1.sql b/sql_scripts/091208_upgrade_evgeny_1.sql new file mode 100644 index 00000000..b1b4107f --- /dev/null +++ b/sql_scripts/091208_upgrade_evgeny_1.sql @@ -0,0 +1 @@ +ALTER TABLE `auth_user` add column `tag_filter_setting` varchar(16) not NULL default 'ignored'; diff --git a/sql_scripts/update_2009_01_25_001.sql b/sql_scripts/update_2009_01_25_001.sql index f6f3bed2..16c3487b 100644 --- a/sql_scripts/update_2009_01_25_001.sql +++ b/sql_scripts/update_2009_01_25_001.sql @@ -1,2 +1,2 @@ ALTER TABLE `award` ADD `content_type_id` INT NULL -ALTER TABLE `award` ADD `object_id` INT NULL
\ No newline at end of file +ALTER TABLE `award` ADD `object_id` INT NULL diff --git a/sql_scripts/update_2009_02_26_001.sql b/sql_scripts/update_2009_02_26_001.sql index 4113d8f5..a6af5931 100644 --- a/sql_scripts/update_2009_02_26_001.sql +++ b/sql_scripts/update_2009_02_26_001.sql @@ -16,4 +16,4 @@ WHERE tagnames like '%c#%' UPDATE question_revision SET tagnames = replace(tagnames, 'c#', 'csharp') -WHERE tagnames like '%c#%'
\ No newline at end of file +WHERE tagnames like '%c#%' diff --git a/sql_scripts/update_2009_04_10_001.sql b/sql_scripts/update_2009_04_10_001.sql index e1ceb3bc..8148632a 100644 --- a/sql_scripts/update_2009_04_10_001.sql +++ b/sql_scripts/update_2009_04_10_001.sql @@ -1,3 +1,3 @@ ALTER TABLE Tag ADD COLUMN deleted_at datetime default null; ALTER TABLE Tag ADD COLUMN deleted_by_id INTEGER NULL; -ALTER TABLE Tag ADD COLUMN deleted TINYINT NOT NULL;
\ No newline at end of file +ALTER TABLE Tag ADD COLUMN deleted TINYINT 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 @@ </u> </div> <script type="text/javascript"> - var GOOG_FIXURL_LANG = 'zh-cn'; + var GOOG_FIXURL_LANG = '{{settings.LANGUAGE_CODE}}'; var GOOG_FIXURL_SITE = '{{site_url}}'; </script> <script type="text/javascript" src="http://linkhelp.clients.google.com/tbproxy/lh/wm/fixurl.js"></script> diff --git a/templates/about.html b/templates/about.html index eaf0d591..2ca75500 100644 --- a/templates/about.html +++ b/templates/about.html @@ -12,10 +12,30 @@ </div> <div class="content"> - <p>edit file templates/about.html. Below are just suggestions of what can go here</p> - <p>what is your site for?</p> - <p>how does it work? what are roles of members?</p> - <p>is there a place to find out more about this website?</p> + <p><strong>NMR Wiki <span class="orange">Q&A</span></strong> is a collaboratively edited question + and answer site created for the <strong>Magnetic Resonance</strong> community, i.e. those people who + work in the fields of <strong>NMR</strong>, <strong>EPR</strong>, <strong>MRI</strong></strong>, etc. + NMR Wiki Q&A is affiliated with <strong><a href="http://nmrwiki.org">NMR Wiki</a></strong> - + the public wiki knowledge base about the Magnetic Resonance, which currently counts ~300 registered users. The most useful information collected here + will be further distilled on the wiki site. + </p> + <p>Here you can <strong>ask</strong> and <strong>answer</strong> questions, <strong>comment</strong> + and <strong>vote</strong> for the questions of others and their answers. Both questions and answers + <strong>can be revised</strong> and improved. Questions can be <strong>tagged</strong> with + the relevant keywords to simplify future access and organize the accumulated material. + </p> + + <p>This <span class="orange">Q&A</span> site is moderated by its members, hopefully - including yourself! + Moderation rights are gradually assigned to the site users based on the accumulated <strong>"reputation"</strong> + points. These points are added to the users account when others vote for his/her questions or answers. + These points (very) roughly reflect the level of trust of the community. + </p> + <p>No points are necessary to ask or answer the questions - so please - + <strong><a href="{% url user_signin %}">join us!</a></strong> + </p> + <p> + If you would like to find out more about this site - please see <strong><a href="{% url faq %}">frequently asked questions</a></strong>. + </p> </div> {% endblock %} <!-- end template about.html --> 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" %} <!-- template answer_edit.html --> {% load i18n %} +{% load extra_tags %} {% block title %}{% spaceless %}{% trans "Edit answer" %}{% endspaceless %}{% endblock %} {% block forejs %} - <script type='text/javascript' src='/content/js/com.cnprog.editor.js'></script> - <script type='text/javascript' src='/content/js/com.cnprog.post.js'></script> - <script type='text/javascript' src='/content/js/jquery.validate.pack.js'></script> - <script type='text/javascript' src='/content/js/wmd/showdown.js'></script> - <script type='text/javascript' src='/content/js/wmd/wmd.js'></script> - <link rel="stylesheet" type="text/css" href="/content/js/wmd/wmd.css" /> + <script type='text/javascript' src='{% href "/content/js/com.cnprog.editor.js" %}'></script> + <script type='text/javascript' src='{% href "/content/js/com.cnprog.post.js" %}'></script> + <script type='text/javascript' src='{% href "/content/js/jquery.validate.pack.js" %}'></script> + <script type='text/javascript' src='{% href "/content/js/wmd/showdown.js" %}'></script> + <script type='text/javascript' src='{% href "/content/js/wmd/wmd.js" %}'></script> + <link rel="stylesheet" type="text/css" href="{% href "/content/js/wmd/wmd.css" %}" /> <script type="text/javascript"> $().ready(function(){ diff --git a/templates/answer_edit_tips.html b/templates/answer_edit_tips.html index 33e4e242..c390da06 100644 --- a/templates/answer_edit_tips.html +++ b/templates/answer_edit_tips.html @@ -16,7 +16,9 @@ {% trans "be clear and concise" %} </li> </ul> - <a href="{% url faq %}" target="_blank" title="{% trans "see frequently asked questions" %}" style="float:right;position:relative">faq »</a> + <p class='info-box-follow-up-links'> + <a href="{% url faq %}" target="_blank" title="{% trans "see frequently asked questions" %}">faq »</a> + </p> </div> </div> @@ -46,6 +48,8 @@ {% trans "basic HTML tags are also supported" %} </li> </ul> - <a href="http://en.wikipedia.org/wiki/Markdown" target="_blank" style="float:right;position:relative">{% trans "learn more about Markdown" %} »</a> + <p class='info-box-follow-up-links'> + <a href="http://en.wikipedia.org/wiki/Markdown" target="_blank">{% trans "learn more about Markdown" %} »</a> + </p> </div> <!-- end template answer_edit_tips.html --> diff --git a/templates/ask.html b/templates/ask.html index 447ee749..2efd9864 100644 --- a/templates/ask.html +++ b/templates/ask.html @@ -1,14 +1,15 @@ {% extends "base.html" %} <!-- template ask.html --> {% load i18n %} +{% load extra_tags %} {% block title %}{% spaceless %}{% trans "Ask a question" %}{% endspaceless %}{% endblock %} {% block forejs %} - <script type='text/javascript' src='/content/js/com.cnprog.editor.js'></script> - <script type='text/javascript' src='/content/js/com.cnprog.post.js'></script> - <script type='text/javascript' src='/content/js/jquery.validate.pack.js'></script> - <script type='text/javascript' src='/content/js/wmd/showdown.js'></script> - <script type='text/javascript' src='/content/js/wmd/wmd.js'></script> - <link rel="stylesheet" type="text/css" href="/content/js/wmd/wmd.css" /> + <script type='text/javascript' src='{% href "/content/js/com.cnprog.editor.js" %}'></script> + <script type='text/javascript' src='{% href "/content/js/com.cnprog.post.js" %}'></script> + <script type='text/javascript' src='{% href "/content/js/jquery.validate.pack.js" %}'></script> + <script type='text/javascript' src='{% href "/content/js/wmd/showdown.js" %}'></script> + <script type='text/javascript' src='{% href "/content/js/wmd/wmd.js" %}'></script> + <link rel="stylesheet" type="text/css" href="{% href "/content/js/wmd/wmd.css" %}" /> <script type="text/javascript"> $().ready(function(){ //set current module button style @@ -70,7 +71,9 @@ {% ifequal settings.EMAIL_VALIDATION 'on' %} {% if not request.user.email_isvalid %} <div class="message"> - {% blocktrans with request.user.email as email %}must have valid {{email}} to post{% endblocktrans %} + {% blocktrans with request.user.email as email %}must have valid {{email}} to post, + see {{email_validation_faq_url}} + {% endblocktrans %} </div> {% endif %} {% endifequal %} @@ -93,9 +96,11 @@ <td> <span id="pre-collapse" title="{% trans "Toggle the real time Markdown editor preview" %}">{% trans "toggle preview" %}</span> </td> + {% if settings.WIKI_ON %} <td style="text-align:right;"> {{ form.wiki }} <span style="font-weight:normal;cursor:help" title="{{form.wiki.help_text}}">{{ form.wiki.label_tag }} </span> </td> + {% endif %} </tr> </table> diff --git a/templates/authopenid/changeemail.html b/templates/authopenid/changeemail.html index 09857d17..94d1881c 100644 --- a/templates/authopenid/changeemail.html +++ b/templates/authopenid/changeemail.html @@ -1,37 +1,44 @@ {% extends "base_content.html" %} {% load i18n %} +{% block title %}{% spaceless %}{% trans "Change email" %}{% endspaceless %}{% endblock %} {% block content %} <!-- changeemail.html action_type={{action_type}}--> {% ifequal action_type "change" %} <div id="main-bar" class="headNormal"> - {% trans "Change email" %} + {% if user.email %} + {% trans "Change email" %} + {% else %} + {% trans "Save your email address" %} + {% endif %} </div> <p class="message"> - {% blocktrans %}change {{email}} info{% endblocktrans %} - </p> - {% if form.errors %} - <p class="errors">{% trans "Please correct errors below:" %}<br /> - {% if form.email.errors %} - <span class="error">{{ form.email.errors|join:", " }}</span> - {% endif %} - {% if form.password.errors %} - <span class="error">{{ form.password.errors|join:", " }}</span> + {% if user.email %} + {% blocktrans %}change {{email}} info{% endblocktrans %} + {% else %} + {% blocktrans %}here is why email is required, see {{gravatar_faq_url}}{% endblocktrans %} {% endif %} </p> - {% endif %} - {% if msg %} - <p class="errors">{{ msg }}</p> + <p class="error">{{ msg }}</p> {% endif %} <div class="aligned"> <form action="." method="post" accept-charset="utf-8"> - - <div class="form-row"><label for="id_email">{% trans "Your new Email" %}</label><br/>{{ form.email }}</div> - <!--<div class="form-row"><label for="id_password">{% trans "Password" %}</label>{{ form.password }}</div>--> + {% if next %} + <input type="hidden" name="next" value="{{next}}"/> + {% endif %} + <div class="form-row-vertical"> + <label for="id_email">{% if user.email %}{% trans "Your new Email" %}{% else %}{% trans "Your Email" %}{% endif %}</label> + {% if form.email.errors %} + <p class="error">{{form.email.errors|join:", "}}</p> + {% endif %} + {{ form.email }} + </div> <div class="submit-row"> - <input class="submit" type="submit" name="change_email" value="{% trans "Change email" %}"> + <input class="submit" type="submit" name="change_email" value="{% if user.email %}{% trans "Change email" %}{% else %}{% trans "Save Email" %}{% endif %}"> + {% if user.email %} <input class="submit" type="submit" name="cancel" value="{% trans "Cancel" %}"> + {% endif %} </div> </form> @@ -42,7 +49,7 @@ {% trans "Validate email" %} </div> <p class="message"> - {% blocktrans %}validate {{email}} info{% endblocktrans %} + {% blocktrans %}validate {{email}} info or go to {{change_email_url}}{% endblocktrans %} </p> {% endifequal %} {% ifequal action_type "keep" %} @@ -50,7 +57,7 @@ {% trans "Email not changed" %} </div> <p class="message"> - {% blocktrans %}old {{email}} kept{% endblocktrans %} + {% blocktrans %}old {{email}} kept, if you like go to {{change_email_url}}{% endblocktrans %} </p> {% endifequal %} {% ifequal action_type "done_novalidate" %} diff --git a/templates/authopenid/changeopenid.html b/templates/authopenid/changeopenid.html index 9b5a196a..d01788fb 100644 --- a/templates/authopenid/changeopenid.html +++ b/templates/authopenid/changeopenid.html @@ -1,7 +1,7 @@ {% extends "base.html" %} <!-- changeopenid.html --> {% load i18n %} - +{% block title %}{% spaceless %}{% trans "Change OpenID" %}{% endspaceless %}{% endblock %} {% block content %} <div id="main-bar" class=""> <h3> diff --git a/templates/authopenid/changepw.html b/templates/authopenid/changepw.html index 0e90b172..8b059544 100644 --- a/templates/authopenid/changepw.html +++ b/templates/authopenid/changepw.html @@ -1,34 +1,17 @@ {% extends "base.html" %} <!-- changepw.html --> {% load i18n %} - -{% block head %} - -{% endblock %} - - - +{% block head %}{% endblock %} +{% block title %}{% spaceless %}{% trans "Change password" %}{% endspaceless %}{% endblock %} {% block content %} -<div id="main-bar" class=""> - <h3> - {% trans "Account: change password" %} - </h3> -</div> -<p>{% blocktrans %}This is where you can change your password. Make sure you remember it!{% endblocktrans %}</p> -{% if form.errors %} -<p class="errors">{% trans "Please correct errors below:" %}<br /> -{{ form.errors }} -</p> -{% endif %} - +<div class="headNormal">{% trans "Account: change password" %}</div> +<p class="message">{% blocktrans %}This is where you can change your password. Make sure you remember it!{% endblocktrans %}</p> <div class="aligned"> <form action="." method="post" accept-charset="utf-8"> - - <div id="form-row"><label for="id_oldpw">{% trans "Current password" %}</label>{{ form.oldpw }}</div> - <div id="form-row"><label for="id_password1">{% trans "New password" %}</label>{{ form.password1 }}</div> - <div id="form-row"><label for="id_password2">{% trans "New password again" %}</label>{{ form.password2 }}</div> - <p><input type="submit" value="{% trans "Change password" %}"></p> - + <ul id="changepw-form" class="form-horizontal-rows"> + {{form.as_ul}} + </ul> + <div class="submit-row"><input type="submit" class="submit" value="{% trans "Change password" %}" /></div> </form> </div> {% endblock %} diff --git a/templates/authopenid/complete.html b/templates/authopenid/complete.html index f29b7670..e3c12ae5 100644 --- a/templates/authopenid/complete.html +++ b/templates/authopenid/complete.html @@ -1,5 +1,19 @@ {% extends "base_content.html" %} <!-- complete.html --> +{% comment %} +views calling this template: +* django_authopenid.views.register with login_type='openid' +* django_authopenid.views.signin - with login_type='legacy' + +parameters: +* provider +* login_type openid|legacy +* username (same as screen name or username in the models, and nickname in openid sreg) +* form1 - OpenidRegisterForm +* form2 - OpenidVerifyForm not clear what this form is supposed to do, not used for legacy +* email_feeds_form forum.forms.EditUserEmailFeedsForm +* openid_username_exists +{% endcomment %} {% load i18n %} {% block head %}{% endblock %} {% block title %}{% spaceless %}{% trans "Connect your OpenID with this site" %}{% endspaceless %}{% endblock %} @@ -9,25 +23,34 @@ </div> <div id="completetxt" > <div class="message"> - {% blocktrans %}register new {{provider}} account info{% endblocktrans %} + {% ifequal login_type 'openid' %} + {% blocktrans %}register new {{provider}} account info, see {{gravatar_faq_url}}{% endblocktrans %} + {% else %} + {% ifequal login_type 'legacy' %} + {% if external_login_name_is_taken %} + {% blocktrans %}{{username}} already exists, choose another name for + {{provider}}. Email is required too, see {{gravatar_faq_url}} + {% endblocktrans %} + {% else %} + {% blocktrans %}register new external {{provider}} account info, see {{gravatar_faq_url}}{% endblocktrans %} + {% endif %} + {% endifequal %} + {% endifequal %} </div> <p style="display:none">{% trans "This account already exists, please use another." %}</p> </div> {% if form1.errors %} - <div class="errors"> - <span class="big">{% trans "Sorry, looks like we have some errors:" %}</span><br/> - <ul class="error-list"> - {% if form1.username.errors %} - <li><span class="error">{{ form1.username.errors|join:", " }}</span></li> - {% endif %} - {% if form1.email.errors %} - <li><span class="error">{{ form1.email.errors|join:", " }}</span></li> - {% endif %} + <ul class="errorlist"> + {% if form1.non_field_errors %} + {% for error in form1.non_field_errors %} + <li>{{error}}</li> + {% endfor %} + {% endif %} </ul> - </div> {% endif %} - {% if form2.errors %} + {% comment %} + {% if form2.errors %}<!--form2 is dysfunctional so commented out --> <div class="errors"> <span class="big">{% trans "Sorry, looks like we have some errors:" %}</span><br/> <ul class="error-list"> @@ -40,29 +63,58 @@ </ul> </div> {% endif %} + {% endcomment %} <div class="login"> + {% ifequal login_type 'openid' %} <form name="fregister" action="{% url user_register %}" method="POST"> - {{ form.next }} - <div class="form-row"><label for="id_username">{% trans "Screen name label" %}</label><br/>{{ form1.username }}</div> - <div class="form-row"><label for="id_email">{% trans "Email address label" %}</label><br/>{{ form1.email }}</div> + {% else %} + <form name="fregister" action="{% url user_signin %}" method="POST"> + {% endifequal %} + {{ form1.next }} + <div class="form-row-vertical"> + <label for="id_username">{% trans "Screen name label" %}</label> + {% if form1.username.errors %} + <p class="error">{{ form1.username.errors|join:", " }}</p> + {% endif %} + {{ form1.username }} + </div> + <div class="form-row-vertical margin-bottom"> + <label for="id_email">{% trans "Email address label" %}</label> + {% if form1.email.errors %} + <p class="error">{{ form1.email.errors|join:", " }}</p> + {% endif %} + {{ form1.email }} + </div> + <p class='nomargin'>{% trans "receive updates motivational blurb" %}</p> + {% include "edit_user_email_feeds_form.html" %} +<<<<<<< HEAD:templates/authopenid/complete.html +======= + <p class='nomargin'>{% trans "Tag filter tool will be your right panel, once you log in." %}</p> +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/authopenid/complete.html <div class="submit-row"><input type="submit" class="submit" name="bnewaccount" value="{% trans "create account" %}"/></div> </form> </div> + {% comment %}<!-- this form associates openID with an existing password-protected account, not yet functional --> + {% if form2 %} <div class="login" style="display:none"> <form name="fverify" action="{% url user_register %}" method="POST"> - {{ form.next }} + {{ form2.next }} <fieldset style="padding:10px"> <legend class="big">{% trans "Existing account" %}</legend> <div class="form-row"><label for="id_username">{% trans "user name" %}</label><br/>{{ form2.username }}</div> <div class="form-row"><label for="id_passwordl">{% trans "password" %}</label><br/>{{ form2.password }}</div> + <p><span class='big strong'>(Optional) receive updates by email</span> - only sent when there are any.</p> + {% include "edit_user_email_feeds_form.html" %} <!--todo double check translation from chinese 确认 = "Register" --> <div class="submit-row"> <input type="submit" class="submit" name="bverify" value="{% trans "Register" %}"/> - <a href="">{% trans "Forgot your password?" %}</a> + <a href="{% url user_sendpw %}">{% trans "Forgot your password?" %}</a> </div> </fieldset> </form> </div> + {% endif %} + {% endcomment %} {% endblock %} <!-- end complete.html --> diff --git a/templates/authopenid/confirm_email.txt b/templates/authopenid/confirm_email.txt index 29b5cd94..0b3b2505 100644 --- a/templates/authopenid/confirm_email.txt +++ b/templates/authopenid/confirm_email.txt @@ -5,8 +5,8 @@ Los detalles de su cuenta son: Nombre de usuario: {{ username }} Contraseña: {{ password }} -Favor inicie sesión aquí: -{{ site_url }}{% trans "signin/" %} +{% trans "Please sign in here:" %} +{{signup_url}} Saludos, El equipo administrador de Hasked.com diff --git a/templates/authopenid/delete.html b/templates/authopenid/delete.html index d39bc962..0f9f1c60 100644 --- a/templates/authopenid/delete.html +++ b/templates/authopenid/delete.html @@ -1,8 +1,7 @@ {% extends "base.html" %} <!-- delete.html --> {% load i18n %} - - +{% block title %}{% spaceless %}{% trans "Delete account" %}{% endspaceless %}{% endblock %} {% block content %} <div id="main-bar" class=""> <h3> diff --git a/templates/authopenid/external_legacy_login_info.html b/templates/authopenid/external_legacy_login_info.html new file mode 100644 index 00000000..dda394c7 --- /dev/null +++ b/templates/authopenid/external_legacy_login_info.html @@ -0,0 +1,21 @@ +{% extends "base_content.html" %} +<!--customize this template--> +{% load i18n %} +{% block title %}{% spaceless %}{% trans "Traditional login information" %}{% endspaceless %}{% endblock %} +{% block content %} +<div class="headNormal"> + {% trans "Traditional login information" %} +</div> +{% spaceless %} +<div class="message"> +<<<<<<< HEAD:templates/authopenid/external_legacy_login_info.html +fill in template templates/authopenid/external_legacy_login_info.html +and explain how to change password, recover password, etc. +<!--add info about your external login site here--> +======= +<!--add info about your external login site here--> +{% trans "how to login with password through external login website" %} +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/authopenid/external_legacy_login_info.html +</div> +{% endspaceless %} +{% endblock %} diff --git a/templates/authopenid/sendpw.html b/templates/authopenid/sendpw.html index 37091261..6241c811 100644 --- a/templates/authopenid/sendpw.html +++ b/templates/authopenid/sendpw.html @@ -2,35 +2,25 @@ <!-- sendpw.html --> {% load i18n %} {% block title %}{% spaceless %}{% trans "Send new password" %}{% endspaceless %}{% endblock %} - {% block content %} -<div id="main-bar" class=""> - <h3>{% trans "Send new password" %}</h3> - -</div> -<div class="paragraph"> -{% trans "Lost your password? No problem - here you can reset it." %}<br/> -{% trans "Please enter your username below and new password will be sent to your registered e-mail" %} +<div class="headNormal"> + {% trans "Send new password" %} </div> -{% if form.errors %} -<p class="errors"><span class="big">{% trans "Sorry, looks like we have some errors:" %}</span><br/> - {% if form.username.errors %} - <span class="error">{{ form.username.errors|join:", " }}</span> - {% endif %} +<p class="message"> +{% trans "password recovery information" %} </p> -{% endif %} {% if msg %} - <div class="paragraph error">{{ msg }}</div> + <p class="action-status"><span>{{msg}}</span><p> {% endif %} <div class="aligned"> <form action="." method="post" accept-charset="utf-8"> - <div id="form-row"><label for="id_username">{% trans "User name" %}:</label>{{ form.username }}</div> - - <p style="padding-top:10px"><input type="submit" value="{% trans "Reset password" %}"> <a href="{% url user_signin %}">{% trans "return to login" %}</a></p> - + <ul id="emailpw-form" class="form-horizontal-rows"> + {{form.as_ul}} + </ul> + <p style="padding-top:10px"><input type="submit" class="submit" value="{% trans "Reset password" %}" /> + <a href="{% url user_signin %}"><span class="strong">{% trans "return to login" %}</span></a></p> </form> - <span class="darkred">{% trans "Note: your new password will be activated only after you click the activation link in the email message" %}</span> </div> {% endblock %} <!-- end sendpw.html --> diff --git a/templates/authopenid/sendpw_email.txt b/templates/authopenid/sendpw_email.txt index 06653d4f..c4910d12 100644 --- a/templates/authopenid/sendpw_email.txt +++ b/templates/authopenid/sendpw_email.txt @@ -1,13 +1,9 @@ -Alguien ha solicitado restablecer su contraseña en {{ site_url }}. -Si no fué usted, ignore este correo +{% load i18n %} +{% blocktrans%}Someone has requested to reset your password on {{site_url}}. +If it were not you, it is safe to ignore this email.{% endblocktrans %} -Sus nuevo datos son: - -Nombre de usuario: {{ username }} -Contraseña: {{ password }} - -Para hacer efectivo el cambio por favor visita: -{{ site_url }}{{ url_confirm }}?key={{ confirm_key }} +{% blocktrans %}email explanation how to use new {{password}} for {{username}} +with the {{key_link}}{% endblocktrans %} Saludos, El Equipo administrador de Hasked.com diff --git a/templates/authopenid/settings.html b/templates/authopenid/settings.html index ecc16c72..66ea5953 100644 --- a/templates/authopenid/settings.html +++ b/templates/authopenid/settings.html @@ -1,7 +1,7 @@ {% extends "base_content.html" %} <!-- settings.html --> {% load i18n %} - +{% block title %}{% spaceless %}{% trans "Account functions" %}{% endspaceless %}{% endblock %} {% block head %} <style type="text/css" media="screen"> h4 {font-size:12pt;} diff --git a/templates/authopenid/signin.html b/templates/authopenid/signin.html index 60aa5e5d..1363661e 100644 --- a/templates/authopenid/signin.html +++ b/templates/authopenid/signin.html @@ -1,13 +1,13 @@ {% extends "base.html" %} <!-- signin.html --> {% load i18n %} +{% load extra_tags %} {% block title %}{% spaceless %}{% trans "User login" %}{% endspaceless %}{% endblock %} {% block forejs %} - <!--<script type="text/javascript" src="/content/js/jquery.openid.js?"></script>--> - <script type='text/javascript' src='/content/js/jquery.validate.pack.js'></script> + <script type='text/javascript' src='{% href "/content/js/jquery.validate.pack.js" %}'></script> - <link rel="stylesheet" type="text/css" media="screen" href="/content/jquery-openid/openid.css"/> - <script type="text/javascript" src="/content/jquery-openid/jquery.openid.js"></script> + <link rel="stylesheet" type="text/css" media="screen" href="{% href "/content/jquery-openid/openid.css" %}"/> + <script type="text/javascript" src="{% href "/content/jquery-openid/jquery.openid.js" %}"></script> <script type="text/javascript"> $().ready( function() { $("form.openid:eq(0)").openid(); })</script> <!--<script type="text/javascript"> $().ready(function(){ @@ -44,25 +44,27 @@ <ul class="providers"> <li class="local" title="Local login"> <div class="logo_box local_login_box"> - <img src="/content/jquery-openid/images/local-login.png" alt="icon" /> + <img src="{% href "/content/jquery-openid/images/local-login.png" %}" alt="your icon here" /> </div> <span></span> </li> <li class="direct" title="Google"> <div class="logo_box google_box"> - <img src="/content/jquery-openid/images/google.gif" alt="icon" /><span>https://www.google.com/accounts/o8/id</span> + <img src="{% href "/content/jquery-openid/images/google.gif" %}" alt="icon" /><span>https://www.google.com/accounts/o8/id</span> </div> </li> <li class="direct" title="Yahoo"> <div class="logo_box yahoo_box"> - <img src="/content/jquery-openid/images/yahoo.gif" alt="icon" /><span>http://yahoo.com/</span> + <img src="{% href "/content/jquery-openid/images/yahoo.gif" %}" alt="icon" /><span>http://yahoo.com/</span> </div> </li> <li class="username" title="AOL screen name"> <div class="logo_box aol_box"> - <img src="/content/jquery-openid/images/aol.gif" alt="icon" /><span>http://openid.aol.com/<strong>username</strong></span> + <img src="{% href "/content/jquery-openid/images/aol.gif" %}" alt="icon" /><span>http://openid.aol.com/<strong>username</strong></span> </div> </li> + </ul> + <ul class="providers"> <!--<li class="openid" title="OpenID"> <div class="logo_box openid_box"> <img src="/content/jquery-openid/images/openid.gif" alt="icon" /> @@ -70,43 +72,43 @@ <span><strong>http://{your-openid-url}</strong></span> </li>--> <li class="openid first_tiny_li" title="OpenID URL"> - <img src="/content/jquery-openid/images/openidico16.png" alt="icon" /> + <img src="{% href "/content/jquery-openid/images/openidico16.png" %}" alt="icon" /> <span>http://{your-openid-url}</span> </li> <li class="username" title="MyOpenID user name"> - <img src="/content/jquery-openid/images/myopenid-2.png" alt="icon" /> + <img src="{% href "/content/jquery-openid/images/myopenid-2.png" %}" alt="icon" /> <span>http://<strong>username</strong>.myopenid.com/</span> </li> <li class="username" title="Flickr user name"> - <img src="/content/jquery-openid/images/flickr.png" alt="icon" /> + <img src="{% href "/content/jquery-openid/images/flickr.png" %}" alt="icon" /> <span>http://flickr.com/<strong>username</strong>/</span> </li> <li class="username" title="Technorati user name"> - <img src="/content/jquery-openid/images/technorati-1.png" alt="icon" /> + <img src="{% href "/content/jquery-openid/images/technorati-1.png" %}" alt="icon" /> <span>http://technorati.com/people/technorati/<strong>username</strong>/</span> </li> <li class="username" title="Wordpress blog name"> - <img src="/content/jquery-openid/images/wordpress.png" alt="icon" /> + <img src="{% href "/content/jquery-openid/images/wordpress.png" %}" alt="icon" /> <span>http://<strong>username</strong>.wordpress.com</span> </li> <li class="username" title="Blogger blog name"> - <img src="/content/jquery-openid/images/blogger-1.png" alt="icon" /> + <img src="{% href "/content/jquery-openid/images/blogger-1.png" %}" alt="icon" /> <span>http://<strong>username</strong>.blogspot.com/</span> </li> <li class="username" title="LiveJournal blog name"> - <img src="/content/jquery-openid/images/livejournal-1.png" alt="icon" /> + <img src="{% href "/content/jquery-openid/images/livejournal-1.png" %}" alt="icon" /> <span>http://<strong>username</strong>.livejournal.com</span> </li> <li class="username" title="ClaimID user name"> - <img src="/content/jquery-openid/images/claimid-0.png" alt="icon" /> + <img src="{% href "/content/jquery-openid/images/claimid-0.png" %}" alt="icon" /> <span>http://claimid.com/<strong>username</strong></span> </li> <li class="username" title="Vidoop user name"> - <img src="/content/jquery-openid/images/vidoop.png" alt="icon" /> + <img src="{% href "/content/jquery-openid/images/vidoop.png" %}" alt="icon" /> <span>http://<strong>username</strong>.myvidoop.com/</span> </li> <li class="username" title="Verisign user name"> - <img src="/content/jquery-openid/images/verisign-2.png" alt="icon" /> + <img src="{% href "/content/jquery-openid/images/verisign-2.png" %}" alt="icon" /> <span>http://<strong>username</strong>.pip.verisignlabs.com/</span> </li> </ul> @@ -125,27 +127,17 @@ </fieldset> <fieldset id='local_login_fs'> <p>{% trans 'Enter your login name and password' %}</p> - {% if form1.errors %} - <p class="errors"> - <span class="big">{% trans "Sorry, looks like we have some errors:" %}</span><br/> - <ul class="error-list"> - {% if form1.username.errors %} - <li><span class="error">{{ form1.username.errors|join:", " }}</span></li> - {% endif %} - {% if form1.password.errors %} - <li><span class="error">{{ form1.password.errors|join:", " }}</span></li> - {% endif %} - </ul> - </p> - {% endif %} - <div><label for="username">{% trans "Login name" %}</label> - {{form1.username}}<br/> - <label for="password">{% trans "Password" %}</label> - {{form1.password}}<br/> + {% if form1.errors %} + {{form1.non_field_errors.as_ul}} + {% endif %} + <div><p class="login"><label for="id_username">{% trans "Login name" %}</label> + {{form1.username}}</p> + <p class="login"><label for="id_password">{% trans "Password" %}</label> + {{form1.password}}</p> <p id="local_login_buttons"> <input id="blogin" name="blogin" type="submit" value="{% trans "Login" %}" /> <a href="{% url user_signup %}">{% trans "Create account" %}</a><br/> - <a href="{% url user_sendpw %}">{% trans "I forgot my password" %}</a> + <a href="{% url user_sendpw %}">{% trans "Forgot your password?" %}</a> </p> </div> </fieldset> @@ -170,9 +162,9 @@ </li> </ul> - <p> - <a href="http://openid.net/what/" target="_blank" style="float:right;position:relative">{% trans "Find out more" %} »</a><br/> - <a href="http://openid.net/get/" target="_blank" style="float:right;position:relative">{% trans "Get OpenID" %} »</a> + <p class="info-box-follow-up-links"> + <a href="http://openid.net/what/" target="_blank">{% trans "Find out more" %} »</a><br/> + <a href="http://openid.net/get/" target="_blank">{% trans "Get OpenID" %} »</a> </p> </div> {% 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" %} <!--signup.html--> {% load i18n %} {% block title %}{% spaceless %}{% trans "Signup" %}{% endspaceless %}{% endblock %} {% block content %} -<div id="main-bar" class=""> - <h3 >{% trans "Signup" %}</h3> - -</div> -<div class="jointxt"> - <p>{% trans "We support two types of user registration: conventional username/password, and" %} <a href="{% url user_signin %}">{% trans "the OpenID method" %}</a>.</p> - - {% if form.errors %} - - <p class="errors"> - <span class="big">{% trans "Sorry, looks like we have some errors" %}</span><br/> - <ul class="error-list"> - {% if form.username.errors %} - <li><span class="error">{{ form.username.errors|join:", " }}</span></li> - {% endif %} - {% if form.email.errors %} - <li><span class="error">{{ form.email.errors|join:", " }}</span></li> - {% endif %} - {% if form.password2.errors %} - <li><span class="error">{{ form.password2.errors|join:", " }} - </span></li> - {% endif %} - </ul> - </p> - {% endif %} +<div class="headNormal"> + {% trans "Create login name and password" %} </div> - <form action="{% url user_signup %}" method="post" accept-charset="utf-8"> - <fieldset class="fieldset"> - <legend class="big">{% trans "Conventional registration" %}</legend> - <div class="form-row"><label for="id_username">{% trans "choose a user name" %}:</label><br/>{{ form.username }}</div> - - <div class="form-row"><label for="id_email">{% trans "your email address" %}:</label><br/>{{ form.email }}</div> - <div class="form-row"><label for="id_password1">{% trans "choose password" %}:</label><br />{{ form.password1 }}</div> - <div class="form-row"><label for="id_password2">{% trans "retype password" %}:</label><br/>{{ form.password2 }}</div> - <div class="submit-row"><input type="submit" class="submit" value="{% trans "login" %}" > - <a href="{% url user_signin %}">{% trans "back to login" %}</a></div> - </fieldset> - </form> - <div style="display:none"> - <h2 class="signup">{% trans "Register with your OpenID" %}</h2> - <form name="fopenid" action="{% url user_signin %}" method="post"> - <div class="form-row">{{ form2.openid_url }}</div> - <div class="submit-row "><input name="bsignin" class="submit" type="submit" value="{% trans "Login with your OpenID" %}"></div> - </form> - </div> +<p class="message">{% trans "Traditional signup info" %}</p> +<form action="{% url user_signup %}" method="post" accept-charset="utf-8"> + <ul class="form-horizontal-rows"> + {{form.as_ul}} + </ul> + <p style="margin:10px 0px 0px 3px;">{% trans "receive updates motivational blurb" %}</p> + {% include "edit_user_email_feeds_form.html" %} + <div class="submit-row"><input type="submit" class="submit" value="{% trans "Create Account" %}" /> + <strong>{% trans "or" %} + <a href="{% url user_signin %}">{% trans "return to OpenID login" %}</a></strong></div> +</form> {% endblock %} <!--end signup.html--> 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 @@ <div class="badges" id="main-body" style="width:100%"> <p> {% trans "Community gives you awards for your questions, answers and votes." %}<br/> - {% 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 %} </p> <div id="medalList"> {% for badge in badges %} @@ -32,7 +34,7 @@ {% endfor %} </div> <div style="float:left;width:180px;"> - <a href="{{badge.get_absolute_url}}" title="{{ badge.get_type_display }} : {{ badge.description }}" class="medal"><span class="badge{{ badge.type }}">●</span> {{ badge.name }}</a><strong> ✕ {{ badge.awarded_count|intcomma }}</strong> + <a href="{{badge.get_absolute_url}}" title="{{ badge.get_type_display }} : {{ badge.description }}" class="medal"><span class="badge{{ badge.type }}">●</span> {{ badge.name }}</a><strong> × {{ badge.awarded_count|intcomma }}</strong> </div> <p style="float:left;width:350px;"> {{ badge.description }} diff --git a/templates/base.html b/templates/base.html index 0f568f73..b4751be1 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,6 +1,7 @@ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <!-- template base.html --> {% load extra_filters %} +{% load extra_tags %} {% load i18n %} <html xmlns="http://www.w3.org/1999/xhtml"> <head> @@ -12,16 +13,21 @@ {% if settings.GOOGLE_SITEMAP_CODE %} <meta name="verify-v1" content="{{settings.GOOGLE_SITEMAP_CODE}}" /> {% endif %} - <link rel="shortcut icon" href="/content/images/favicon.ico" /> - <link href="/content/style/style.css" rel="stylesheet" type="text/css" /> + <link rel="shortcut icon" href="{% href "/content/images/favicon.ico" %}" /> + <link href="{% href "/content/style/style.css" %}" rel="stylesheet" type="text/css" /> <script src="http://www.google.com/jsapi" type="text/javascript"></script> <script type="text/javascript">google.load("jquery", "1.2.6");</script> <script type="text/javascript"> +<<<<<<< HEAD:templates/base.html var i18nLang = '{{settings.LANGUAGE_CODE}}'; +======= + var i18nLang = '{{settings.LANGUAGE_CODE}}'; + var scriptUrl = '/{{settings.FORUM_SCRIPT_ALIAS}}' +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/base.html </script> - <script type='text/javascript' src='/content/js/com.cnprog.i18n.js'></script> - <script type='text/javascript' src='/content/js/jquery.i18n.js'></script> - <script type='text/javascript' src='/content/js/com.cnprog.utils.js'></script> + <script type='text/javascript' src='{% href "/content/js/com.cnprog.i18n.js" %}'></script> + <script type='text/javascript' src='{% href "/content/js/jquery.i18n.js" %}'></script> + <script type='text/javascript' src='{% href "/content/js/com.cnprog.utils.js" %}'></script> <!--<script type="text/javascript"> var uservoiceJsHost = ("https:" == document.location.protocol) ? "https://uservoice.com" : "http://cdn.uservoice.com"; document.write(unescape("%3Cscript src='" + uservoiceJsHost + "/javascripts/widgets/tab.js' type='text/javascript'%3E%3C/script%3E")) @@ -39,7 +45,7 @@ }) </script>--> <!-- todo move this to settings --> - {% if messages %} + {% if user_messages %} <style type="text/css"> body { margin-top:2.4em; } </style> @@ -57,8 +63,8 @@ <body> <div class="notify" style="display:none"> {% autoescape off %} - {% if messages %} - {% for message in messages %} + {% if user_messages %} + {% for message in user_messages %} <p class="darkred">{{ message }}<p> {% endfor %} {% endif %} @@ -73,7 +79,6 @@ {% endblock%} </div> - <div id="CARight"> {% block sidebar%} {% endblock%} diff --git a/templates/base_content.html b/templates/base_content.html index a05198f5..12297215 100644 --- a/templates/base_content.html +++ b/templates/base_content.html @@ -1,24 +1,36 @@ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <!-- base_content.html --> {% load i18n %} +{% load extra_tags %} <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>{% block title %}{% endblock %} - {{ settings.APP_TITLE }}</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> +<<<<<<< HEAD:templates/base_content.html <meta name="verify-v1" content="{{ settings.GOOGLE_SITEMAP_CODE }}" /> - <link rel="shortcut icon" href="/content/images/favicon.ico" /> - <link href="/content/style/style.css" rel="stylesheet" type="text/css" /> +======= + {% if settings.GOOGLE_SITEMAP_CODE %} + <meta name="verify-v1" content="{{ settings.GOOGLE_SITEMAP_CODE }}" /> + {% endif %} +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/base_content.html + <link rel="shortcut icon" href="{% href "/content/images/favicon.ico" %}" /> + <link href="{% href "/content/style/style.css" %}" rel="stylesheet" type="text/css" /> {% spaceless %} {% block forestyle %}{% endblock %} {% endspaceless %} <script src="http://www.google.com/jsapi" type="text/javascript"></script> <script type="text/javascript">google.load("jquery", "1.2.6");</script> <script type="text/javascript"> +<<<<<<< HEAD:templates/base_content.html var i18nLang = '{{ settings.LANGUAGE_CODE }}'; +======= + var i18nLang = '{{ settings.LANGUAGE_CODE }}'; + var scriptUrl = '/{{settings.FORUM_SCRIPT_ALIAS}}' +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/base_content.html </script> - <script type='text/javascript' src='/content/js/com.cnprog.i18n.js'></script> - <script type='text/javascript' src='/content/js/jquery.i18n.js'></script> - <script type='text/javascript' src='/content/js/com.cnprog.utils.js'></script> + <script type='text/javascript' src='{% href "/content/js/com.cnprog.i18n.js" %}'></script> + <script type='text/javascript' src='{% href "/content/js/jquery.i18n.js" %}'></script> + <script type='text/javascript' src='{% href "/content/js/com.cnprog.utils.js" %}'></script> <!-- <script type="text/javascript"> var uservoiceJsHost = ("https:" == document.location.protocol) ? "https://uservoice.com" : "http://cdn.uservoice.com"; @@ -39,7 +51,7 @@ </script>--> <!-- todo move this to settings--> - {% if messages %} + {% if user_messages %} <style type="text/css"> body { margin-top:2.4em; } </style> @@ -58,8 +70,8 @@ <body> <div class="notify" style="display:none"> {% autoescape off %} - {% if messages %} - {% for message in messages %} + {% if user_messages %} + {% for message in user_messages %} <p class="darkred">{{ message }}<p> {% endfor %} {% endif %} diff --git a/templates/book.html b/templates/book.html index cc6fc77b..e83268e4 100644 --- a/templates/book.html +++ b/templates/book.html @@ -85,12 +85,12 @@ {% if question.favourite_count %} {% if question.favorited_myself %} <div class="favorites-count"> - <img title="{% trans "this question was selected as favorite" %} {{question.favourite_count}} {% trans "number of times" %}" src="/content/images/vote-favorite-on.png"> + <img title="{% trans "this question was selected as favorite" %} {{question.favourite_count}} {% trans "number of times" %}" src="{% href "/content/images/vote-favorite-on.png" %}"> <div><b>{{question.favourite_count|intcomma}}</b></div> </div> {% else %} <div class="favorites-count-off"> - <img title="{% trans "this question was selected as favorite" %} {{question.favourite_count}} {% trans "number of times" %}" src="/content/images/vote-favorite-off.png"> + <img title="{% trans "this question was selected as favorite" %} {{question.favourite_count}} {% trans "number of times" %}" src="{% href "/content/images/vote-favorite-off.png" %}"> <div><b>{{question.favourite_count|intcomma}}</b></div> </div> {% endif %} @@ -144,7 +144,7 @@ </div> <div class="bookFeed"> <div id="feeds"> - <a href="/feeds/rss" title="{% trans "subscribe to book RSS feed" %}">{% trans "subscribe to the questions feed" %}</a> + <a href="{% href "/feeds/rss" %} " title="{% trans "subscribe to book RSS feed" %}">{% trans "subscribe to the questions feed" %}</a> </div> </div> diff --git a/templates/content/images/blue-up-arrow-h18px.png b/templates/content/images/blue-up-arrow-h18px.png Binary files differnew file mode 100644 index 00000000..e1f29e86 --- /dev/null +++ b/templates/content/images/blue-up-arrow-h18px.png diff --git a/templates/content/images/close-small-dark.png b/templates/content/images/close-small-dark.png Binary files differnew file mode 100644 index 00000000..280c1fc7 --- /dev/null +++ b/templates/content/images/close-small-dark.png diff --git a/templates/content/images/gray-up-arrow-h18px.png b/templates/content/images/gray-up-arrow-h18px.png Binary files differnew file mode 100644 index 00000000..78767445 --- /dev/null +++ b/templates/content/images/gray-up-arrow-h18px.png diff --git a/templates/content/images/cnprog_logo_200_56.gif b/templates/content/images/logo.gif Binary files differindex ab690de2..ab690de2 100644 --- a/templates/content/images/cnprog_logo_200_56.gif +++ b/templates/content/images/logo.gif diff --git a/templates/content/images/logo.png b/templates/content/images/logo.png Binary files differindex 640eb1da..b53732e3 100644 --- a/templates/content/images/logo.png +++ b/templates/content/images/logo.png diff --git a/templates/content/jquery-openid/jquery.openid.js b/templates/content/jquery-openid/jquery.openid.js index 6486fd39..763af2c6 100644 --- a/templates/content/jquery-openid/jquery.openid.js +++ b/templates/content/jquery-openid/jquery.openid.js @@ -36,7 +36,7 @@ $.fn.openid = function() { }; var local = function() { var $li = $(this); - $li.parent().find('li').removeClass('highlight'); + $('#openid_form .providers li').removeClass('highlight'); $li.addClass('highlight'); $usrfs.hide(); $idfs.hide(); @@ -47,7 +47,7 @@ $.fn.openid = function() { var direct = function() { var $li = $(this); - $li.parent().find('li').removeClass('highlight'); + $('#openid_form .providers li').removeClass('highlight'); $li.addClass('highlight'); $usrfs.fadeOut('slow'); $localfs.fadeOut('slow'); @@ -59,7 +59,7 @@ $.fn.openid = function() { var openid = function() { var $li = $(this); - $li.parent().find('li').removeClass('highlight'); + $('#openid_form .providers li').removeClass('highlight'); $li.addClass('highlight'); $usrfs.hide(); $localfs.hide(); @@ -71,7 +71,7 @@ $.fn.openid = function() { var username = function() { var $li = $(this); - $li.parent().find('li').removeClass('highlight'); + $('#openid_form .providers li').removeClass('highlight'); $li.addClass('highlight'); $idfs.hide(); $localfs.hide(); diff --git a/templates/content/jquery-openid/openid.css b/templates/content/jquery-openid/openid.css index 2ec8adf4..88960b56 100644 --- a/templates/content/jquery-openid/openid.css +++ b/templates/content/jquery-openid/openid.css @@ -1,28 +1,35 @@ -fieldset {border-style:none;} +fieldset { border-style:none; } img {border-style:none;} -.logo_box {display:inline-block;width:90px;height:40px;background:white;border:1px solid #dddddd;} +.logo_box {display:inline-block;float:left;width:90px;height:40px;background:white;border:1px solid #dddddd;} .openid_box img {margin-top:6px;} .aol_box img {margin-top:6px;} .yahoo_box img {margin-top:13px;} .google_box img {margin-top:6px;} -.local_login_box img {margin-top:9px;} +.local_login_box img {margin-top:2px;margin-left:-3px;} form.openid ul{ margin:0;padding:0;text-align:center; list-style-type:none; display:block;} -form.openid ul li {float:left; padding:4px;} +form.openid ul li {float:left; padding:4px;display:inline-block;} +form.openid ul li div {display:inline-block;} form.openid ul li span {padding:0 1em 0 3px} form.openid ul li.first_tiny_li {clear:left;} form.openid fieldset {clear:both;padding:10px 0px 0px 0px;} form.openid div+fieldset {display:none} -form.openid label {display:block; font-weight:bold; margin-bottom:.5em} +form.openid label {display:block; font-weight:bold;} input[name=openid_username] {width:8em} input[name=openid_identifier] {width:18em} form.openid ul li.highlight { -moz-border-radius:4px; -webkit-border-radius:4px; background-color: #FD6} -form.openid fieldset div {-moz-border-radius:4px; -webkit-border-radius:4px; - background: #DCDCDC; - padding:10px;display:inline-block} +form.openid fieldset div { + -moz-border-radius:4px; + -webkit-border-radius:4px; + background: #DCDCDC; + padding:10px; + display:inline-block; + float:left; +} form.openid p {margin-bottom:4px;} form.openid fieldset div p {padding:0px;margin:0px;} +form.openid fieldset div p.login {padding:0px;margin:0 0 10px 0;} form.openid label { display:inline-block; font-weight:normal; @@ -47,3 +54,16 @@ background: url(images/openidico.png) no-repeat; #openid_login {float:left; width:30%; margin:2em 1em; text-align:center} #openid_login div{margin-top:0.5em} + +form.openid ul.errorlist { + border: none; + list-style-position:inside; + list-style-type: disc; + margin-bottom:5px; +} +form.openid ul.errorlist li { + text-align: left; + margin: 5px; + float: none; + color:blue; +} diff --git a/templates/content/js/com.cnprog.admin.js b/templates/content/js/com.cnprog.admin.js new file mode 100644 index 00000000..7e91af79 --- /dev/null +++ b/templates/content/js/com.cnprog.admin.js @@ -0,0 +1,17 @@ +$().ready( function(){ + var options = { + success: function(a,b){$('.admin #action_status').html($.i18n._('changes saved'));}, + dataType:'json', + timeout:5000, +<<<<<<< HEAD:templates/content/js/com.cnprog.admin.js + url: $.i18n._('/') + $.i18n._('moderate-user/') + viewUserID + '/' +======= + url: scriptUrl + $.i18n._('moderate-user/') + viewUserID + '/' +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/content/js/com.cnprog.admin.js + }; + var form = $('.admin #moderate_user_form').ajaxForm(options); + var box = $('.admin input#id_is_approved').click(function(){ + $('.admin #action_status').html($.i18n._('sending data...')); + form.ajaxSubmit(options); + }); +}); diff --git a/templates/content/js/com.cnprog.i18n.js b/templates/content/js/com.cnprog.i18n.js index 6ba8b59d..018927aa 100644 --- a/templates/content/js/com.cnprog.i18n.js +++ b/templates/content/js/com.cnprog.i18n.js @@ -20,7 +20,7 @@ var i18nZh = { 'post recovered':"操作成功!该帖子已被恢复。", 'post deleted':"操作成功!该帖子已删除。", 'add comment':'添加评论', - 'community reputation points':'社区积分', + 'community karma points':'社区积分', 'to comment, need':'评论需要', 'delete this comment':'删除此评论', 'hide comments':"隐藏评论", @@ -53,27 +53,32 @@ var i18nZh = { 'redo':'重做', 'enter image url':'<b>输入图片地址</b></p><p>示例:<br />http://www.example.com/image.jpg \"我的截图\"', 'enter url':'<b>输入Web地址</b></p><p>示例:<br />http://www.cnprog.com/ \"我的网站\"</p>"', - 'upload image':'或者上传本地图片:', + 'upload image':'或者上传本地图片:' }; var i18nEn = { - '>15 points requried to upvote':'>15 points requried to upvote ', +<<<<<<< HEAD:templates/content/js/com.cnprog.i18n.js + '/':'/', +======= +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/content/js/com.cnprog.i18n.js + 'need >15 points to report spam':'need >15 points to report spam ', + '>15 points requried to upvote':'>15 points required to upvote ', 'tags cannot be empty':'please enter at least one tag', - 'anonymous users cannot vote':'anonymous users cannot vote ', - 'anonymous users cannot select favorite questions':'anonymous users cannot select favorite questions ', - 'to comment, need': 'to comment, need reputation ', + 'anonymous users cannot vote':'sorry, anonymous users cannot vote ', + 'anonymous users cannot select favorite questions':'sorry, anonymous users cannot select favorite questions ', + 'to comment, need': '(to comment other people\'s posts, karma ', 'please see':'please see ', - 'community reputation points':' ', + 'community karma points':' or more is necessary) - ', 'upload image':'Upload image:', 'enter image url':'enter URL of the image, e.g. http://www.example.com/image.jpg \"image title\"', 'enter url':'enter Web address, e.g. http://www.example.com \"page title\"', 'daily vote cap exhausted':'sorry, you\'ve used up todays vote cap', - 'cannot pick own answer as best':'cannot accept own answer', - 'cannot revoke old vote':'older votes cannot be revoked', + 'cannot pick own answer as best':'sorry, you cannot accept your own answer', + 'cannot revoke old vote':'sorry, older votes cannot be revoked', 'please confirm offensive':'are you sure this post is offensive, contains spam, advertising, malicious remarks, etc.?', - 'flag offensive cap exhausted':'sorry, you\'ve used up todays cap of flagging offensive messages', + 'flag offensive cap exhausted':'sorry, you\'ve used up todays cap of flagging offensive messages ', 'confirm delete':'are you sure you want to delete this?', - 'anonymous users cannot delete/undelete':'anonymous users cannot delete or undelete posts', + 'anonymous users cannot delete/undelete':'sorry, anonymous users cannot delete or undelete posts', 'post recovered':'your post is now restored!', 'post deleted':'your post has been deleted', 'confirm delete comment':'do you really want to delete this comment?', @@ -82,6 +87,9 @@ var i18nEn = { 'content minchars': 'please enter more than {0} characters', 'title minchars':"please enter at least {0} characters", 'characters':'characters left', + 'cannot vote for own posts':'sorry, you cannot vote for your own posts', + 'cannot flag message as offensive twice':'cannot flag message as offensive twice ', + '>100 points required to downvote':'>100 points required to downvote ' }; var i18nEs = { @@ -106,7 +114,7 @@ var i18nEs = { 'post recovered':"publicación recuperada", 'post deleted':"publicación borrada。", 'add comment':'agregar comentario', - 'community reputation points':'reputación comunitaria', + 'community karma points':'reputación comunitaria', 'to comment, need':'para comentar, necesita reputación', 'delete this comment':'borrar este comentario', 'hide comments':"ocultar comentarios", @@ -141,7 +149,7 @@ var i18nEs = { 'enter url':'introduzca direcciones web, ejemplo:<br />http://www.cnprog.com/ \"titulo del enlace\"</p>"', 'upload image':'cargar imagen:', 'questions/' : 'preguntas/', - 'vote/' : 'votar/', + 'vote/' : 'votar/' }; var i18n = { diff --git a/templates/content/js/com.cnprog.post.js b/templates/content/js/com.cnprog.post.js index aa6c51b6..a884b571 100644 --- a/templates/content/js/com.cnprog.post.js +++ b/templates/content/js/com.cnprog.post.js @@ -52,31 +52,33 @@ var Vote = function(){ var acceptAnonymousMessage = $.i18n._('insufficient privilege'); var acceptOwnAnswerMessage = $.i18n._('cannot pick own answer as best'); - var favoriteAnonymousMessage = $.i18n._('anonymous users cannot select favorite questions') - + "<a href='/account/signin/?next=/questions/{{QuestionID}}'>" + +<<<<<<< HEAD:templates/content/js/com.cnprog.post.js + var pleaseLogin = "<a href='" + $.i18n._("/") + $.i18n._("account/") + $.i18n._("signin/") + + "?next=" + $.i18n._("/") + $.i18n._("questions/") + "{{QuestionID}}'>" + $.i18n._('please login') + "</a>"; - var voteAnonymousMessage = $.i18n._('anonymous users cannot vote') - + "<a href='/account/signin/?next=/questions/{{QuestionID}}'>" + + var pleaseSeeFAQ = $.i18n._('please see') + "<a href='" + $.i18n._("/") + $.i18n._("faq/") + "'>faq</a>"; +======= + var pleaseLogin = "<a href='" + scriptUrl + $.i18n._("account/") + $.i18n._("signin/") + + "?next=" + scriptUrl + $.i18n._("questions/") + "{{QuestionID}}'>" + $.i18n._('please login') + "</a>"; - var upVoteRequiredScoreMessage = $.i18n._('>15 points requried to upvote') - + $.i18n._('please see') + "<a href='/faq'>faq</a>"; - var downVoteRequiredScoreMessage = $.i18n._('>100 points requried to downvote') - + $.i18n._('please see') + "<a href='/faq'>faq</a>"; + + var pleaseSeeFAQ = $.i18n._('please see') + "<a href='" + scriptUrl + $.i18n._("faq/") + "'>faq</a>"; +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/content/js/com.cnprog.post.js + + var favoriteAnonymousMessage = $.i18n._('anonymous users cannot select favorite questions') + var voteAnonymousMessage = $.i18n._('anonymous users cannot vote') + pleaseLogin; + var upVoteRequiredScoreMessage = $.i18n._('>15 points requried to upvote') + pleaseSeeFAQ; + var downVoteRequiredScoreMessage = $.i18n._('>100 points required to downvote') + pleaseSeeFAQ; var voteOwnDeniedMessage = $.i18n._('cannot vote for own posts'); - var voteRequiredMoreVotes = $.i18n._('daily vote cap exhausted') - + $.i18n._('please see') + "<a href='/faq'>faq</a>"; - var voteDenyCancelMessage = $.i18n._('cannot revoke old vote') - + $.i18n._('please see') + "<a href='/faq'>faq</a>"; + var voteRequiredMoreVotes = $.i18n._('daily vote cap exhausted') + pleaseSeeFAQ; + var voteDenyCancelMessage = $.i18n._('cannot revoke old vote') + pleaseSeeFAQ; var offensiveConfirmation = $.i18n._('please confirm offensive'); - var offensiveAnonymousMessage = $.i18n._('anonymous users cannot flag offensive posts') - + "<a href='/account/signin/?next=/questions/{{QuestionID}}'>" - + $.i18n._('please login') + "</a>"; - var offensiveTwiceMessage = $.i18n._('cannot flag message as offensive twice') - + $.i18n._('please see') + "<a href='/faq'>faq</a>"; - var offensiveNoFlagsLeftMessage = $.i18n._('flag offensive cap exhausted') - + $.i18n._('please see') + "<a href='/faq'>faq</a>"; - var offensiveNoPermissionMessage = $.i18n._('need >15 points to report spam') - + $.i18n._('please see') + "<a href='/faq'>faq</a>"; + var offensiveAnonymousMessage = $.i18n._('anonymous users cannot flag offensive posts') + pleaseLogin; + var offensiveTwiceMessage = $.i18n._('cannot flag message as offensive twice') + pleaseSeeFAQ; + var offensiveNoFlagsLeftMessage = $.i18n._('flag offensive cap exhausted') + pleaseSeeFAQ; + var offensiveNoPermissionMessage = $.i18n._('need >15 points to report spam') + pleaseSeeFAQ; var removeConfirmation = $.i18n._('confirm delete'); var removeAnonymousMessage = $.i18n._('anonymous users cannot delete/undelete'); var recoveredMessage = $.i18n._('post recovered'); @@ -94,7 +96,7 @@ var Vote = function(){ removeQuestion: 9, removeAnswer:10, questionSubscribeUpdates:11, - questionUnsubscribeUpdates:12, + questionUnsubscribeUpdates:12 }; var getFavoriteButton = function(){ @@ -131,7 +133,7 @@ var Vote = function(){ }; var getOffensiveQuestionFlag = function(){ - var offensiveQuestionFlag = 'table[id=question-table] span[class='+ offensiveClassFlag +']'; + var offensiveQuestionFlag = '#question-table span[class='+ offensiveClassFlag +']'; return $(offensiveQuestionFlag); }; @@ -157,17 +159,30 @@ var Vote = function(){ var setVoteImage = function(voteType, undo, object){ var flag = undo ? "" : "-on"; var arrow = (voteType == VoteType.questionUpVote || voteType == VoteType.answerUpVote) ? "up" : "down"; - object.attr("src", "/content/images/vote-arrow-"+ arrow + flag +".png"); +<<<<<<< HEAD:templates/content/js/com.cnprog.post.js + object.attr("src", $.i18n._("/") + "content/images/vote-arrow-"+ arrow + flag +".png"); +======= + object.attr("src", scriptUrl + "content/images/vote-arrow-"+ arrow + flag +".png"); +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/content/js/com.cnprog.post.js // if undo voting, then undo the pair of arrows. if(undo){ if(voteType == VoteType.questionUpVote || voteType == VoteType.questionDownVote){ - $(getQuestionVoteUpButton()).attr("src", "/content/images/vote-arrow-up.png"); - $(getQuestionVoteDownButton()).attr("src", "/content/images/vote-arrow-down.png"); +<<<<<<< HEAD:templates/content/js/com.cnprog.post.js + $(getQuestionVoteUpButton()).attr("src", $.i18n._("/") + "content/images/vote-arrow-up.png"); + $(getQuestionVoteDownButton()).attr("src", $.i18n._("/") + "content/images/vote-arrow-down.png"); + } + else{ + $(getAnswerVoteUpButton(postId)).attr("src", $.i18n._("/") + "content/images/vote-arrow-up.png"); + $(getAnswerVoteDownButton(postId)).attr("src", $.i18n._("/") + "content/images/vote-arrow-down.png"); +======= + $(getQuestionVoteUpButton()).attr("src", scriptUrl + "content/images/vote-arrow-up.png"); + $(getQuestionVoteDownButton()).attr("src", scriptUrl + "content/images/vote-arrow-down.png"); } else{ - $(getAnswerVoteUpButton(postId)).attr("src", "/content/images/vote-arrow-up.png"); - $(getAnswerVoteDownButton(postId)).attr("src", "/content/images/vote-arrow-down.png"); + $(getAnswerVoteUpButton(postId)).attr("src", scriptUrl + "content/images/vote-arrow-up.png"); + $(getAnswerVoteDownButton(postId)).attr("src", scriptUrl + "content/images/vote-arrow-down.png"); +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/content/js/com.cnprog.post.js } } }; @@ -182,42 +197,42 @@ var Vote = function(){ if(questionAuthorId == currentUserId){ var acceptedButtons = 'div.'+ voteContainerId +' img[id^='+ imgIdPrefixAccept +']'; $(acceptedButtons).unbind('click').click(function(event){ - Vote.accept($(event.target)) + Vote.accept($(event.target)); }); } // set favorite question var favoriteButton = getFavoriteButton(); favoriteButton.unbind('click').click(function(event){ - Vote.favorite($(event.target)) + Vote.favorite($(event.target)); }); // question vote up var questionVoteUpButton = getQuestionVoteUpButton(); questionVoteUpButton.unbind('click').click(function(event){ - Vote.vote($(event.target), VoteType.questionUpVote) + Vote.vote($(event.target), VoteType.questionUpVote); }); var questionVoteDownButton = getQuestionVoteDownButton(); questionVoteDownButton.unbind('click').click(function(event){ - Vote.vote($(event.target), VoteType.questionDownVote) + Vote.vote($(event.target), VoteType.questionDownVote); }); var answerVoteUpButton = getAnswerVoteUpButtons(); answerVoteUpButton.unbind('click').click(function(event){ - Vote.vote($(event.target), VoteType.answerUpVote) + Vote.vote($(event.target), VoteType.answerUpVote); }); var answerVoteDownButton = getAnswerVoteDownButtons(); answerVoteDownButton.unbind('click').click(function(event){ - Vote.vote($(event.target), VoteType.answerDownVote) + Vote.vote($(event.target), VoteType.answerDownVote); }); getOffensiveQuestionFlag().unbind('click').click(function(event){ - Vote.offensive(this, VoteType.offensiveQuestion) + Vote.offensive(this, VoteType.offensiveQuestion); }); getOffensiveAnswerFlags().unbind('click').click(function(event){ - Vote.offensive(this, VoteType.offensiveAnswer) + Vote.offensive(this, VoteType.offensiveAnswer); }); getremoveQuestionLink().unbind('click').click(function(event){ @@ -234,7 +249,7 @@ var Vote = function(){ }); getremoveAnswersLinks().unbind('click').click(function(event){ - Vote.remove(this, VoteType.removeAnswer) + Vote.remove(this, VoteType.removeAnswer); }); }; @@ -243,14 +258,18 @@ var Vote = function(){ type: "POST", cache: false, dataType: "json", - url: "/" + $.i18n._("questions/") + questionId + "/" + $.i18n._("vote/"), +<<<<<<< HEAD:templates/content/js/com.cnprog.post.js + url: $.i18n._("/") + $.i18n._("questions/") + questionId + "/" + $.i18n._("vote/"), +======= + url: scriptUrl + $.i18n._("questions/") + questionId + "/" + $.i18n._("vote/"), +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/content/js/com.cnprog.post.js data: { "type": voteType, "postId": postId }, error: handleFail, success: function(data){callback(object, voteType, data)}}); }; var handleFail = function(xhr, msg){ - alert("Callback invoke error: " + msg) + alert("Callback invoke error: " + msg); }; // callback function for Accept Answer action @@ -262,19 +281,31 @@ var Vote = function(){ showMessage(object, acceptOwnAnswerMessage); } else if(data.status == "1"){ - object.attr("src", "/content/images/vote-accepted.png"); +<<<<<<< HEAD:templates/content/js/com.cnprog.post.js + object.attr("src", $.i18n._("/") + "content/images/vote-accepted.png"); +======= + object.attr("src", scriptUrl + "content/images/vote-accepted.png"); +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/content/js/com.cnprog.post.js $("#"+answerContainerIdPrefix+postId).removeClass("accepted-answer"); $("#"+commentLinkIdPrefix+postId).removeClass("comment-link-accepted"); } else if(data.success == "1"){ var acceptedButtons = 'div.'+ voteContainerId +' img[id^='+ imgIdPrefixAccept +']'; - $(acceptedButtons).attr("src", "/content/images/vote-accepted.png"); +<<<<<<< HEAD:templates/content/js/com.cnprog.post.js + $(acceptedButtons).attr("src", $.i18n._("/") + "content/images/vote-accepted.png"); +======= + $(acceptedButtons).attr("src", scriptUrl + "content/images/vote-accepted.png"); +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/content/js/com.cnprog.post.js var answers = ("div[id^="+answerContainerIdPrefix +"]"); $(answers).removeClass("accepted-answer"); var commentLinks = ("div[id^="+answerContainerIdPrefix +"] div[id^="+ commentLinkIdPrefix +"]"); $(commentLinks).removeClass("comment-link-accepted"); - object.attr("src", "/content/images/vote-accepted-on.png"); +<<<<<<< HEAD:templates/content/js/com.cnprog.post.js + object.attr("src", $.i18n._("/") + "content/images/vote-accepted-on.png"); +======= + object.attr("src", scriptUrl + "content/images/vote-accepted-on.png"); +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/content/js/com.cnprog.post.js $("#"+answerContainerIdPrefix+postId).addClass("accepted-answer"); $("#"+commentLinkIdPrefix+postId).addClass("comment-link-accepted"); } @@ -288,7 +319,11 @@ var Vote = function(){ showMessage(object, favoriteAnonymousMessage.replace("{{QuestionID}}", questionId)); } else if(data.status == "1"){ - object.attr("src", "/content/images/vote-favorite-off.png"); +<<<<<<< HEAD:templates/content/js/com.cnprog.post.js + object.attr("src", $.i18n._("/") + "content/images/vote-favorite-off.png"); +======= + object.attr("src", scriptUrl + "content/images/vote-favorite-off.png"); +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/content/js/com.cnprog.post.js var fav = getFavoriteNumber(); fav.removeClass("my-favorite-number"); if(data.count == 0) @@ -296,7 +331,11 @@ var Vote = function(){ fav.text(data.count); } else if(data.success == "1"){ - object.attr("src", "/content/images/vote-favorite-on.png"); +<<<<<<< HEAD:templates/content/js/com.cnprog.post.js + object.attr("src", $.i18n._("/") + "/content/images/vote-favorite-on.png"); +======= + object.attr("src", scriptUrl + "content/images/vote-favorite-on.png"); +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/content/js/com.cnprog.post.js var fav = getFavoriteNumber(); fav.text(data.count); fav.addClass("my-favorite-number"); @@ -310,69 +349,79 @@ var Vote = function(){ if(data.allowed == "0" && data.success == "0"){ showMessage(object, voteAnonymousMessage.replace("{{QuestionID}}", questionId)); } - else if(data.allowed == "-3"){ + else if (data.allowed == "-3"){ showMessage(object, voteRequiredMoreVotes); } - else if(data.allowed == "-2"){ - if(voteType == VoteType.questionUpVote || voteType == VoteType.answerUpVote){ + else if (data.allowed == "-2"){ + if (voteType == VoteType.questionUpVote || voteType == VoteType.answerUpVote){ showMessage(object, upVoteRequiredScoreMessage); } - else if(voteType == VoteType.questionDownVote || voteType == VoteType.answerDownVote){ + else if (voteType == VoteType.questionDownVote || voteType == VoteType.answerDownVote){ showMessage(object, downVoteRequiredScoreMessage); } } - else if(data.allowed == "-1"){ + else if (data.allowed == "-1"){ showMessage(object, voteOwnDeniedMessage); } - else if(data.status == "2"){ + else if (data.status == "2"){ showMessage(object, voteDenyCancelMessage); } - else if(data.status == "1"){ + else if (data.status == "1"){ setVoteImage(voteType, true, object); setVoteNumber(object, data.count); } - else if(data.success == "1"){ + else if (data.success == "1"){ setVoteImage(voteType, false, object); setVoteNumber(object, data.count); - if(data.message.length > 0) + if (data.message.length > 0){ showMessage(object, data.message); + } } }; var callback_offensive = function(object, voteType, data){ object = $(object); - if(data.allowed == "0" && data.success == "0"){ + if (data.allowed == "0" && data.success == "0"){ showMessage(object, offensiveAnonymousMessage.replace("{{QuestionID}}", questionId)); } - else if(data.allowed == "-3"){ + else if (data.allowed == "-3"){ showMessage(object, offensiveNoFlagsLeftMessage); } - else if(data.allowed == "-2"){ + else if (data.allowed == "-2"){ showMessage(object, offensiveNoPermissionMessage); } - else if(data.status == "1"){ + else if (data.status == "1"){ showMessage(object, offensiveTwiceMessage); } - else if(data.success == "1"){ + else if (data.success == "1"){ $(object).children('span[class=darkred]').text("("+ data.count +")"); } }; var callback_remove = function(object, voteType, data){ - if(data.allowed == "0" && data.success == "0"){ + if (data.allowed == "0" && data.success == "0"){ showMessage(object, removeAnonymousMessage.replace("{{QuestionID}}", questionId)); } else if (data.success == "1"){ - if (removeActionType == 'delete'){ - postNode.addClass('deleted'); - postRemoveLink.innerHTML = $.i18n._('undelete'); - showMessage(object, deletedMessage); - } - else if (removeActionType == 'undelete') { - postNode.removeClass('deleted'); - postRemoveLink.innerHTML = $.i18n._('delete'); - showMessage(object, recoveredMessage); - } + if (voteType == VoteType.removeQuestion){ +<<<<<<< HEAD:templates/content/js/com.cnprog.post.js + window.location.href = $.i18n._("/") + $.i18n._("questions/"); +======= + window.location.href = scriptUrl + $.i18n._("questions/"); +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/content/js/com.cnprog.post.js + } + else { + if (removeActionType == 'delete'){ + postNode.addClass('deleted'); + postRemoveLink.innerHTML = $.i18n._('undelete'); + showMessage(object, deletedMessage); + } + else if (removeActionType == 'undelete') { + postNode.removeClass('deleted'); + postRemoveLink.innerHTML = $.i18n._('delete'); + showMessage(object, recoveredMessage); + } + } } }; @@ -391,7 +440,7 @@ var Vote = function(){ }, favorite: function(object){ - if(!currentUserId || currentUserId.toUpperCase() == "NONE"){ + if (!currentUserId || currentUserId.toUpperCase() == "NONE"){ showMessage(object, favoriteAnonymousMessage.replace("{{QuestionID}}", questionId)); return false; } @@ -399,14 +448,14 @@ var Vote = function(){ }, vote: function(object, voteType){ - if(!currentUserId || currentUserId.toUpperCase() == "NONE"){ + if (!currentUserId || currentUserId.toUpperCase() == "NONE"){ showMessage(object, voteAnonymousMessage.replace("{{QuestionID}}", questionId)); return false; } - if(voteType == VoteType.answerUpVote){ + if (voteType == VoteType.answerUpVote){ postId = object.attr("id").substring(imgIdPrefixAnswerVoteup.length); } - else if(voteType == VoteType.answerDownVote){ + else if (voteType == VoteType.answerDownVote){ postId = object.attr("id").substring(imgIdPrefixAnswerVotedown.length); } @@ -414,39 +463,43 @@ var Vote = function(){ }, offensive: function(object, voteType){ - if(!currentUserId || currentUserId.toUpperCase() == "NONE"){ + if (!currentUserId || currentUserId.toUpperCase() == "NONE"){ showMessage($(object), offensiveAnonymousMessage.replace("{{QuestionID}}", questionId)); return false; } - if(confirm(offensiveConfirmation)){ + if (confirm(offensiveConfirmation)){ postId = object.id.substr(object.id.lastIndexOf('-') + 1); submit(object, voteType, callback_offensive); } }, remove: function(object, voteType){ - if(!currentUserId || currentUserId.toUpperCase() == "NONE"){ + if (!currentUserId || currentUserId.toUpperCase() == "NONE"){ showMessage($(object), removeAnonymousMessage.replace("{{QuestionID}}", questionId)); return false; } - if(confirm(removeConfirmation)){ - bits = object.id.split('-'); - postId = bits.pop();/* this seems to be used within submit! */ - postType = bits.shift(); - - if (postType == 'answer'){ - postNode = $('#answer-container-' + postId); - postRemoveLink = object; - if (postNode.hasClass('deleted')){ - removeActionType = 'undelete'; - } - else { - removeActionType = 'delete'; - } - } + bits = object.id.split('-'); + postId = bits.pop();/* this seems to be used within submit! */ + postType = bits.shift(); + + var do_proceed = false; + if (postType == 'answer'){ + postNode = $('#answer-container-' + postId); + postRemoveLink = object; + if (postNode.hasClass('deleted')){ + removeActionType = 'undelete'; + do_proceed = true; + } + else { + removeActionType = 'delete'; + do_proceed = confirm(removeConfirmation); + } + } + else { + do_proceed = confirm(removeConfirmation); + } + if (do_proceed) { submit($(object), voteType, callback_remove); - - } } } @@ -457,21 +510,24 @@ var Vote = function(){ function createComments(type) { var objectType = type; var jDivInit = function(id) { - return $("#comments-" + objectType + '-' + id); + return $("#comments-container-" + objectType + '-' + id); }; var appendLoaderImg = function(id) { - appendLoader("#comments-" + objectType + '-' + id + " div.comments"); + appendLoader("#comments-container-" + objectType + '-' + id); }; - var canPostComments = function(id, jDiv) { - var jHidden = jDiv.siblings("#can-post-comments-" + objectType + '-' + id); + var canPostComments = function(id) { + var jHidden = $("#can-post-comments-" + objectType + '-' + id); return jHidden.val().toLowerCase() == "true"; }; - var renderForm = function(id, jDiv) { + var renderForm = function(id) { var formId = "form-comments-" + objectType + "-" + id; - if (canPostComments(id, jDiv)) { + var jDiv = $('#comments-link-' + objectType + "-" + id).parent(); + $(jDiv).css('background','none'); + $(jDiv).css('padding-left',0); + if (canPostComments(id)) { if (jDiv.find("#" + formId).length == 0) { var form = '<form id="' + formId + '" class="post-comments"><div>'; form += '<textarea name="comment" cols="60" rows="5" maxlength="300" onblur="'+ objectType +'Comments.updateTextCounter(this)" '; @@ -490,64 +546,96 @@ function createComments(type) { else { var divId = "comments-rep-needed-" + objectType + '-' + id; if (jDiv.find("#" + divId).length == 0) { - jDiv.append('<div id="' + divId + '" style="color:red">' + jDiv.append('<p id="' + divId + '" class="comment">' + $.i18n._('to comment, need') + ' ' + - + repNeededForComments + ' ' + $.i18n._('community reputation points') - + '<a href="/faq" class="comment-user">' + $.i18n._('please see') + 'faq</a></span>'); + + repNeededForComments + ' ' + $.i18n._('community karma points') +<<<<<<< HEAD:templates/content/js/com.cnprog.post.js + + '<a href="' + $.i18n._('/') + $.i18n._('faq/') + '" class="comment-user">' +======= + + '<a href="' + scriptUrl + $.i18n._('faq/') + '" class="comment-user">' +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/content/js/com.cnprog.post.js + + $.i18n._('please see') + 'faq</a></span></p>'); } } }; var getComments = function(id, jDiv) { - appendLoaderImg(id); - $.getJSON("/" + objectType + "s/" + id + "/comments/", function(json) { showComments(id, json); }); + //appendLoaderImg(id); +<<<<<<< HEAD:templates/content/js/com.cnprog.post.js + $.getJSON($.i18n._("/") + objectType + "s/" + id + "/" + $.i18n._("comments/") +======= + $.getJSON(scriptUrl + objectType + "s/" + id + "/" + $.i18n._("comments/") +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/content/js/com.cnprog.post.js + , function(json) { showComments(id, json); }); }; var showComments = function(id, json) { var jDiv = jDivInit(id); - jDiv = jDiv.find("div.comments"); // this div should contain any fetched comments.. - jDiv.find("div[id^='comment-" + objectType + "-'" + "]").remove(); // clean previous calls.. - + //jDiv = jDiv.find("div.comments"); // this div should contain any fetched comments.. + //jDiv.find("div[id^='comment-" + objectType + "-'" + "]").remove(); // clean previous calls.. + jDiv.children().remove(); removeLoader(); - if (json && json.length > 0) { for (var i = 0; i < json.length; i++) renderComment(jDiv, json[i]); - jDiv.children().show(); } }; + var renderDeleteCommentIcon = function(post_id, delete_url){ + if (canPostComments(post_id)){ + var html = ''; +<<<<<<< HEAD:templates/content/js/com.cnprog.post.js + var img = $.i18n._("/") + "content/images/close-small.png"; + var imgHover = $.i18n._("/") + "content/images/close-small-hover.png"; +======= + var img = scriptUrl + "content/images/close-small.png"; + var imgHover = scriptUrl + "content/images/close-small-hover.png"; +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/content/js/com.cnprog.post.js + html += '<img class="delete-icon" onclick="' + objectType + 'Comments.deleteComment($(this), ' + post_id + ', \'' + delete_url + '\')" src="' + img; + html += '" onmouseover="$(this).attr(\'src\', \'' + imgHover + '\')" onmouseout="$(this).attr(\'src\', \'' + img + html += '\')" title="' + $.i18n._('delete this comment') + '" />'; + return html; + } + else{ + return ''; + } + } + // {"Id":6,"PostId":38589,"CreationDate":"an hour ago","Text":"hello there!","UserDisplayName":"Jarrod Dixon","UserUrl":"/users/3/jarrod-dixon","DeleteUrl":null} var renderComment = function(jDiv, json) { - var html = '<div id="comment-' + objectType + "-" + json.id + '" style="display:none">' + json.text; - html += json.user_url ? ' – <a href="' + json.user_url + '"' : '<span'; + var html = '<p id="comment-' + json.id + '" class="comment" style="display:none">' + json.text; + html += json.user_url ? ' - <a href="' + json.user_url + '"' : '<span'; html += ' class="comment-user">' + json.user_display_name + (json.user_url ? '</a>' : '</span>'); - html += ' <span class="comment-date">(' + json.add_date + ')</span>'; + html += ' (' + json.comment_age + ')'; + + if (json.delete_url){ + html += renderDeleteCommentIcon(json.object_id, json.delete_url); + } if (json.delete_url) { - var img = "/content/images/close-small.png"; - var imgHover = "/content/images/close-small-hover.png"; - html += '<img onclick="' + objectType + 'Comments.deleteComment($(this), ' + json.object_id + ', \'' + json.delete_url + '\')" src="' + img; - html += '" onmouseover="$(this).attr(\'src\', \'' + imgHover + '\')" onmouseout="$(this).attr(\'src\', \'' + img - html += '\')" title="' + $.i18n._('delete this comment') + '" />'; } - html += '</div>'; + html += '</p>'; jDiv.append(html); }; var postComment = function(id, formId) { - appendLoaderImg(id); + //appendLoaderImg(id); var formSelector = "#" + formId; var textarea = $(formSelector + " textarea"); + //todo fix url translations!!! $.ajax({ type: "POST", - url: "/" + objectType + "s/" + id + "/comments/", +<<<<<<< HEAD:templates/content/js/com.cnprog.post.js + url: $.i18n._("/") + objectType + "s/" + id + "/" + $.i18n._("comments/"), +======= + url: scriptUrl + objectType + "s/" + id + "/" + $.i18n._("comments/"), +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/content/js/com.cnprog.post.js dataType: "json", data: { comment: textarea.val() }, success: function(json) { @@ -569,18 +657,64 @@ function createComments(type) { init: function() { // Setup "show comments" clicks.. - $("a[id^='comments-link-" + objectType + "-" + "']").unbind("click").click(function() { commentsFactory[objectType].show($(this).attr("id").substr(("comments-link-" + objectType + "-").length)); }); + $("a[id^='comments-link-" + objectType + "-" + "']").unbind("click").click(function() { + commentsFactory[objectType].show($(this).attr("id").substr(("comments-link-" + objectType + "-").length)); + }); + + var cBox = $("[id^='comments-container-" + objectType + "']"); + cBox.each( function(i){ + var post_id = $(this).attr('id').replace('comments-container-' + objectType + '-', ''); + $(this).children().each( + function(i){ + var comment_id = $(this).attr('id').replace('comment-',''); +<<<<<<< HEAD:templates/content/js/com.cnprog.post.js + var delete_url = $.i18n._('/') + objectType + 's/' + post_id + '/' +======= + var delete_url = scriptUrl + objectType + 's/' + post_id + '/' +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/content/js/com.cnprog.post.js + + $.i18n._('comments/') + comment_id + '/' + $.i18n._('delete/'); + var html = $(this).html(); + var CommentsClass; + if (objectType == 'question'){ + CommentsClass = questionComments; + } + else if (objectType == 'answer') { + CommentsClass = answerComments; + } + var delete_icon = $(this).find('img.delete-icon'); + delete_icon.click(function(){CommentsClass.deleteComment($(this),comment_id,delete_url);}); + delete_icon.unbind('mouseover').bind('mouseover', + function(){ +<<<<<<< HEAD:templates/content/js/com.cnprog.post.js + $(this).attr('src',$.i18n._('/') + 'content/images/close-small-hover.png'); +======= + $(this).attr('src',scriptUrl + 'content/images/close-small-hover.png'); +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/content/js/com.cnprog.post.js + } + ); + delete_icon.unbind('mouseout').bind('mouseout', + function(){ +<<<<<<< HEAD:templates/content/js/com.cnprog.post.js + $(this).attr('src',$.i18n._('/') + 'content/images/close-small.png'); +======= + $(this).attr('src',scriptUrl + 'content/images/close-small.png'); +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/content/js/com.cnprog.post.js + } + ); + } + ); + }); }, show: function(id) { var jDiv = jDivInit(id); getComments(id, jDiv); - renderForm(id, jDiv); + renderForm(id); jDiv.show(); - if (canPostComments(id, jDiv)) jDiv.find("textarea").get(0).focus(); - jDiv.siblings("a").unbind("click").click(function(){ - commentsFactory[objectType].hide(id); - }).text($.i18n._('hide comments')); + + var link = $('#comments-link-' + objectType + '-' + id); + if (canPostComments(id)) link.parent().find("textarea").get(0).focus(); + link.remove(); }, hide: function(id) { @@ -596,9 +730,9 @@ function createComments(type) { deleteComment: function(jImg, id, deleteUrl) { if (confirm($.i18n._('confirm delete comment'))) { jImg.hide(); - appendLoaderImg(id); $.post(deleteUrl, { dataNeeded: "forIIS7" }, function(json) { - showComments(id, json); + var par = jImg.parent(); + par.remove(); }, "json"); } }, diff --git a/templates/content/js/com.cnprog.tag_selector.js b/templates/content/js/com.cnprog.tag_selector.js new file mode 100644 index 00000000..f6c16c9c --- /dev/null +++ b/templates/content/js/com.cnprog.tag_selector.js @@ -0,0 +1,168 @@ +function pickedTags(){ + + var sendAjax = function(tagname, reason, action, callback){ + url = scriptUrl; + if (action == 'add'){ + url += $.i18n._('mark-tag/'); + if (reason == 'good'){ + url += $.i18n._('interesting/'); + } + else { + url += $.i18n._('ignored/'); + } + } + else { + url += $.i18n._('unmark-tag/'); + } + url = url + tagname + '/'; + + call_settings = { + type:'POST', + url:url + } + if (callback != false){ + call_settings['success'] = callback; + } + $.ajax(call_settings); + } + + + var unpickTag = function(from_target ,tagname, reason, send_ajax){ + //send ajax request to delete tag + var deleteTagLocally = function(){ + from_target[tagname].remove(); + delete from_target[tagname]; + } + if (send_ajax){ + sendAjax(tagname,reason,'remove',deleteTagLocally); + } + else { + deleteTagLocally(); + } + + } + + var setupTagDeleteEvents = function(obj,tag_store,tagname,reason,send_ajax){ + obj.unbind('mouseover').bind('mouseover', function(){ + $(this).attr('src', scriptUrl + 'content/images/close-small-hover.png'); + }); + obj.unbind('mouseout').bind('mouseout', function(){ + $(this).attr('src', scriptUrl + 'content/images/close-small-dark.png'); + }); + obj.click( function(){ + unpickTag(tag_store,tagname,reason,send_ajax); + }); + } + + var handlePickedTag = function(obj,reason){ + var tagname = $.trim($(obj).prev().attr('value')); + to_target = interestingTags; + from_target = ignoredTags; + if (reason == 'bad'){ + to_target = ignoredTags; + from_target = interestingTags; + to_tag_container = $('div .tags.ignored'); + } + else if (reason != 'good'){ + return; + } + else { + to_tag_container = $('div .tags.interesting'); + } + + if (tagname in from_target){ + unpickTag(from_target,tagname,reason,false); + } + + if (!(tagname in to_target)){ + //send ajax request to pick this tag + + sendAjax(tagname,reason,'add',function(){ + new_tag = $('<span></span>'); + new_tag.addClass('deletable-tag'); + tag_link = $('<a></a>'); + tag_link.attr('rel','tag'); + tag_link.attr('href', scriptUrl + $.i18n._('tags/') + tagname); + tag_link.html(tagname); + del_link = $('<img></img>'); + del_link.addClass('delete-icon'); + del_link.attr('src', scriptUrl + 'content/images/close-small-dark.png'); + + setupTagDeleteEvents(del_link, to_target, tagname, reason, true); + + new_tag.append(tag_link); + new_tag.append(del_link); + to_tag_container.append(new_tag); + + to_target[tagname] = new_tag; + }); + } + } + + var collectPickedTags = function(){ + var good_prefix = 'interesting-tag-'; + var bad_prefix = 'ignored-tag-'; + var good_re = RegExp('^' + good_prefix); + var bad_re = RegExp('^' + bad_prefix); + interestingTags = {}; + ignoredTags = {}; + $('.deletable-tag').each( + function(i,item){ + item_id = $(item).attr('id') + if (good_re.test(item_id)){ + tag_name = item_id.replace(good_prefix,''); + tag_store = interestingTags; + reason = 'good'; + } + else if (bad_re.test(item_id)){ + tag_name = item_id.replace(bad_prefix,''); + tag_store = ignoredTags; + reason = 'bad'; + } + else { + return; + } + tag_store[tag_name] = $(item); + setupTagDeleteEvents($(item).find('img'),tag_store,tag_name,reason,true) + } + ); + } + + var setupHideIgnoredQuestionsControl = function(){ + $('#hideIgnoredTagsCb').unbind('click').click(function(){ + $.ajax({ + type: 'POST', + dataType: 'json', + cache: false, + url: scriptUrl + $.i18n._('command/'), + data: {command:'toggle-ignored-questions'} + }); + }); + } + return { + init: function(){ + collectPickedTags(); + setupHideIgnoredQuestionsControl(); + $("#interestingTagInput, #ignoredTagInput").autocomplete(tags, { + minChars: 1, + matchContains: true, + max: 20, + multiple: true, + multipleSeparator: " ", + formatItem: function(row, i, max) { + return row.n + " ("+ row.c +")"; + }, + formatResult: function(row, i, max){ + return row.n; + } + + }); + $("#interestingTagAdd").click(function(){handlePickedTag(this,'good')}); + $("#ignoredTagAdd").click(function(){handlePickedTag(this,'bad')}); + } + }; +} + +$(document).ready( function(){ + pickedTags().init(); +}); diff --git a/templates/content/js/com.cnprog.utils.js b/templates/content/js/com.cnprog.utils.js index e271ed78..5c0c4a27 100644 --- a/templates/content/js/com.cnprog.utils.js +++ b/templates/content/js/com.cnprog.utils.js @@ -23,7 +23,12 @@ var notify = function() { }, close: function(doPostback) { if (doPostback) { - $.post("/messages/markread/", { formdata: "required" }); +<<<<<<< HEAD:templates/content/js/com.cnprog.utils.js + $.post($.i18n._("/") + $.i18n._("messages/") + +======= + $.post(scriptUrl + $.i18n._("messages/") + +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/content/js/com.cnprog.utils.js + $.i18n._("markread/"), { formdata: "required" }); } $(".notify").fadeOut("fast"); $("body").css("margin-top", "0"); @@ -35,7 +40,11 @@ var notify = function() { function appendLoader(containerSelector) { $(containerSelector).append('<img class="ajax-loader" ' - +'src="/content/images/indicator.gif" title="' +<<<<<<< HEAD:templates/content/js/com.cnprog.utils.js + +'src="' + $.i18n._('/') + 'content/images/indicator.gif" title="' +======= + +'src="' + scriptUrl + 'content/images/indicator.gif" title="' +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/content/js/com.cnprog.utils.js +$.i18n._('loading...') +'" alt="' +$.i18n._('loading...') @@ -103,7 +112,7 @@ var CPValidator = function(){ return { tags: { required: " " + $.i18n._('tags cannot be empty'), - maxlength: " " + $.i18n._('tablimits info'), + maxlength: " " + $.i18n._('tablimits info') }, text: { required: " " + $.i18n._('content cannot be empty'), diff --git a/templates/content/js/compress.bat b/templates/content/js/compress.bat index 41e1882a..5b2673cf 100644 --- a/templates/content/js/compress.bat +++ b/templates/content/js/compress.bat @@ -2,5 +2,4 @@ #java -jar yuicompressor-2.4.2.jar --type js --charset utf-8 wmd\showdown.js -o wmd\showdown-min.js #java -jar yuicompressor-2.4.2.jar --type js --charset utf-8 com.cnprog.post.js -o com.cnprog.post.pack.js java -jar yuicompressor-2.4.2.jar --type js --charset utf-8 se_hilite_src.js -o se_hilite.js - -pause
\ No newline at end of file +pause diff --git a/templates/content/js/flot-build.bat b/templates/content/js/flot-build.bat index fc715e3a..f9f32cb7 100644 --- a/templates/content/js/flot-build.bat +++ b/templates/content/js/flot-build.bat @@ -1,3 +1,3 @@ java -jar yuicompressor-2.4.2.jar --type js --charset utf-8 jquery.flot.js -o jquery.flot.pack.js -pause
\ No newline at end of file +pause diff --git a/templates/content/js/jquery.form.js b/templates/content/js/jquery.form.js new file mode 100644 index 00000000..443114fd --- /dev/null +++ b/templates/content/js/jquery.form.js @@ -0,0 +1,654 @@ +/* + * jQuery Form Plugin + * version: 2.33 (22-SEP-2009) + * @requires jQuery v1.2.6 or later + * + * Examples and documentation at: http://malsup.com/jquery/form/ + * Dual licensed under the MIT and GPL licenses: + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + */ +;(function($) { + +/* + Usage Note: + ----------- + Do not use both ajaxSubmit and ajaxForm on the same form. These + functions are intended to be exclusive. Use ajaxSubmit if you want + to bind your own submit handler to the form. For example, + + $(document).ready(function() { + $('#myForm').bind('submit', function() { + $(this).ajaxSubmit({ + target: '#output' + }); + return false; // <-- important! + }); + }); + + Use ajaxForm when you want the plugin to manage all the event binding + for you. For example, + + $(document).ready(function() { + $('#myForm').ajaxForm({ + target: '#output' + }); + }); + + When using ajaxForm, the ajaxSubmit function will be invoked for you + at the appropriate time. +*/ + +/** + * ajaxSubmit() provides a mechanism for immediately submitting + * an HTML form using AJAX. + */ +$.fn.ajaxSubmit = function(options) { + // fast fail if nothing selected (http://dev.jquery.com/ticket/2752) + if (!this.length) { + log('ajaxSubmit: skipping submit process - no element selected'); + return this; + } + + if (typeof options == 'function') + options = { success: options }; + + var url = $.trim(this.attr('action')); + if (url) { + // clean url (don't include hash vaue) + url = (url.match(/^([^#]+)/)||[])[1]; + } + url = url || window.location.href || ''; + + options = $.extend({ + url: url, + type: this.attr('method') || 'GET' + }, options || {}); + + // hook for manipulating the form data before it is extracted; + // convenient for use with rich editors like tinyMCE or FCKEditor + var veto = {}; + this.trigger('form-pre-serialize', [this, options, veto]); + if (veto.veto) { + log('ajaxSubmit: submit vetoed via form-pre-serialize trigger'); + return this; + } + + // provide opportunity to alter form data before it is serialized + if (options.beforeSerialize && options.beforeSerialize(this, options) === false) { + log('ajaxSubmit: submit aborted via beforeSerialize callback'); + return this; + } + + var a = this.formToArray(options.semantic); + if (options.data) { + options.extraData = options.data; + for (var n in options.data) { + if(options.data[n] instanceof Array) { + for (var k in options.data[n]) + a.push( { name: n, value: options.data[n][k] } ); + } + else + a.push( { name: n, value: options.data[n] } ); + } + } + + // give pre-submit callback an opportunity to abort the submit + if (options.beforeSubmit && options.beforeSubmit(a, this, options) === false) { + log('ajaxSubmit: submit aborted via beforeSubmit callback'); + return this; + } + + // fire vetoable 'validate' event + this.trigger('form-submit-validate', [a, this, options, veto]); + if (veto.veto) { + log('ajaxSubmit: submit vetoed via form-submit-validate trigger'); + return this; + } + + var q = $.param(a); + + if (options.type.toUpperCase() == 'GET') { + options.url += (options.url.indexOf('?') >= 0 ? '&' : '?') + q; + options.data = null; // data is null for 'get' + } + else + options.data = q; // data is the query string for 'post' + + var $form = this, callbacks = []; + if (options.resetForm) callbacks.push(function() { $form.resetForm(); }); + if (options.clearForm) callbacks.push(function() { $form.clearForm(); }); + + // perform a load on the target only if dataType is not provided + if (!options.dataType && options.target) { + var oldSuccess = options.success || function(){}; + callbacks.push(function(data) { + $(options.target).html(data).each(oldSuccess, arguments); + }); + } + else if (options.success) + callbacks.push(options.success); + + options.success = function(data, status) { + for (var i=0, max=callbacks.length; i < max; i++) + callbacks[i].apply(options, [data, status, $form]); + }; + + // are there files to upload? + var files = $('input:file', this).fieldValue(); + var found = false; + for (var j=0; j < files.length; j++) + if (files[j]) + found = true; + + var multipart = false; +// var mp = 'multipart/form-data'; +// multipart = ($form.attr('enctype') == mp || $form.attr('encoding') == mp); + + // options.iframe allows user to force iframe mode + if (options.iframe || found || multipart) { + // hack to fix Safari hang (thanks to Tim Molendijk for this) + // see: http://groups.google.com/group/jquery-dev/browse_thread/thread/36395b7ab510dd5d + if (options.closeKeepAlive) + $.get(options.closeKeepAlive, fileUpload); + else + fileUpload(); + } + else{ + $.ajax(options); + } + + // fire 'notify' event + this.trigger('form-submit-notify', [this, options]); + return this; + + + // private function for handling file uploads (hat tip to YAHOO!) + function fileUpload() { + var form = $form[0]; + + if ($(':input[name=submit]', form).length) { + alert('Error: Form elements must not be named "submit".'); + return; + } + + var opts = $.extend({}, $.ajaxSettings, options); + var s = $.extend(true, {}, $.extend(true, {}, $.ajaxSettings), opts); + + var id = 'jqFormIO' + (new Date().getTime()); + var $io = $('<iframe id="' + id + '" name="' + id + '" src="about:blank" />'); + var io = $io[0]; + + $io.css({ position: 'absolute', top: '-1000px', left: '-1000px' }); + + var xhr = { // mock object + aborted: 0, + responseText: null, + responseXML: null, + status: 0, + statusText: 'n/a', + getAllResponseHeaders: function() {}, + getResponseHeader: function() {}, + setRequestHeader: function() {}, + abort: function() { + this.aborted = 1; + $io.attr('src','about:blank'); // abort op in progress + } + }; + + var g = opts.global; + // trigger ajax global events so that activity/block indicators work like normal + if (g && ! $.active++) $.event.trigger("ajaxStart"); + if (g) $.event.trigger("ajaxSend", [xhr, opts]); + + if (s.beforeSend && s.beforeSend(xhr, s) === false) { + s.global && $.active--; + return; + } + if (xhr.aborted) + return; + + var cbInvoked = 0; + var timedOut = 0; + + // add submitting element to data if we know it + var sub = form.clk; + if (sub) { + var n = sub.name; + if (n && !sub.disabled) { + options.extraData = options.extraData || {}; + options.extraData[n] = sub.value; + if (sub.type == "image") { + options.extraData[name+'.x'] = form.clk_x; + options.extraData[name+'.y'] = form.clk_y; + } + } + } + + // take a breath so that pending repaints get some cpu time before the upload starts + setTimeout(function() { + // make sure form attrs are set + var t = $form.attr('target'), a = $form.attr('action'); + + // update form attrs in IE friendly way + form.setAttribute('target',id); + if (form.getAttribute('method') != 'POST') + form.setAttribute('method', 'POST'); + if (form.getAttribute('action') != opts.url) + form.setAttribute('action', opts.url); + + // ie borks in some cases when setting encoding + if (! options.skipEncodingOverride) { + $form.attr({ + encoding: 'multipart/form-data', + enctype: 'multipart/form-data' + }); + } + + // support timout + if (opts.timeout) + setTimeout(function() { timedOut = true; cb(); }, opts.timeout); + + // add "extra" data to form if provided in options + var extraInputs = []; + try { + if (options.extraData) + for (var n in options.extraData) + extraInputs.push( + $('<input type="hidden" name="'+n+'" value="'+options.extraData[n]+'" />') + .appendTo(form)[0]); + + // add iframe to doc and submit the form + $io.appendTo('body'); + io.attachEvent ? io.attachEvent('onload', cb) : io.addEventListener('load', cb, false); + form.submit(); + } + finally { + // reset attrs and remove "extra" input elements + form.setAttribute('action',a); + t ? form.setAttribute('target', t) : $form.removeAttr('target'); + $(extraInputs).remove(); + } + }, 10); + + var domCheckCount = 50; + + function cb() { + if (cbInvoked++) return; + + io.detachEvent ? io.detachEvent('onload', cb) : io.removeEventListener('load', cb, false); + + var ok = true; + try { + if (timedOut) throw 'timeout'; + // extract the server response from the iframe + var data, doc; + + doc = io.contentWindow ? io.contentWindow.document : io.contentDocument ? io.contentDocument : io.document; + + var isXml = opts.dataType == 'xml' || doc.XMLDocument || $.isXMLDoc(doc); + log('isXml='+isXml); + if (!isXml && (doc.body == null || doc.body.innerHTML == '')) { + if (--domCheckCount) { + // in some browsers (Opera) the iframe DOM is not always traversable when + // the onload callback fires, so we loop a bit to accommodate + cbInvoked = 0; + setTimeout(cb, 100); + return; + } + log('Could not access iframe DOM after 50 tries.'); + return; + } + + xhr.responseText = doc.body ? doc.body.innerHTML : null; + xhr.responseXML = doc.XMLDocument ? doc.XMLDocument : doc; + xhr.getResponseHeader = function(header){ + var headers = {'content-type': opts.dataType}; + return headers[header]; + }; + + if (opts.dataType == 'json' || opts.dataType == 'script') { + // see if user embedded response in textarea + var ta = doc.getElementsByTagName('textarea')[0]; + if (ta) + xhr.responseText = ta.value; + else { + // account for browsers injecting pre around json response + var pre = doc.getElementsByTagName('pre')[0]; + if (pre) + xhr.responseText = pre.innerHTML; + } + } + else if (opts.dataType == 'xml' && !xhr.responseXML && xhr.responseText != null) { + xhr.responseXML = toXml(xhr.responseText); + } + data = $.httpData(xhr, opts.dataType); + } + catch(e){ + ok = false; + $.handleError(opts, xhr, 'error', e); + } + + // ordering of these callbacks/triggers is odd, but that's how $.ajax does it + if (ok) { + opts.success(data, 'success'); + if (g) $.event.trigger("ajaxSuccess", [xhr, opts]); + } + if (g) $.event.trigger("ajaxComplete", [xhr, opts]); + if (g && ! --$.active) $.event.trigger("ajaxStop"); + if (opts.complete) opts.complete(xhr, ok ? 'success' : 'error'); + + // clean up + setTimeout(function() { + $io.remove(); + xhr.responseXML = null; + }, 100); + }; + + function toXml(s, doc) { + if (window.ActiveXObject) { + doc = new ActiveXObject('Microsoft.XMLDOM'); + doc.async = 'false'; + doc.loadXML(s); + } + else + doc = (new DOMParser()).parseFromString(s, 'text/xml'); + return (doc && doc.documentElement && doc.documentElement.tagName != 'parsererror') ? doc : null; + }; + }; +}; + +/** + * ajaxForm() provides a mechanism for fully automating form submission. + * + * The advantages of using this method instead of ajaxSubmit() are: + * + * 1: This method will include coordinates for <input type="image" /> elements (if the element + * is used to submit the form). + * 2. This method will include the submit element's name/value data (for the element that was + * used to submit the form). + * 3. This method binds the submit() method to the form for you. + * + * The options argument for ajaxForm works exactly as it does for ajaxSubmit. ajaxForm merely + * passes the options argument along after properly binding events for submit elements and + * the form itself. + */ +$.fn.ajaxForm = function(options) { + return this.ajaxFormUnbind().bind('submit.form-plugin', function() { + $(this).ajaxSubmit(options); + return false; + }).bind('click.form-plugin', function(e) { + var $el = $(e.target); + if (!($el.is(":submit,input:image"))) { + return; + } + var form = this; + form.clk = e.target; + if (e.target.type == 'image') { + if (e.offsetX != undefined) { + form.clk_x = e.offsetX; + form.clk_y = e.offsetY; + } else if (typeof $.fn.offset == 'function') { // try to use dimensions plugin + var offset = $el.offset(); + form.clk_x = e.pageX - offset.left; + form.clk_y = e.pageY - offset.top; + } else { + form.clk_x = e.pageX - e.target.offsetLeft; + form.clk_y = e.pageY - e.target.offsetTop; + } + } + // clear form vars + setTimeout(function() { form.clk = form.clk_x = form.clk_y = null; }, 10); + }); +}; + +// ajaxFormUnbind unbinds the event handlers that were bound by ajaxForm +$.fn.ajaxFormUnbind = function() { + return this.unbind('submit.form-plugin click.form-plugin'); +}; + +/** + * formToArray() gathers form element data into an array of objects that can + * be passed to any of the following ajax functions: $.get, $.post, or load. + * Each object in the array has both a 'name' and 'value' property. An example of + * an array for a simple login form might be: + * + * [ { name: 'username', value: 'jresig' }, { name: 'password', value: 'secret' } ] + * + * It is this array that is passed to pre-submit callback functions provided to the + * ajaxSubmit() and ajaxForm() methods. + */ +$.fn.formToArray = function(semantic) { + var a = []; + if (this.length == 0) return a; + + var form = this[0]; + var els = semantic ? form.getElementsByTagName('*') : form.elements; + if (!els) return a; + for(var i=0, max=els.length; i < max; i++) { + var el = els[i]; + var n = el.name; + if (!n) continue; + + if (semantic && form.clk && el.type == "image") { + // handle image inputs on the fly when semantic == true + if(!el.disabled && form.clk == el) { + a.push({name: n, value: $(el).val()}); + a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y}); + } + continue; + } + + var v = $.fieldValue(el, true); + if (v && v.constructor == Array) { + for(var j=0, jmax=v.length; j < jmax; j++) + a.push({name: n, value: v[j]}); + } + else if (v !== null && typeof v != 'undefined') + a.push({name: n, value: v}); + } + + if (!semantic && form.clk) { + // input type=='image' are not found in elements array! handle it here + var $input = $(form.clk), input = $input[0], n = input.name; + if (n && !input.disabled && input.type == 'image') { + a.push({name: n, value: $input.val()}); + a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y}); + } + } + return a; +}; + +/** + * Serializes form data into a 'submittable' string. This method will return a string + * in the format: name1=value1&name2=value2 + */ +$.fn.formSerialize = function(semantic) { + //hand off to jQuery.param for proper encoding + return $.param(this.formToArray(semantic)); +}; + +/** + * Serializes all field elements in the jQuery object into a query string. + * This method will return a string in the format: name1=value1&name2=value2 + */ +$.fn.fieldSerialize = function(successful) { + var a = []; + this.each(function() { + var n = this.name; + if (!n) return; + var v = $.fieldValue(this, successful); + if (v && v.constructor == Array) { + for (var i=0,max=v.length; i < max; i++) + a.push({name: n, value: v[i]}); + } + else if (v !== null && typeof v != 'undefined') + a.push({name: this.name, value: v}); + }); + //hand off to jQuery.param for proper encoding + return $.param(a); +}; + +/** + * Returns the value(s) of the element in the matched set. For example, consider the following form: + * + * <form><fieldset> + * <input name="A" type="text" /> + * <input name="A" type="text" /> + * <input name="B" type="checkbox" value="B1" /> + * <input name="B" type="checkbox" value="B2"/> + * <input name="C" type="radio" value="C1" /> + * <input name="C" type="radio" value="C2" /> + * </fieldset></form> + * + * var v = $(':text').fieldValue(); + * // if no values are entered into the text inputs + * v == ['',''] + * // if values entered into the text inputs are 'foo' and 'bar' + * v == ['foo','bar'] + * + * var v = $(':checkbox').fieldValue(); + * // if neither checkbox is checked + * v === undefined + * // if both checkboxes are checked + * v == ['B1', 'B2'] + * + * var v = $(':radio').fieldValue(); + * // if neither radio is checked + * v === undefined + * // if first radio is checked + * v == ['C1'] + * + * The successful argument controls whether or not the field element must be 'successful' + * (per http://www.w3.org/TR/html4/interact/forms.html#successful-controls). + * The default value of the successful argument is true. If this value is false the value(s) + * for each element is returned. + * + * Note: This method *always* returns an array. If no valid value can be determined the + * array will be empty, otherwise it will contain one or more values. + */ +$.fn.fieldValue = function(successful) { + for (var val=[], i=0, max=this.length; i < max; i++) { + var el = this[i]; + var v = $.fieldValue(el, successful); + if (v === null || typeof v == 'undefined' || (v.constructor == Array && !v.length)) + continue; + v.constructor == Array ? $.merge(val, v) : val.push(v); + } + return val; +}; + +/** + * Returns the value of the field element. + */ +$.fieldValue = function(el, successful) { + var n = el.name, t = el.type, tag = el.tagName.toLowerCase(); + if (typeof successful == 'undefined') successful = true; + + if (successful && (!n || el.disabled || t == 'reset' || t == 'button' || + (t == 'checkbox' || t == 'radio') && !el.checked || + (t == 'submit' || t == 'image') && el.form && el.form.clk != el || + tag == 'select' && el.selectedIndex == -1)) + return null; + + if (tag == 'select') { + var index = el.selectedIndex; + if (index < 0) return null; + var a = [], ops = el.options; + var one = (t == 'select-one'); + var max = (one ? index+1 : ops.length); + for(var i=(one ? index : 0); i < max; i++) { + var op = ops[i]; + if (op.selected) { + var v = op.value; + if (!v) // extra pain for IE... + v = (op.attributes && op.attributes['value'] && !(op.attributes['value'].specified)) ? op.text : op.value; + if (one) return v; + a.push(v); + } + } + return a; + } + return el.value; +}; + +/** + * Clears the form data. Takes the following actions on the form's input fields: + * - input text fields will have their 'value' property set to the empty string + * - select elements will have their 'selectedIndex' property set to -1 + * - checkbox and radio inputs will have their 'checked' property set to false + * - inputs of type submit, button, reset, and hidden will *not* be effected + * - button elements will *not* be effected + */ +$.fn.clearForm = function() { + return this.each(function() { + $('input,select,textarea', this).clearFields(); + }); +}; + +/** + * Clears the selected form elements. + */ +$.fn.clearFields = $.fn.clearInputs = function() { + return this.each(function() { + var t = this.type, tag = this.tagName.toLowerCase(); + if (t == 'text' || t == 'password' || tag == 'textarea') + this.value = ''; + else if (t == 'checkbox' || t == 'radio') + this.checked = false; + else if (tag == 'select') + this.selectedIndex = -1; + }); +}; + +/** + * Resets the form data. Causes all form elements to be reset to their original value. + */ +$.fn.resetForm = function() { + return this.each(function() { + // guard against an input with the name of 'reset' + // note that IE reports the reset function as an 'object' + if (typeof this.reset == 'function' || (typeof this.reset == 'object' && !this.reset.nodeType)) + this.reset(); + }); +}; + +/** + * Enables or disables any matching elements. + */ +$.fn.enable = function(b) { + if (b == undefined) b = true; + return this.each(function() { + this.disabled = !b; + }); +}; + +/** + * Checks/unchecks any matching checkboxes or radio buttons and + * selects/deselects and matching option elements. + */ +$.fn.selected = function(select) { + if (select == undefined) select = true; + return this.each(function() { + var t = this.type; + if (t == 'checkbox' || t == 'radio') + this.checked = select; + else if (this.tagName.toLowerCase() == 'option') { + var $sel = $(this).parent('select'); + if (select && $sel[0] && $sel[0].type == 'select-one') { + // deselect all other options + $sel.find('option').selected(false); + } + this.selected = select; + } + }); +}; + +// helper fn for console logging +// set $.fn.ajaxSubmit.debug to true to enable debug logging +function log() { + if ($.fn.ajaxSubmit.debug && window.console && window.console.log) + window.console.log('[jquery.form] ' + Array.prototype.join.call(arguments,'')); +}; + +})(jQuery); diff --git a/templates/content/js/wmd/wmd.js b/templates/content/js/wmd/wmd.js index 7b611dba..2234250b 100644 --- a/templates/content/js/wmd/wmd.js +++ b/templates/content/js/wmd/wmd.js @@ -54,7 +54,11 @@ Attacklab.wmdBase = function(){ var uploadImageHTML ="<div>" + $.i18n._('upload image') + "</div>" + "<input type=\"file\" name=\"file-upload\" id=\"file-upload\" size=\"26\" "+ "onchange=\"return ajaxFileUpload($('#image-url'));\"/><br>" + - "<img id=\"loading\" src=\"/content/images/indicator.gif\" style=\"display:none;\"/>"; +<<<<<<< HEAD:templates/content/js/wmd/wmd.js + "<img id=\"loading\" src=\"" + $.i18n._("/") + "content/images/indicator.gif\" style=\"display:none;\"/>"; +======= + "<img id=\"loading\" src=\"" + scriptUrl + "content/images/indicator.gif\" style=\"display:none;\"/>"; +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/content/js/wmd/wmd.js // The default text that appears in the dialog input box when entering // links. diff --git a/templates/content/style/default.css b/templates/content/style/default.css index 0221cc03..2bc185ad 100644 --- a/templates/content/style/default.css +++ b/templates/content/style/default.css @@ -7,9 +7,9 @@ Style sheet for cnprog.com All rights reserved. 2008 CNPROG.COM */ -@import url(/content/style/jquery.autocomplete.css); -@import url(/content/style/openid.css); -@import url(/content/style/prettify.css); +@import url(content/style/jquery.autocomplete.css); +@import url(content/style/openid.css); +@import url(content/style/prettify.css); html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, font, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, form, label, table, caption, tbody, tfoot, thead, tr, th, td { @@ -232,19 +232,19 @@ h4 {display:block;font-size:90%; font-family:Verdana;color:#ccc;} } #main-bar .golden{ - background:url(/content/images/bg_title_golden.gif) no-repeat; + background:url(../../images/bg_title_golden.gif) no-repeat; } #main-bar .pink{ - background:url(/content/images/bg_title_red.gif) no-repeat; + background:url(../../images/bg_title_red.gif) no-repeat; } #main-bar .orange{ - background:url(/content/images/bg_title_orange.gif) no-repeat; + background:url(../../images/bg_title_orange.gif) no-repeat; } #main-bar .green{ - background:url(/content/images/bg_title_green.gif) no-repeat; + background:url(../../images/bg_title_green.gif) no-repeat; } #tab{ @@ -906,7 +906,7 @@ h4 {display:block;font-size:90%; font-family:Verdana;color:#ccc;} /* 2 textarea resizer styles */ div.grippie { - background:#EEEEEE url(/content/images/grippie.png) no-repeat scroll center 2px; + background:#EEEEEE url(../../images/grippie.png) no-repeat scroll center 2px; border-color:#DDDDDD; border-style:solid; border-width:0pt 1px 1px; @@ -923,14 +923,14 @@ div.grippie { } .openid-input{ - background:url(/content/images/openid.gif) no-repeat; + background:url(../../images/openid.gif) no-repeat; padding-left:15px; cursor:pointer; } .openid-login-input{ background-position:center left; - background:url(/content/images/openid.gif) no-repeat 0% 50%; + background:url(../../images/openid.gif) no-repeat 0% 50%; padding:5px 5px 5px 15px; cursor:pointer; font-family:Trebuchet MS; @@ -941,7 +941,7 @@ div.grippie { .openid-login-submit{ padding:6px; - #padding:4px; + /*padding:4px;*/ cursor:pointer; font-weight:bold; font-size:120%; @@ -990,7 +990,6 @@ div.grippie { .item-right{ float:left; - } .vote-number{ @@ -1609,7 +1608,9 @@ div.comments { } div.post-comments{ - width:585px + width:585px; + clear:both; + float:left; } form.post-comments textarea { diff --git a/templates/content/style/jquery.autocomplete.css b/templates/content/style/jquery.autocomplete.css index 7c3127d1..3bf2c2d9 100644 --- a/templates/content/style/jquery.autocomplete.css +++ b/templates/content/style/jquery.autocomplete.css @@ -36,7 +36,7 @@ } .ac_loading { - background: white url('/content/images/indicator.gif') right center no-repeat; + background: white url(../../images/indicator.gif) right center no-repeat; } .ac_odd { diff --git a/templates/content/style/style.css b/templates/content/style/style.css index 241d72da..cf35ff68 100644 --- a/templates/content/style/style.css +++ b/templates/content/style/style.css @@ -1,15 +1,16 @@ -@import url(/content/style/jquery.autocomplete.css); -@import url(/content/style/openid.css); -@import url(/content/style/prettify.css); +@import url(jquery.autocomplete.css); +@import url(openid.css); +@import url(prettify.css); /* 公用 */ body{background:#FFF; font-size:12px; line-height:150%; margin:0; padding:0; color:#000; font-family: sans-serif; } div{margin:0 auto; padding:0;} h1,h2,h3,h4,h5,h6,ul,li,dl,dt,dd,form,img,p{margin:0; padding:0; border:none; } -input, select {font-family:Trebuchet MS,"segoe ui",Helvetica,"Microsoft YaHei",宋体,Tahoma,Verdana,MingLiu,PMingLiu,Arial,sans-serif;} +label {vertical-align:middle;} +hr {border:none;border-top: 1px dashed #ccccce;} +input, select {vertical-align:middle;font-family:Trebuchet MS,"segoe ui",Helvetica,"Microsoft YaHei",宋体,Tahoma,Verdana,MingLiu,PMingLiu,Arial,sans-serif;} p{margin-bottom:13px; font-size:13px; line-height:140%;} a {color:#333333; text-decoration:none;} - .badges a {color:#763333;text-decoration:underline;} a:hover {text-decoration:underline;} .block{width:960px; height:auto;} @@ -49,6 +50,12 @@ ol margin-bottom: 1em; padding-left:0px; } +td ul { + vertical-align:middle; +} +li input { + margin: 3px 3px 4px 3px; +} pre { font-family: Consolas, Monaco, Liberation Mono, Lucida Console, Monospace; @@ -112,62 +119,129 @@ blockquote #navBar .nav {margin:20px 0px 0px 16px; /*letter-spacing:1px; */ } -#navBar .nav a {color:#333333; background-color:#F9F7ED; - border: 1px solid #aaaaaa; - border-bottom: none; +#navBar .nav a {color:#333333; background-color:#fff0e0; + /*border-left: 1px solid #eeeeec; + border-right: 1px solid #babdb6; + border-top: 1px solid #eeeeec;*/ + border: 1px solid #888888; + border-bottom: none; padding:0px 12px 3px 12px; height:25px; line-height:30px;margin-left:10px; font-size:14px; font-weight:400; text-decoration:none;display: block;float: left;} #navBar .nav a:hover {text-decoration:underline} -#navBar .nav a.on {height:24px;line-height:28px;border:1px solid #d64000; background:#B02B2C; color:#FFF; font-weight:600; text-decoration:none} -#navBar .nav a.special {font-size:15px; color:#B02B2C; font-weight:bold; text-decoration:none; } +#navBar .nav a.on {height:24px;line-height:28px; + border-bottom: 1px solid #a40000; + border-right:1px solid #820000; + border-top:1px solid #d40000; + border-left:1px solid #d40000; + /*background:#A31E39; */ + background:#a40000; + color:#FFF; font-weight:600; text-decoration:none} +#navBar .nav a.special {font-size:14px; color:#B02B2C; font-weight:bold; text-decoration:none; } #navBar .nav a.special:hover {text-decoration:underline;} #navBar .nav div.focus {float:right; padding-right:0px;} /*搜索栏*/ -#searchBar { - background-color:#9db2b1;/*#e9b96e;*/ - padding:5px 0 0 0;} /* #B02B2C */ +#searchBar {width:958px; + background-color:#888a85;/*#e9b96e;*/ + border: 1px solid #aaaaaa; + padding:4px 0 0 0;} /* #B02B2C */ #searchBar .content { } #searchBar .searchInput {font-size:13px; height:18px; width:400px;} #searchBar .searchBtn {font-size:14px; height:26px; width:80px;} -#searchBar .options {padding-top:5px; font-size:100%;color:#EEE; +#searchBar .options {padding:3px 0 3px 0;font-size:100%;color:#EEE; /*letter-spacing:1px;*/ } -#searchBar .options INPUT {margin-left:15px;} +#searchBar .options INPUT {margin:0 3px 0 15px;} #searchBar .options INPUT:hover {cursor:pointer} /*问题列表*/ #listA {float:left; background-color:#FFF;padding:0 0px 0 0px; width:100%;} -#listA .qstA {padding:3px 5px 0 5px; margin:0 0px 10px 0px; background:url(/content/images/quest-bg.gif) repeat-x top;} +#listA .qstA { + position:relative; + padding:3px 5px 5px 10px; + border-top:1px dashed #ccccce; + /*border-left:1px solid #ebebbe; + border-right:1px solid #b4b48e; + border-bottom:1px solid #b4b48e;*/ + background: white;/* #f9f7ed;*/ + /*margin:10px 0 10px 0;*/ + /*background:url(../images/quest-bg.gif) repeat-x top;*/ +} #listA .qstA thumb {float:left; } -#listA .qstA H2 {font-size:15px; font-weight:800; margin:8px auto;padding:0px;} -#listA .qstA H2 a {color:#663333; } -#listA .qstA .stat {font-size:13px;letter-spacing:1px;float:right;} +#listA .qstA H2 {font-size:14px; font-weight:800; margin:8px auto;padding:0px;} +#listA .qstA H2 a {color:333333/*#2e3436*/;font-size:15px;} +#listA .qstA .stat { + position:absolute; + right:0px; + bottom:5px; + font-size:12px; + /*letter-spacing:1px;*/ + float:right; +} #listA .qstA .stat span {margin-right:5px;} #listA .qstA .stat td {min-width:40px;text-align:center;} -#listA .qstA .stat .num {font-family:arial;color:#905213; font-size:20px; font-weight:800;} -#listA .qstA .stat .unit {color:#777;} -#listA .qstA .from {margin-top:5px; font-size:13px;} -#listA .qstA .from .score {font-family:arial;} -#listA .qstA .date {margin-left:20px; color:#777;} -#listA .qstA .wiki {color:#663333;font-size:12px;} +#listA .qstA .stat .num { + font-family:sans-serif; + color:#a40000; + /*background:#eeeeec; + border: 1px solid #babdb6;*/ + margin:0px; + font-size:17px; + font-weight:800; +} +#listA .qstA table {border-spacing:0px;} +#listA .qstA table td {padding:0px;width:60px;text-align:center;} +#listA .qstA .stat .unit {color:#777777;margin:0px} +#listA .qstA .from {margin-top:5px; font-size:13px;color:#777777;} +#listA .qstA .from .score {font-family:sans-serif;color:#555555;} +#listA .qstA .date {margin-left:10px; color:#777777;} +#listA .qstA .wiki {color:#763333;font-size:12px;} #listA .qstA .from a {} #listA .qstA .from IMG {vertical-align:middle;} #listA .qstA .author {font-weight:400; } -#listA .qstA .author a{ } +#listA .qstA .author a{color:#444444; } #listA .qstA .summary{margin-right:5px;} - +#question-table { + margin-bottom:10px; + /*border-bottom:1px solid #888a85;*/ +} .evenMore {font-size:14px; font-weight:800;} -.questions-count{font-size:32px;font-family:sans-serif;font-weight:600;padding:0 0 5px 7px;color:#a40000;} + +.questions-count{ + font-size:32px; + font-family:sans-serif; + font-weight:600; + padding:0 0 5px 0px; + color:#a40000; + margin-top:3px; +} /*内容块*/ -.boxA {background:#777; padding:6px; margin-bottom:8px;} +.boxA {background:#888a85; padding:6px; margin-bottom:8px;border 1px solid #babdb6;} .boxA H3 {font-size:13px; font-weight:800; color:#FFF; margin:0; padding:0; margin-bottom:4px;} .boxA .body {border:1px solid #999; padding:8px; background:#FFF; font-size:13px;} .boxA .more {padding:2px; text-align:right; font-weight:800;} -.boxB {background:#F9F7CD; padding:6px; margin-bottom:8px;} -.boxB H3 {font-size:13px; font-weight:800; color:#000; margin:0; padding:0 0 0 15px; margin-bottom:4px; background:url(/content/images/dot-g.gif) no-repeat left center;} -.boxB .body {border:1px solid #FFFF88; padding:8px; background:#FFF; font-size:13px; line-height:160%;} +.boxB {background:#F9F7ED; padding:6px; margin-bottom:8px;border:solid 1px #aaaaaa;} +.boxB H3 {font-size:13px; font-weight:800; color:#000; margin:0; padding:0 0 0 15px; margin-bottom:4px; background:url(../images/dot-g.gif) no-repeat left center;} +.boxB .body {border:1px solid #aaaaaa; padding:8px; background:#FFF; font-size:13px; line-height:160%;} .boxB .more {padding:1px; text-align:right; font-weight:800;} -.boxC {background:#F5F5F5; padding:6px; margin-bottom:8px;} +.boxC { + background: #cacdc6;/*f9f7ed;*/ + padding:10px; + margin-bottom:8px; + border-top:1px solid #eeeeec; + border-left:1px solid #eeeeec; + border-right:1px solid #a9aca5; + border-bottom:1px solid #babdb6; +} +.boxC p { + margin-bottom:8px; +} +.boxC p.nomargin { + margin:0px; +} +.boxC p.info-box-follow-up-links { + text-align:right; + margin:0; +} /*分页*/ .pager {margin-top:10px; margin-bottom:16px; float:left;} .pagesize {margin-top:10px; margin-bottom:16px; float:right;} @@ -197,7 +271,7 @@ blockquote border:1px solid #fff; background-color:#fff; color:#777; - padding:.3em; + padding:2px 4px 3px 4px; font:bold 100% sans-serif; } @@ -248,37 +322,46 @@ blockquote /*标签*/ .tag {font-size:13px; font-weight:normal; color:#333; text-decoration:none;background-color:#EEE; border-left:3px solid #777; border-top:1px solid #EEE; border-bottom:1px solid #CCC; border-right:1px solid #CCC; padding:1px 8px 1px 8px;} .tags {font-family:sans-serif; line-height:200%; display:block; margin-top:5px;} -.tags a {font-size:13px; font-weight:normal; color:#333; text-decoration:none;background-color:#EEE; border-left:3px solid #777; border-top:1px solid #EEE; border-bottom:1px solid #CCC; border-right:1px solid #CCC; padding:1px 8px 1px 8px;} +.tags a {white-space: nowrap; font-size:13px; font-weight:normal; color:#333; text-decoration:none;background-color:#EEE; border-left:3px solid #777; border-top:1px solid #EEE; border-bottom:1px solid #CCC; border-right:1px solid #CCC; padding:1px 8px 1px 8px;} .tags a:hover {background-color:#fFF;color:#333;} .tagsbox {line-height:200%;} .tagsbox a {font-size:13px; font-weight:normal; color:#333; text-decoration:none;background-color:#EEE; border-left:3px solid #777; border-top:1px solid #EEE; border-bottom:1px solid #CCC; border-right:1px solid #CCC; padding:1px 8px 1px 8px;} .tagsbox a:hover {background-color:#fFF;color:#333;} .tag-number {font-weight:700;font-family:sans-serif;} +.marked-tags { margin-top: 0px;margin-bottom: 5px; } +.deletable-tag { margin-right: 3px; white-space:nowrap; } /*奖牌*/ -a.medal { font-size:14px; line-height:250%; font-weight:800; color:#333; text-decoration:none; background:url(/content/images/medala.gif) no-repeat; border-left:1px solid #EEE; border-top:1px solid #EEE; border-bottom:1px solid #CCC; border-right:1px solid #CCC; padding:4px 12px 4px 6px;} -a:hover.medal {color:#333; text-decoration:none; background:url(/content/images/medala_on.gif) no-repeat; border-left:1px solid #E7E296; border-top:1px solid #E7E296; border-bottom:1px solid #D1CA3D; border-right:1px solid #D1CA3D;} +a.medal { font-size:14px; line-height:250%; font-weight:800; color:#333; text-decoration:none; background:url(../images/medala.gif) no-repeat; border-left:1px solid #EEE; border-top:1px solid #EEE; border-bottom:1px solid #CCC; border-right:1px solid #CCC; padding:4px 12px 4px 6px;} +a:hover.medal {color:#333; text-decoration:none; background:url(../images/medala_on.gif) no-repeat; border-left:1px solid #E7E296; border-top:1px solid #E7E296; border-bottom:1px solid #D1CA3D; border-right:1px solid #D1CA3D;} /*Tab栏*/ -.tabBar{background-color:#FFF;border-bottom: 1px solid #666;height: 30px; width: 100%;clear:both; margin-bottom:3px;} +.tabBar{background-color:#FFF;border-bottom: 1px solid white;height: 30px; width: 100%;clear:both; margin-bottom:3px;} .tabsA {background-color:#FFF;float:right;position:relative;display:block;font-weight:bold;height:20px;} .tabsB {background-color:#FFF;float:left;position:relative;display:block;font-weight:bold;height:20px;} -.tabsA a.on, .tabsA a.on:hover,.tabsB a.on, .tabsB a.on:hover {background: #fff; - color:#333; - border: 1px solid #777; - border-bottom:2px solid #FFF; - height: 25px; +.tabsA a.on, .tabsA a:hover,.tabsB a.on, .tabsB a:hover { + background: #fff; + color:#a40000; + border-top:1px solid #babdb6; + border-left:1px solid #babdb6; + border-right:1px solid #888a85; + border-bottom:1px solid #888a85; + height: 24px; line-height: 26px; margin-top: 3px; padding: 0px 11px 0px 11px;} -.tabsA a {background: #eee; - border: 1px solid #eee; - color: #777; +.tabsA a { + background: #f9f7eb; + border-top:1px solid #eeeeec; + border-left:1px solid #eeeeec; + border-right:1px solid #a9aca5; + border-bottom:1px solid #888a85; + color: #888a85; display: block; float: left; - height: 22px; - line-height: 28px; + height: 20px; + line-height: 22px; margin: 5px 4px 0 0; padding: 0 11px 0 11px; text-decoration: none; @@ -294,19 +377,28 @@ a:hover.medal {color:#333; text-decoration:none; background:url(/content/images padding: 0 11px 0 11px; text-decoration: none; } -.tabsA a:hover, .tabsB a:hover {background: #fff;border: 1px solid #777;border-bottom:3px solid #FFF;} +/*.tabsA a:hover, .tabsB a:hover {background: #fff;border: 1px solid #777;border-bottom:3px solid #FFF;}*/ .headlineA {font-size:13px; border-bottom:1px solid #777; padding-bottom:2px; font-weight:800; margin-bottom:12px; text-align:right; height:30px;} -.headQuestions {float:left; height:23px; line-height:23px; margin:5px 0 0 5px;padding:0px 6px 0px 15px; font-size:15px; font-weight:700; border-bottom:0px solid #777; border-left:0px solid #darkred; background-color:#FFF;background:url(/content/images/dot-list.gif) no-repeat left center;} -.headAnswers {float:left; padding:3px; font-size:18px; font-weight:800; background:url(/content/images/ico_answers.gif) left 2px no-repeat; padding-left:24px;} -.headTags {float:left; padding:3px; font-size:18px; font-weight:800; background:url(/content/images/ico_tags.gif) no-repeat; padding-left:24px;} -.headUsers {float:left; height:23px; line-height:23px; margin:5px 0 0 5px;padding:0px 6px 0px 15px; font-size:15px; font-weight:700; border-bottom:0px solid #777; border-left:0px solid #darkred; background-color:#FFF;background:url(/content/images/dot-list.gif) no-repeat left center;} -.headMedals {float:left; height:23px; line-height:23px; margin:5px 0 0 5px;padding:0px 6px 0px 15px; font-size:15px; font-weight:700; border-bottom:0px solid #777; border-left:0px solid #darkred; background-color:#FFF;background:url(/content/images/dot-list.gif) no-repeat left center;} -.headLogin {float:left; padding:3px; font-size:15px; font-weight:800; background:url(/content/images/ico_login.gif) no-repeat; padding-left:24px;} -.headNormal {text-align:left;padding:3px; font-size:15px; margin-bottom:12px; font-weight:800;border-bottom:1px solid #777;} -.headUser {text-align:left;padding:5px; font-size:20px; letter-spacing:1px;margin-bottom:12px; font-weight:800;border-bottom:1px solid #777;} +.headQuestions {float:left; height:23px; line-height:23px; margin:5px 0 0 5px;padding:0px 6px 0px 15px; font-size:15px; font-weight:700; border-bottom:0px solid #777; border-left:0px solid #darkred; background-color:#FFF;background:url(../images/dot-list.gif) no-repeat left center;} +.headAnswers {float:left; padding:3px; font-size:18px; font-weight:800; background:url(../images/ico_answers.gif) left 2px no-repeat; padding-left:24px;} +.headTags {float:left; padding:3px; font-size:18px; font-weight:800; background:url(../images/ico_tags.gif) no-repeat; padding-left:24px;} +.headUsers {float:left; height:23px; line-height:23px; margin:5px 0 0 5px;padding:0px 6px 0px 15px; font-size:15px; font-weight:700; border-bottom:0px solid #777; border-left:0px solid #darkred; background-color:#FFF;background:url(../images/dot-list.gif) no-repeat left center;} +.headMedals {float:left; height:23px; line-height:23px; margin:5px 0 0 5px;padding:0px 6px 0px 15px; font-size:15px; font-weight:700; border-bottom:0px solid #777; border-left:0px solid #darkred; background-color:#FFF;background:url(../images/dot-list.gif) no-repeat left center;} +.headLogin {float:left; padding:3px; font-size:15px; font-weight:800; background:url(../images/ico_login.gif) no-repeat; padding-left:24px;} +.headNormal { + text-align:left; + padding:3px; + font-size:15px; + margin-bottom:12px; + font-weight:bold; + border-bottom: 1px solid #777; +} +.headUser {text-align:left;padding:5px; font-size:20px; + /*letter-spacing:1px;*/ + margin-bottom:12px; font-weight:800;border-bottom:1px solid #777;} /*RSS订阅*/ #feeds {margin:10px 0; } -#feeds a {background:url(/content/images/feed-icon-small.png) no-repeat 0; padding-left:18px; font-weight:700; font-size:13px; } +#feeds a {background:url(../images/feed-icon-small.png) no-repeat 0; padding-left:18px; font-weight:700; font-size:13px; } /*问题*/ #question {margin-bottom:30px;} @@ -354,9 +446,9 @@ a:hover.medal {color:#333; text-decoration:none; background:url(/content/images font-weight:bold; color:#777; } -.question-img-upvote:hover{background:url(/content/images/vote-arrow-up-on.png)} -.question-img-downvote:hover{background:url(/content/images/vote-arrow-down-on.png)} -.question-img-favorite:hover{background:url(/content/images/vote-favorite-on.png)} +.question-img-upvote:hover{background:url(../images/vote-arrow-up-on.png)} +.question-img-downvote:hover{background:url(../images/vote-arrow-down-on.png)} +.question-img-favorite:hover{background:url(../images/vote-favorite-on.png)} .favorite-number{padding:0px;font-size:100%; font-family:Arial;font-weight:bold;color:#777;} .vote-notification { @@ -404,7 +496,7 @@ a:hover.medal {color:#333; text-decoration:none; background:url(/content/images cursor:pointer; } -.action-link a:hover{ +.action-link: a hover{ background-color:#777; text-decoration:none; color:#fff; @@ -424,8 +516,11 @@ div.comments { } div.post-comments{ + clear:both; + background: url(../images/gray-up-arrow-h18px.png) no-repeat; width:100%; - margin-bottom:10px; + padding-left: 12px; + margin:3px 0 10px 0; } form.post-comments textarea { @@ -441,6 +536,7 @@ form.post-comments input { } span.text-counter { margin-right:20px; + font-size:11px; } span.form-error { @@ -453,13 +549,7 @@ p.form-item { } div.comments-container, div.comments-container-accepted, div.comments-container-owner, div.comments-container-deleted { - display:none; - margin-top:-1px; - padding:0 5px 5px; -} - -div.comments-container, a.comments-link { - background-color:#EEEEEE; + padding:0; } .post-comments a { @@ -469,7 +559,9 @@ div.comments-container, a.comments-link { a.comments-link, a.comments-link-accepted, a.comments-link-owner, a.comments-link-deleted { color:black; - padding:2px; + font-size:11px; + background: #eeeeee; + padding:3px; cursor:pointer; } @@ -481,7 +573,7 @@ a.comments-link, a.comments-link-accepted, a.comments-link-owner, a.comments-lin a.comment-user, a.comment-user:hover { background-color:inherit; - color:#0077CC; + color:blue; padding:0; } @@ -493,6 +585,7 @@ a.comment-user:hover { .answer{ padding-top:10px; width: 100%; + border-bottom:1px solid #ccccce; } .answer-body{ min-height:80px; @@ -555,7 +648,7 @@ a.comment-user:hover { color: #E1E818; } -.answer-img-accept:hover{background:url(/content/images/vote-accepted-on.png)} +.answer-img-accept:hover{background:url(../images/vote-accepted-on.png)} .deleted{ background:#F4E7E7 none repeat scroll 0 0; @@ -574,21 +667,44 @@ a.comment-user:hover { .list-item LI{list-style-type:disc; font-size:13px; line-height:20px; margin-bottom:10px;} /* openid styles */ .form-row{line-height:25px;} +table.form-as-table { + margin-top:5px; +} +table.form-as-table ul { + list-style-type:none; + display: inline; +} +table.form-as-table li { + display: inline; +} +table.form-as-table td { + text-align:right; +} +table.form-as-table th { + text-align:left; + font-weight:normal; +} +/*.form-row li label { + display: inline +}*/ .submit-row{line-height:30px;padding-top:10px;} .errors{line-height:20px;color:red;} -.error{color:red;} +.error{ + color:darkred; + margin:0; + font-size: 10px; +} .error-list li{padding:5px;} -.login{margin-bottom:10px;} .fieldset{ /* border:solid 1px #777;*/ border: none; margin-top:10px; padding:10px; } -.openid-input{background:url(/content/images/openid.gif) no-repeat;padding-left:15px;cursor:pointer;} +.openid-input{background:url(../images/openid.gif) no-repeat;padding-left:15px;cursor:pointer;} .openid-login-input{ background-position:center left; - background:url(/content/images/openid.gif) no-repeat 0% 50%; + background:url(../images/openid.gif) no-repeat 0% 50%; padding:5px 5px 5px 15px; cursor:pointer; font-family:Trebuchet MS; @@ -626,7 +742,7 @@ span.form-error { margin-left:5px; } .title-desc{ - color:#999; + color:#666666; font-size:90%; } @@ -704,6 +820,8 @@ span.form-error { .revision .summary span{ background-color:yellow; + padding-left:3px; + padding-right:3px; display:inline; } .revision h1{ @@ -715,6 +833,7 @@ span.form-error { .revision-mark{ width:200px; text-align:left; + display:inline-block; font-size:90%; overflow:hidden; } @@ -744,13 +863,13 @@ background-color: #97ff97; } /*用户资料页面*/ -.count {font-family:Arial;font-size:24px;font-weight:700;color:#777} +.count {font-family:Arial;font-size:200%;font-weight:700;color:#777} .scoreNumber{font-family:Arial;font-size:35px;font-weight:800;color:#777;line-height:40px; /*letter-spacing:0px*/ } .user-details{font-size:13px;} .user-about{background-color:#EEEEEE;height:200px;line-height:20px; overflow:auto;padding:10px;width:90%;} -.user-edit-link {background:url(/content/images/edit.png) no-repeat; padding-left:20px; font-weight:600;} +.user-edit-link {background:url(../images/edit.png) no-repeat; padding-left:20px;} .favorites-count-off { color:#919191; float:left; @@ -814,7 +933,8 @@ width:950;margin-bottom:10px; } .narrow .summary { - width:620px; + width:600px; + display:inline-block; } .narrow .summary h3 { @@ -823,8 +943,8 @@ width:950;margin-bottom:10px; } .narrow .views { - float:left; height:42px; + float:left; margin:0 7px 0 0; /*padding:5px 0 5px 4px;*/ padding: 5px; @@ -850,11 +970,22 @@ width:950;margin-bottom:10px; .narrow .vote-count-post { font-weight:800; + display:block; margin:0; font-size: 190%; color:#555; line-height:20px; } -.narrow .answer-count-post{font-weight:800;margin:0; font-size: 190%; } -.narrow .views-count-post{font-weight:800;margin:0; font-size: 190%;} +.narrow .answer-count-post{ + font-weight:800; + display:block; + margin:0; + font-size: 190%; +} +.narrow .views-count-post{ + font-weight:800; + display:block; + margin:0; + font-size: 190%; +} div.started { color:#999999; float:right; @@ -885,6 +1016,7 @@ div.started .reputation-score { .narrow .tags{float:left;} .answer-summary { + display:block; clear:both; padding:3px; } @@ -1008,7 +1140,7 @@ div.started .reputation-score { a.comment {background:#EEE; color:#993300; padding:4px;} a.permLink {padding:2px;} a.offensive {color:#999;} -ul.bulleta li {background:url(/content/images/bullet_green.gif) no-repeat 0px 2px; padding-left:16px; margin-bottom:4px;} +ul.bulleta li {background:url(../images/bullet_green.gif) no-repeat 0px 2px; padding-left:16px; margin-bottom:4px;} .user {padding:5px; line-height:140%; width:170px;} .user ul {margin:0; list-style-type:none;} .user .thumb{clear:both;float:left; margin-right:4px; display:inline;} @@ -1027,92 +1159,291 @@ ul.bulleta li {background:url(/content/images/bullet_green.gif) no-repeat 0px 2p .message p { margin-bottom:0px; } - -.warning{color:red;} -.darkred{color:darkred;} -.submit{ - cursor:pointer; - /*letter-spacing:1px;*/ - background-color:#D4D0C8; - height:40px; - border:1px solid #777; -/* width:100px; */ - font-weight:bold; - font-size:120%;} -.submit:hover{text-decoration:underline;} -.ask-body{padding-right:10px;} -.thousand{color:orange;} -.notify -{ - position: fixed; - top: 0px; - left: 0px; - width: 100%; - z-index: 100; - padding: 0; - text-align: center; - font-weight: Bold; - color: #444; - background-color: #F4A83D; -} -.notify p { - margin-top:5px; - margin-bottom:5px; - font-size:16px; -} -#close-notify -{ - position:absolute; - right:5px; - top:5px; - padding:0 3px 0 3px; - color: #735005; - text-decoration: none; - font-size:14px; - line-height:18px; - background-color: #FAD163; - border: 2px #735005 solid; - cursor:pointer; -} -#close-notify:hover { - text-decoration:none; +.message p.space-above { + margin-top:10px; } -.big { - font-size:15px; -} -.bigger { - font-size:14px; -} -.strong { - font-weight:bold; -} -.orange -{ - color:#d64000; - font-weight:bold; -} -.grey { - color:#808080; -} -.about div { - padding:10px 5px 10px 5px; - border-top:1px dashed #aaaaaa; -} -.about div.first { - padding-top:0; - border-top:none; -} -.about p { - margin-bottom:10px; -} -.about a {color:#d64000;text-decoration:underline;} -.about h3{ - line-height:30px; - font-size:15px; - font-weight:700; - padding-top: 0px; -} -.highlight { - background-color:#FFF8C6; -} + .warning{color:red;} + .darkred{color:darkred;} + .submit{ + cursor:pointer; + /*letter-spacing:1px;*/ + background-color:#D4D0C8; + height:40px; + border:1px solid #777777; + /* width:100px; */ + font-weight:bold; + padding-bottom:4px; + font-size:120%;} + .submit:hover{text-decoration:underline;} + .ask-body{padding-right:10px;} + .thousand{color:orange;} + + .notify + { + position: fixed; + top: 0px; + left: 0px; + width: 100%; + z-index: 100; + padding: 0; + text-align: center; + font-weight: Bold; + color: #444; + background-color: #F4A83D; + } + + .notify p { + margin-top:5px; + margin-bottom:5px; + font-size:16px; + } + + #close-notify + { + position:absolute; + right:5px; + top:5px; + padding:0 3px 0 3px; + color: #735005; + text-decoration: none; + font-size:14px; + line-height:18px; + background-color: #FAD163; + border: 2px #735005 solid; + cursor:pointer; + } + #close-notify:hover { + text-decoration:none; + } + + .big { + font-size:15px; + } + .bigger { + font-size:14px; + } + .strong { + font-weight:bold; + } + .orange + { + color:#d64000; + font-weight:bold; + } + .grey { + color:#808080; + } + .about div { + padding:10px 5px 10px 5px; + border-top:1px dashed #aaaaaa; + } + .about div.first { + padding-top:0; + border-top:none; + } + .about p { + margin-bottom:10px; + } + .about a {color:#d64000;text-decoration:underline;} + .about h3{ + line-height:30px; + font-size:15px; + font-weight:700; + padding-top: 0px; + } + .highlight { + background-color:#FFF8C6; + } + .nomargin { + margin:0; + } + .margin-bottom { + margin-bottom: 10px; + } + .inline-block { + display:inline-block; + } + .action-status { + margin:0; + border:none; + text-align:center; + line-height:10px; + font-size:12px; + padding:0; + } + .action-status span { + padding:3px 5px 3px 5px; + background-color:#fff380;/* nice yellow */ + font-weight:normal; + -moz-border-radius: 5px; + -khtml-border-radius: 5px; + -webkit-border-radius: 5px; + } + .tight { + margin:0; + padding:0; + } + + .list-table td { + vertical-align:top; + } + + p.comment { + border-top: 1px dotted #ccccce; + margin:0; + font-size:11px; + color: #444444; + padding:5px 0 5px 0; + } + + .delete-icon { + vertical-align:middle; + padding-left:3px; + } + /* these need to go */ + table.form-as-table .errorlist { + display: block; + margin:0; + padding:0 0 0 5px; + text-align:left; + font-size:10px; + color:darkred; + } + table.form-as-table input { + display: inline; + margin-left: 4px; + } + table.form-as-table th { + vertical-align:bottom; + padding-bottom:4px; + } + .form-row-vertical { + margin-top: 8px; + display: block; + } + .form-row-vertical label { + margin-bottom:3px; + display:block; + } + /* above stuff needs to go */ + .text-align-right { + text-align: center; + } + ul.form-horizontal-rows { + list-style:none; + margin:0; + } + ul.form-horizontal-rows li { + position:relative; + height:40px; + } + ul.form-horizontal-rows label { + display:inline-block; + } + ul.form-horizontal-rows ul.errorlist { + list-style:none; + color:darkred; + font-size:10px; + line-height:10px; + position:absolute; + top:2px; + left:180px; + text-align:left; + margin:0; + } + ul.form-horizontal-rows ul.errorlist li { + height:10px; + } + ul.form-horizontal-rows label { + position:absolute; + left:0px; + bottom:6px; + margin:0px; + line-height: 12px; + font-size: 12px; + } + ul.form-horizontal-rows li input { + position:absolute; + bottom:0px; + left:180px; + margin:0px; + } + #emailpw-form li input { + left:170px; + } + #emailpw-form ul.errorlist { + left:170px; + } + #changepw-form li input { + left:150px; + } + #changepw-form ul.errorlist { + left:150px; + } + .narrow .summary { + float: left; + } + .narrow .summary .question-title { + font-weight: bold; + font-size: 120%; + } + .user-profile-tool-links { + padding-bottom:10px; + font-weight: bold; + } + .post-controls { + float:left; + font-size:11px; + line-height:12px; + min-width:200px; + margin-bottom:5px; + } + #question-controls .tags { + margin:0 0 3px 0; + } + .post-update-info-container { + float: right; + min-width:190px; + } + .post-update-info { + display:inline-block; + float:right; + width:190px; + margin-bottom:5px; + } + .post-update-info p { + font-size:11px; + line-height:15px; + margin:0 0 4px 0; + padding:0; + } + .post-update-info img { + float: left; + width: 32px; + margin: 4px 8px 0 0; + } + .comments-container { + clear:both; + } + .admin { + background-color:#fff380;/* nice yellow */ + border: 1px solid darkred; + padding: 0 5px 0 5px; + } + .admin p { + margin-bottom: 3px; + } + .admin #action_status { + text-align:center; + font-weight:bold; + } + #tagSelector { + padding-bottom: 2px; + } + #hideIgnoredTagsControl { + margin: 5px 0 0 0; + } + #hideIgnoredTagsCb { + margin: 0 2px 0 1px; + } diff --git a/templates/edit_user_email_feeds_form.html b/templates/edit_user_email_feeds_form.html new file mode 100644 index 00000000..65902e7e --- /dev/null +++ b/templates/edit_user_email_feeds_form.html @@ -0,0 +1,4 @@ +{% load i18n %} +<table class='form-as-table'> +{{email_feeds_form.as_table}} +</table> diff --git a/templates/faq.html b/templates/faq.html index aec37a56..236f4f76 100644 --- a/templates/faq.html +++ b/templates/faq.html @@ -72,10 +72,12 @@ <td style="text-align:right;padding-right:5px"><strong>500</strong></td> <td>{% trans "retag questions" %}</td> </tr> + {% if settings.WIKI_ON %} <tr> <td style="text-align:right;padding-right:5px"><strong>750</strong></td> <td>{% trans "edit community wiki questions" %}</td> </tr> + {% endif %} <tr> <td style="text-align:right;padding-right:5px"><strong>2000</strong></td> <td>{% trans "edit any answer" %}</td> @@ -99,7 +101,7 @@ <div> <a id='validate'></a><h3 class="subtitle">{% trans "how to validate email title" %}</h3> <!--special case here message must contain paragraphs--> - {% trans "how to validate email info" %} + {% blocktrans %}how to validate email info with {{send_email_key_url}} {{gravatar_faq_url}}{% endblocktrans %} </div> {% endifequal %} <div> @@ -121,7 +123,7 @@ </div> <div> <h3 class="subtitle">{% trans "Still have questions?" %}</h3> - <p>{% trans "Please ask your question, help make our community better!" %} + <p>{% blocktrans %}Please ask your question at {{ask_question_url}}, help make our community better!{% endblocktrans %} <!-- <a href="{% url tags %}faq" class="big">{{ settings.APP_TITLE }} {% trans "questions" %}</a>{% trans "." %} --> diff --git a/templates/feedback.html b/templates/feedback.html new file mode 100644 index 00000000..38bb48ff --- /dev/null +++ b/templates/feedback.html @@ -0,0 +1,55 @@ +{% extends "base_content.html" %} +<!-- template about.html --> +{% load i18n %} +{% load extra_tags %} +{% load humanize %} +{% block title %}{% spaceless %}{% trans "Feedback" %}{% endspaceless %}{% endblock %} +{% block forejs %} +{% endblock %} +{% block content %} +<div class="headNormal"> +{% trans "Give us your feedback!" %} +</div> +<div class="content"> + <form method="post" action="{% url feedback %}" accept-charset="utf-8"> + {% if user.is_authenticated %} + <p class="message"> + {% blocktrans with user.username as user_name %} + <span class='big strong'>Dear {{user_name}}</span>, we look forward to hearing your feedback. + Please type and send us your message below. + {% endblocktrans %} + <p> + {% else %} + <p class="message"> + {% blocktrans %} + <span class='big strong'>Dear visitor</span>, we look forward to hearing your feedback. + Please type and send us your message below. + {% endblocktrans %} + </p> + <div class="form-row"><label>{{form.name.label}}</label><br/>{{form.name}}</div> + <div class="form-row"> + <label>{{form.email.label}} + {% if form.errors.email %} + <span class='red'>(please enter a valid email)</span> + {% endif %} + </label><br/>{{form.email}} + </div> + {% endif %} + <div class="form-row"> + <label>{{form.message.label}} + {% if form.errors.message %} + <span class="red">{% trans "(this field is required)" %}</span> + </label> + {% endif %} + <br/> + {{form.message}} + </div> + {{form.next}} + <div class="submit-row"> + <input type="submit" class="submit" value="{% trans "Send Feedback" %}"/> + <input type="submit" class="submit" name="cancel" value="{% trans "Cancel" %}"/> + </div> + </form> +</div> +{% endblock %} +<!-- end template about.html --> diff --git a/templates/feedback_email.txt b/templates/feedback_email.txt new file mode 100644 index 00000000..df768180 --- /dev/null +++ b/templates/feedback_email.txt @@ -0,0 +1,19 @@ +{% load i18n %} +{% spaceless %} +{% blocktrans with settings.APP_TITLE|safe as site_title %} +Hello, this is a {{site_title}} forum feedback message +{% endblocktrans %} +{% endspaceless %} + +{% spaceless %} +{% trans "Sender is" %} +{% if user.is_authenticated %} + {{user.username|safe}} {% trans "email" %}:{{user.email|safe}} +{% else %} + {% if name %}{{name|safe}}{% else %}{% trans "anonymous" %}{% endif %} + {% if email %}{% trans "email" %}:{{email|safe}}{% endif %} +{% endif %} + ip:{{request.META.REMOTE_ADDR}} +{% endspaceless %} + +{% trans "Message body:" %} {{message|safe}} diff --git a/templates/footer.html b/templates/footer.html index 34064fd5..9d19b41e 100644 --- a/templates/footer.html +++ b/templates/footer.html @@ -3,33 +3,47 @@ {% load i18n %} <!-- 页面底部开始: --> <div id="ground"> - <div class="footerLinks" > - <a href="{% url about %}">{% trans "about" %}</a><span class="link-separator"> |</span> - <a href="{% url faq %}">{% trans "faq" %}</a><span class="link-separator"> |</span> - <a href="{{ blog_url }}">{% trans "blog" %}</a><span class="link-separator"> |</span> - <a href="{{ webmaster_email }}">{% trans "contact us" %}</a><span class="link-separator"> |</span> - <a href="{% url privacy %}">{% trans "privacy policy" %}</a><span class="link-separator"> |</span> - <a href="{{ feedback_url }}" target="_blank">{% trans "give feedback" %}</a> - </div> - <!--<p style="margin-top:10px;"> - <a href="http://code.google.com/p/cnprog/" target="_blank"> - <img src="/content/images/djangomade124x25_grey.gif" border="0" alt="Made with Django." title="Made with Django." /> - </a> - <div style="font-size:90%;color:#333">{% trans "current revision" %}: R-0120-20090406</div> - </p>--> - <p id="licenseLogo"><img src="/content/images/cc-wiki.png" title="Creative Commons: Attribution - Share Alike" alt="cc-wiki" width="50" height="68" /></p> + <div> + <div class="footerLinks" > + <a href="{% url about %}">{% trans "about" %}</a><span class="link-separator"> |</span> + <a href="{% url faq %}">{% trans "faq" %}</a><span class="link-separator"> |</span> + <!--<a href="{{ blog_url }}">{% trans "blog" %}</a><span class="link-separator"> |</span>--> + <!--<a href="{{ webmaster_email }}">{% trans "contact us" %}</a><span class="link-separator"> |</span>--> + <a href="{% url privacy %}">{% trans "privacy policy" %}</a><span class="link-separator"> |</span> + {% spaceless %} + <a href= + {% if settings.FEEDBACK_SITE_URL %} + "{{settings.FEEDBACK_SITE_URL}}" + target="_blank"> + {% else %} + "{% url feedback %}?next={{request.path}}"> + {% endif %} + {% trans "give feedback" %} + </a> + {% endspaceless %} + </div> + <p> + <a href="http://github.com/cnprog/CNPROG/network" target="_blank"> + powered by cnprog platform + <!--<img src="{% href "/content/images/djangomade124x25_grey.gif" %}" border="0" alt="Made with Django." title="Made with Django." >--> + </a> + </p> + </div> + <div id="licenseLogo"> + <a href="http://creativecommons.org/licenses/by/3.0/"> + <img src="{% href "/content/images/cc-wiki.png" %}" title="Creative Commons: Attribution - Share Alike" alt="cc-wiki" width="50" height="68" /> + </a> + </div> </div> <!-- 页面底部结束: --> - {% if settings.GOOGLE_ANALYTICS_KEY %} <script type="text/javascript"> var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www."); document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E")); </script> <script type="text/javascript"> try { - var pageTracker = _gat._getTracker("{{ settings.GOOGLE_ANALYTICS_KEY }}"); + var pageTracker = _gat._getTracker('{{ settings.GOOGLE_ANALYTICS_KEY }}'); pageTracker._trackPageview(); } catch(err) {} </script> - {% endif %} -<!-- end footer.html --> +<!-- end template footer.html --> diff --git a/templates/header.html b/templates/header.html index c9b01a20..ede6cce5 100644 --- a/templates/header.html +++ b/templates/header.html @@ -4,23 +4,21 @@ <div id="roof"> <div id="navBar"> <div id="top"> - <!--<div id="header">--> - {% if request.user.is_authenticated %} - <a href="{% url users %}{{ request.user.id }}/{{ request.user.username }}/">{{ request.user.username }}</a> {% get_score_badge request.user %} - <a href="{% url logout %}">{% trans "logout" %}</a> - {% else %} - <a href="{% url user_signin %}">{% trans "login" %}</a> - {% endif %} - <a href="{% url about %}">{% trans "about" %}</a> - <a href="{% url faq %}">{% trans "faq" %}</a> - <!--</div>--> + {% if request.user.is_authenticated %} + <a href="{% url users %}{{ request.user.id }}/{{ request.user.username }}/">{{ request.user.username }}</a> {% get_score_badge request.user %} + <a href="{% url logout %}">{% trans "logout" %}</a> + {% else %} + <a href="{% url user_signin %}">{% trans "login" %}</a> + {% endif %} + <a href="{% url about %}">{% trans "about" %}</a> + <a href="{% url faq %}">{% trans "faq" %}</a> </div> <table width="100%" border="0" cellspacing="0" cellpadding="0"> <tr> <td width="23%"> <div id="logo"> - <a href="/"> - <img src="/content/images/logo.png" title="{% trans "back to home page" %}" alt="{{settings.APP_TITLE}} logo"/> + <a href="{% url index %}"> + <img src="{% href "/content/images/logo.png" %}" title="{% trans "back to home page" %}" alt="{{settings.APP_TITLE}} logo"/> </a> </div> </td> @@ -60,9 +58,9 @@ </div> <div class="options"> <input id="type-question" type="radio" value="question" name="t" - checked="checked" />{% trans "questions" %} - <input id="type-tag" type="radio" value="tag" name="t" />{% trans "tags" %} - <input id="type-user" type="radio" value="user" name="t" />{% trans "users" %} + checked="checked" /><label for="type-question">{% trans "questions" %}</label> + <input id="type-tag" type="radio" value="tag" name="t" /><label for="type-tag">{% trans "tags" %}</label> + <input id="type-user" type="radio" value="user" name="t" /><label for="type-user">{% trans "users" %}</label> </div> </form> </td> diff --git a/templates/index.html b/templates/index.html index bc83b637..b920db1b 100644 --- a/templates/index.html +++ b/templates/index.html @@ -4,18 +4,21 @@ {% load extra_tags %} {% load humanize %} {% load extra_filters %} +{% load smart_if %} {% block title %}{% spaceless %}{% trans "Home" %}{% endspaceless %}{% endblock %} {% block meta %}<meta name="keywords" content="{{ settings.APP_KEYWORDS }}" /> <meta name="description" content="{{ settings.APP_DESCRIPTION }}" />{% endblock %} {% block forejs %} <script type="text/javascript"> - $().ready(function(){ - var tab_id = "{{ tab_id }}"; - $("#"+tab_id).attr('className',"on"); - $("#nav_questions").attr('className',"on"); - }); - - </script> + var tags = {{ tags_autocomplete|safe }}; + $().ready(function(){ + var tab_id = "{{ tab_id }}"; + $("#"+tab_id).attr('className',"on"); + $("#nav_questions").attr('className',"on"); + }); + </script> + <script type='text/javascript' src='{% href "/content/js/com.cnprog.editor.js" %}'></script> + <script type='text/javascript' src='{% href "/content/js/com.cnprog.tag_selector.js" %}'></script> {% endblock %} {% block content %} <div class="tabBar"> @@ -33,30 +36,31 @@ {% for question in questions %} <div class="qstA"> <h2> - <a href="{{ question.get_absolute_url }}" title="{{ question.summary|collapse }}..."> - {{ question.get_question_title }} - </a> + <a href="{{ question.get_absolute_url }}">{{ question.get_question_title }}</a> </h2> <div class="stat"> <table> - <tr> + <tr> <td><span class="num">{{ question.answer_count|intcomma }}</span> </td> <td><span class="num">{{ question.score|intcomma }}</span> </td> <td><span class="num">{{ question.view_count|cnprog_intword|safe }}</span> </td> - </tr> + </tr> <tr> <td><span class="unit">{% trans "answers" %}</span></td> <td><span class="unit">{% trans "votes" %}</span></td> <td><span class="unit">{% trans "views" %}</span></td> - </tr> + </tr> </table> </div> + <div class="summary"> - {{ question.summary}}... + {{ question.summary }}... </div> - {% if question.wiki %} + + {% ifequal tab_id 'active'%} + {% if question.wiki and settings.WIKI_ON %} <span class="from wiki">{% trans "community wiki" %}</span> - <span class="date" title="{{ question.last_activity_at }}">{% diff_date question.last_activity_at %}</span> + <span class="date" title="{{ question.added_at }}">{% diff_date question.added_at %}</span> {% else %} <div class="from"> {% comment %}{% gravatar question.last_activity_by 24 %}{% endcomment %} @@ -65,6 +69,38 @@ <span class="date" title="{{ question.last_activity_at }}">{% diff_date question.last_activity_at %}</span> </div> {% endif %} + {% else %} + {% if question.wiki and settings.WIKI_ON %} + <span class="from wiki">{% trans "community wiki" %}</span> + <span class="date" title="{{ question.added_at }}">{% diff_date question.added_at %}</span> + {% else %} + <div class="from"> + {% comment %}{% gravatar question.author 24 %}{% endcomment %} + {% if question.last_activity_at != question.added_at %} + {% if question.author.id != question.last_activity_by.id %} + {% trans "Posted:" %} + <span class="author"><a href="{{ question.author.get_profile_url }}">{{ question.author }}</a></span> + <span class="score">{% get_score_badge question.author %} </span> + / {% trans "Updated:" %} + <span class="author"><a href="{{ question.last_activity_by.get_profile_url }}">{{ question.last_activity_by }}</a></span> + <span class="score">{% get_score_badge question.last_activity_by %} </span> + <span class="date" title="{{ question.last_activity_at }}">{% diff_date question.last_activity_at %}</span> + {% else %} + {% trans "Updated:" %} + <span class="author"><a href="{{ question.last_activity_by.get_profile_url }}">{{ question.last_activity_by }}</a></span> + <span class="score">{% get_score_badge question.last_activity_by %} </span> + <span class="date" title="{{ question.last_activity_at }}">{% diff_date question.last_activity_at %}</span> + {% endif %} + {% else %} + {% trans "Posted:" %} + <span class="author"><a href="{{ question.author.get_profile_url }}">{{ question.author }}</a></span> + <span class="score">{% get_score_badge question.author %} </span> + <span class="date" title="{{ question.added_at }}">{% diff_date question.added_at %}</span> + {% endif %} + </div> + {% endif %} + {% endifequal %} + <div class="tags"> {% for tag in question.tagname_list %} <a href="{% url forum.views.tag tag|urlencode %}" title="{% trans "see questions tagged" %}'{{ tag }}'{% trans "using tags" %}" rel="tag">{{ tag }}</a> @@ -85,8 +121,10 @@ <div class="more"><a href="{% url faq %}">{% trans "faq" %} »</a></div> </div> </div> +{% else %} +{% include "tag_selector.html" %} {% endif %} -<div class="boxB"> +<div class="boxC"> <h3>{% trans "Recent tags" %}</h3> <div class="body"> <div class="tags"> @@ -99,7 +137,7 @@ </div> </div> {% if awards %} -<div class="boxB"> +<div class="boxC"> <h3>{% trans "Recent awards" %}</h3> <div class="body"> <ul class="badge-list"> @@ -116,12 +154,12 @@ </div> {% endif %} <div id="feeds"> -<a href="/feeds/rss" title="{% trans "subscribe to last 30 questions by RSS" %}">{% trans "subscribe to the questions feed" %}</a> +<a href="{% href "/feeds/rss" %}" title="{% trans "subscribe to last 30 questions by RSS" %}">{% trans "subscribe to the questions feed" %}</a> </div> {% endblock %} {% block tail %} <div style="padding:5px 0 5px 5px;"> -<span class="evenMore">{% trans "Still looking for more? See" %} <a href="{% url questions %}">{% trans "complete list of questions" %}</a> {% trans "or" %} <a href="/tags/">{% trans "popular tags" %}</a>{% trans "." %} {% trans "Please help us answer" %} <a href="{% url questions %}unanswered">{% trans "list of unanswered questions" %}</a>{% trans "." %}</span> +<span class="evenMore">{% trans "Still looking for more? See" %} <a href="{% url questions %}">{% trans "complete list of questions" %}</a> {% trans "or" %} <a href="{% url tags %}">{% trans "popular tags" %}</a>{% trans "." %} {% trans "Please help us answer" %} <a href="{% url questions %}unanswered">{% trans "list of unanswered questions" %}</a>{% trans "." %}</span> </div> {% endblock %} <!-- index.html --> diff --git a/templates/logout.html b/templates/logout.html index 2cd935a0..650ba044 100644 --- a/templates/logout.html +++ b/templates/logout.html @@ -8,7 +8,6 @@ <script type="text/javascript"> $().ready(function(){ $('#btLogout').bind('click', function(){ window.location.href='{% url user_signout %}?next={{ next }}'; }); - }); </script> {% endblock %} diff --git a/templates/post_contributor_info.html b/templates/post_contributor_info.html new file mode 100644 index 00000000..9997be5f --- /dev/null +++ b/templates/post_contributor_info.html @@ -0,0 +1,55 @@ +{% load i18n %} +{% load smart_if %} +{% load extra_tags %} +<div class='post-update-info'> +{% ifequal contributor_type "original_author" %} + {% if wiki %} + <p>{% trans "community wiki" %}</p> + <p> + {% blocktrans count post.revisions.all|length as rev_count %} + one revision + {% plural %} + {{rev_count}} revisions + {% endblocktrans %} + </p> + <p>{{post.author.get_profile_link}}</p> + {% else %} + <p style="line-height:12px;"> + {% ifequal post_type "question" %} + {% trans "asked" %} + {% else %} + {% ifequal post_type "answer" %} + {% trans "answered" %} + {% else %} + {% trans "posted" %} + {% endifequal %} + {% endifequal %} + {% ifequal post_type "revision" %} + <strong>{% diff_date post.revised_at %}</strong> + {% else %} + <strong>{% diff_date post.added_at %}</strong> + {% endifequal %} + </p> + {% gravatar post.author 32 %} + <p>{{post.author.get_profile_link}}<br/> + {% get_score_badge post.author %}</p> + {% endif %} +{% else %} + {% if post.last_edited_at %} + <p style="line-height:12px;"> + {% ifequal post_type 'question' %} + <a href="{% url question_revisions post.id %}"> + {% else %} + <a href="{% url answer_revisions post.id %}"> + {% endifequal %} + {% trans "updated" %} <strong>{% diff_date post.last_edited_at %}</strong> + </a> + </p> + {% if post.author != post.last_edited_by or wiki %} + {% gravatar post.last_edited_by 32 %} + <p style="float:left">{{post.last_edited_by.get_profile_link}}<br/> + {% get_score_badge post.last_edited_by %}</p> + {% endif %} + {% endif %} +{% endifequal %} +</div> diff --git a/templates/question.html b/templates/question.html index 7d7c9c4c..9652e3f6 100644 --- a/templates/question.html +++ b/templates/question.html @@ -2,21 +2,23 @@ <!-- question.html --> {% load extra_tags %} {% load extra_filters %} +{% load smart_if %} {% load humanize %} {% load i18n %} {% block title %}{% spaceless %}{{ question.get_question_title }}{% endspaceless %}{% endblock %} {% block forejs %} <meta name="description" content="{{question.summary}}" /> <meta name="keywords" content="{{question.tagname_meta_generator}}" /> - <link rel="canonical" href="{{settings.APP_URL}}{{question.get_absolute_url}}"/> + <link rel="canonical" href="{{settings.APP_URL}}{{question.get_absolute_url}}" /> {% if not question.closed %} - <script type='text/javascript' src='/content/js/com.cnprog.editor.js'></script> - <script type='text/javascript' src='/content/js/wmd/showdown.js'></script> - <script type='text/javascript' src='/content/js/wmd/wmd.js'></script> - <link rel="stylesheet" type="text/css" href="/content/js/wmd/wmd.css" /> + <script type='text/javascript' src='{% href "/content/js/com.cnprog.editor.js" %}'></script> + <script type='text/javascript' src='{% href "/content/js/wmd/showdown.js" %}'></script> + <script type='text/javascript' src='{% href "/content/js/wmd/wmd.js" %}'></script> + <link rel="stylesheet" type="text/css" href="{% href "/content/js/wmd/wmd.css" %}" /> {% endif %} - <script type='text/javascript' src='/content/js/com.cnprog.post.js'></script> - <script type='text/javascript' src='/content/js/jquery.validate.pack.js'></script> + <script type='text/javascript' src='{% href "/content/js/com.cnprog.post.js" %}'></script> + <script type='text/javascript' src='{% href "/content/js/jquery.validate.pack.js" %}'></script> + <script type="text/javascript"> // define reputation needs for comments var repNeededForComments = 50; @@ -61,14 +63,17 @@ </div> <div id="main-body" class=""> <div id="askform"> - <form id="fmanswer" action="{% url answer question.id %}" method="post"> <table style="width:100%;" id="question-table" {% if question.deleted %}class="deleted"{%endif%}> <tr> <td style="width:30px;vertical-align:top"> <div class="vote-buttons"> {% if question_vote %} <img id="question-img-upvote-{{ question.id }}" class="question-img-upvote" - src="/content/images/vote-arrow-up{% if question_vote.is_upvote %}-on{% endif %}.png" + {% if question_vote.is_upvote %} + src="{% href "/content/images/vote-arrow-up-on.png" %}" + {% else %} + src="{% href "/content/images/vote-arrow-up.png" %}" + {% endif %} alt="{% trans "i like this post (click again to cancel)" %}" title="{% trans "i like this post (click again to cancel)" %}" /> <div id="question-vote-number-{{ question.id }}" class="vote-number" @@ -76,34 +81,38 @@ {{ question.score }} </div> <img id="question-img-downvote-{{ question.id }}" class="question-img-downvote" - src="/content/images/vote-arrow-down{% if question_vote.is_downvote %}-on{% endif %}.png" + {% if question_vote.is_downvote %} + src="{% href "/content/images/vote-arrow-down-on.png" %}" + {% else %} + src="{% href "/content/images/vote-arrow-down.png" %}" + {% endif %} alt="{% trans "i dont like this post (click again to cancel)" %}" title="{% trans "i dont like this post (click again to cancel)" %}" /> {% else %} <img id="question-img-upvote-{{ question.id }}" class="question-img-upvote" alt="{% trans "i like this post (click again to cancel)" %}" - src="/content/images/vote-arrow-up.png" + src="{% href "/content/images/vote-arrow-up.png" %}" title="{% trans "i like this post (click again to cancel)" %}" /> <div id="question-vote-number-{{ question.id }}" class="vote-number" title="{% trans "current number of votes" %}"> {{ question.score }} </div> <img id="question-img-downvote-{{ question.id }}" class="question-img-downvote" - src="/content/images/vote-arrow-down.png" + src="{% href "/content/images/vote-arrow-down.png" %}" alt="{% trans "i dont like this post (click again to cancel)" %}" title="{% trans "i dont like this post (click again to cancel)" %}" /> {% endif %} {% if favorited %} - <img class="question-img-favorite" src="/content/images/vote-favorite-on.png" + <img class="question-img-favorite" src="{% href "/content/images/vote-favorite-on.png" %}" alt="{% trans "mark this question as favorite (click again to cancel)" %}" title="{% trans "mark this question as favorite (click again to cancel)" %}" /> <div id="favorite-number" class="favorite-number my-favorite-number"> {{ question.favourite_count }} </div> {% else %} - <img class="question-img-favorite" src="/content/images/vote-favorite-off.png" + <img class="question-img-favorite" src="{% href "/content/images/vote-favorite-off.png" %}" alt="{% trans "remove favorite mark from this question (click again to restore mark)" %}" title="{% trans "remove favorite mark from this question (click again to restore mark)" %}" /> <div id="favorite-number" class="favorite-number"> @@ -119,135 +128,86 @@ <div class="question-body"> {{ question.html|safe }} </div> - <div id="question-tags" class="tags" > - {% for tag in question.tagname_list %} - <a href="{% url forum.views.tag tag|urlencode %}" class="post-tag" - title="{% blocktrans with tag as tagname %}see questions tagged '{{ tagname }}'{% endblocktrans %}" rel="tag">{{ tag }}</a> - {% endfor %} + <div id="question-controls" class="post-controls"> + <div id="question-tags" class="tags"> + {% for tag in question.tagname_list %} + <a href="{% url forum.views.tag tag|urlencode %}" class="post-tag" + title="{% blocktrans with tag as tagname %}see questions tagged '{{ tagname }}'{% endblocktrans %}" rel="tag">{{ tag }}</a> + {% endfor %} + </div> + {% joinitems using '<span class="action-link-separator">|</span>' %} + {% if request.user|can_edit_post:question %} + <span class="action-link"><a href="{% url edit_question question.id %}">{% trans 'edit' %}</a></span> + {% endif %} + {% separator %} + {% if question.closed %} + {% if request.user|can_reopen_question:question %} + <span class="action-link"><a href="{% url reopen question.id %}">{% trans "reopen" %}</a></span> + {% endif %} + {% else %} + {% if request.user|can_close_question:question %} + <span class="action-link"><a href="{% url close question.id %}">{% trans "close" %}</a></span> + {% endif %} + {% endif %} + {% separator %} + {% if request.user|can_flag_offensive %} + <span id="question-offensive-flag-{{ question.id }}" class="offensive-flag" + title="{% trans "report as offensive (i.e containing spam, advertising, malicious text, etc.)" %}"> + <a>{% trans "flag offensive" %}</a> + {% if request.user|can_view_offensive_flags and question.offensive_flag_count %} + <span class="darkred">({{ question.offensive_flag_count }})</span> + {% endif %} + </span> + {% endif %} + {% separator %} + {% if request.user|can_delete_post:question %} + <span class="action-link"><a id="question-delete-link-{{question.id}}">{% trans "delete" %}</a></span> + {% endif %} + {% endjoinitems %} </div> - {% trans "Category: " %} <a href="{% url forum.views.category question.category|urlencode %}">{{question.category}}</a> - <div id="question-controls" style="clear:both;"> - <table width="100%"> - <tr> - <td width="210px" style="vertical-align:top"> - - {% if request.user|can_edit_post:question %} - <span class="action-link"><a href="{% url edit_question question.id %}">{% trans 'edit' %}</a></span> - <span class="action-link-separator">|</span> - {% endif %} - {% if request.user|can_delete_post:question %} - <span class="action-link"><a id="question-delete-link-{{question.id}}">{% trans "delete" %}</a></span> - <span class="action-link-separator">|</span> - {% endif %} - {% if question.closed %} - {% if request.user|can_reopen_question:question %} - <span class="action-link"><a href="{% url reopen question.id %}">{% trans "reopen" %}</a></span> - <span class="action-link-separator">|</span> - {% endif %} - {% else %} - {% if request.user|can_close_question:question %} - <span class="action-link"><a href="{% url close question.id %}">{% trans "close" %}</a></span> - <span class="action-link-separator">|</span> - {% endif %} - {% endif %} - - <span id="question-offensive-flag-{{ question.id }}" class="offensive-flag" - title="{% trans "report as offensive (i.e containing spam, advertising, malicious text, etc.)" %}"> - <a>{% trans "flag offensive" %}</a> - <span class="darkred">{% if request.user|can_view_offensive_flags %} - {% if question.offensive_flag_count %}({{ question.offensive_flag_count }}){% endif %}{% endif %}</span> - </span> - - </td> - <td width="210px" style="vertical-align:top"> - {% if question.last_edited_by %} - <div class="question-edit" > - <table width="200px" > - <tr> - <td colspan="2"> - {% trans "updated" %} <a href="{% url question_revisions question.id %}"><strong title="{{question.last_edited_at }}">{% diff_date question.last_edited_at %}</strong></a> - </td> - - </tr> - {% if question.wiki %} - <tr> - <td style="width:40px;vertical-align:bottom"> - {% gravatar question.last_edited_by 32 %} - </td> - <td style="width:160px; vertical-align:top"> - <a href="{% url users %}{{ question.last_edited_by.id }}/{{ question.last_edited_by.username }}">{{ question.last_edited_by.username }}</a> - </td> - </tr> - {% else %} - {% ifequal question.last_edited_by question.author %} - <tr> - <td> </td> - <td> </td> - </tr> - {% else %} - <tr> - <td style="width:40px;vertical-align:bottom"> - {% gravatar question.last_edited_by 32 %} - </td> - <td style="width:160px; vertical-align:top"> - <div><a href="{% url users %}{{ question.last_edited_by.id }}/{{ question.last_edited_by.username }}">{{ question.last_edited_by.username }}</a></div> - - <div> - {% get_score_badge question.last_edited_by %} - </div> - - </td> - </tr> - {% endifequal %} - {% endif %} - </table> - </div> - {% endif %} - - </td> - <td style="vertical-align:top"> - {% if question.wiki %} - <span class="wiki-category">{% trans "community wiki" %}</span> - <div style="margin-bottom:10px"></div> - {% else %} - <div class="question-mark"> - <table width="200px"> - <tr> - <td colspan="2"> - {% trans "asked" %} <strong title="{{ question.added_at }}">{% diff_date question.added_at %}</strong> - </td> - - </tr> - - <tr> - <td style="width:40px; vertical-align:bottom"> - {% gravatar question.author 32 %} - </td> - <td align="left" style="width:160px;vertical-align:top"> - <div><a href="{% url users %}{{ question.author.id }}/{{ question.author }}">{{ question.author }}</a></div> - <div> - {% get_score_badge question.author %} - </div> - </td> - </tr> - - </table> - </div> - {% endif %} - - </td> - </tr> - </table> - + <div class="post-update-info-container"> + {% post_contributor_info question "original_author" %} + {% post_contributor_info question "last_updater" %} + </div> + <div class="comments-container" id="comments-container-question-{{question.id}}"> + {% for comment in question.get_comments|slice:":5" %} + <p class="comment" id="comment-{{comment.id}}"> + {{comment.comment}} + - <a class="comment-user" href="{{comment.user.get_profile_url}}">{{comment.user}}</a> + {% spaceless %} + <span class="comment-age">({% diff_date comment.added_at %})</span> + {% if request.user|can_delete_comment:comment %} + <img class="delete-icon" + src="{% href "/content/images/close-small.png" %}" + title="{% trans "delete this comment" %}"/> + {% endif %} + {% endspaceless %} + </p> + {% endfor %} </div> <div class="post-comments" style="margin-bottom:20px"> - <input id="can-post-comments-question-{{question.id}}" type="hidden" value="{{ request.user|can_add_comments }}"/> - <a id="comments-link-question-{{question.id}}" class="comments-link"> - {% if question.comment_count %}{% trans "comments" %} <strong>({{question.comment_count}})</strong> - {% else %}{% trans "add comment" %} - {% endif %}</a> - <div id="comments-question-{{question.id}}" class="comments-container"> - <div class="comments"/></div> + <input id="can-post-comments-question-{{question.id}}" type="hidden" value="{{ request.user|can_add_comments:question }}"/> + {% if request.user|can_add_comments:question or question.comment_count > 5 %} + <a id="comments-link-question-{{question.id}}" class="comments-link"> + {% if request.user|can_add_comments:question %} + {% trans "add comment" %} + {% endif %} + {% if question.comment_count > 5 %} + {% if request.user|can_add_comments:question %}/ + {% blocktrans count question.get_comments|slice:"5:"|length as counter %} + see <strong>one</strong> more + {% plural %} + see <strong>{{counter}}</strong> more + {% endblocktrans %} + {% else %} + {% blocktrans count question.get_comments|slice:"5:"|length as counter %} + see <strong>one</strong> more comment + {% plural %} + see <strong>{{counter}}</strong> more comments + {% endblocktrans %} + {% endif %} + {% endif %}</a> + {% endif %} </div> </div> @@ -256,20 +216,29 @@ </table> {% if question.closed %} <div class="question-status" style="margin-bottom:15px"> - <h3>{% trans "The question has been closed for the following reason" %} "{{ question.get_close_reason_display }}" {% trans "by"%} + <h3>{% blocktrans with question.get_close_reason_display as close_reason %}The question has been closed for the following reason "{{ close_reason }}" by{% endblocktrans %} <a href="{{ question.closed_by.get_profile_url }}">{{ question.closed_by.username }}</a> - {% blocktrans %}close date {% endblocktrans %} {{question.closed_at|date:"d-m-Y H:i"}}</h3> + {% blocktrans with question.closed_at as closed_at %}close date {{closed_at}}{% endblocktrans %}</h3> </div> {% endif %} - - {% ifnotequal answers.length 0 %} + {% if answers %} + <hr/> <div class="tabBar"> <a name="sort-top"></a> - <div class="headQuestions">{{ answers|length }}{% trans "Answers" %}:</div> + <div class="headQuestions"> + {% blocktrans count answers|length as counter %} + One Answer: + {% plural %} + {{counter}} Answers: + {% endblocktrans %} + </div> <div class="tabsA"> - <a id="oldest" href="?sort=oldest#sort-top" title="{% trans "oldest answers will be shown first" %}">{% trans "oldest answers" %}</a> - <a id="latest" href="?sort=latest#sort-top" title="{% trans "newest answers will be shown first" %}">{% trans "newest answers" %}</a> - <a id="votes" href="?sort=votes#sort-top" title="{% trans "most voted answers will be shown first" %}">{% trans "popular answers" %}</a> + <a id="oldest" href="{% url question question.id %}?sort=oldest#sort-top" + title="{% trans "oldest answers will be shown first" %}">{% trans "oldest answers" %}</a> + <a id="latest" href="{% url question question.id %}?sort=latest#sort-top" + title="{% trans "newest answers will be shown first" %}">{% trans "newest answers" %}</a> + <a id="votes" href="{% url question question.id %}?sort=votes#sort-top" + title="{% trans "most voted answers will be shown first" %}">{% trans "popular answers" %}</a> </div> </div> {% cnprog_paginator context %} @@ -282,26 +251,26 @@ <td style="width:30px;vertical-align:top"> <div class="vote-buttons"> <img id="answer-img-upvote-{{ answer.id }}" class="answer-img-upvote" - src="/content/images/vote-arrow-up{% get_user_vote_image user_answer_votes answer.id 1 %}.png" + src="{% blockresource %}/content/images/vote-arrow-up{% get_user_vote_image user_answer_votes answer.id 1 %}.png{% endblockresource %}" alt="{% trans "i like this answer (click again to cancel)" %}" title="{% trans "i like this answer (click again to cancel)" %}"/> <div id="answer-vote-number-{{ answer.id }}" class="vote-number" title="{% trans "current number of votes" %}"> {{ answer.score }} </div> <img id="answer-img-downvote-{{ answer.id }}" class="answer-img-downvote" - src="/content/images/vote-arrow-down{% get_user_vote_image user_answer_votes answer.id -1 %}.png" + src="{% blockresource %}/content/images/vote-arrow-down{% get_user_vote_image user_answer_votes answer.id -1 %}.png{% endblockresource %}" alt="{% trans "i dont like this answer (click again to cancel)" %}" title="{% trans "i dont like this answer (click again to cancel)" %}" /> {% ifequal request.user question.author %} <img id="answer-img-accept-{{ answer.id }}" class="answer-img-accept" - src="/content/images/vote-accepted{% if answer.accepted %}-on{% endif %}.png" + src="{% blockresource %}/content/images/vote-accepted{% if answer.accepted %}-on{% endif %}.png{% endblockresource %}" alt="{% trans "mark this answer as favorite (click again to undo)" %}" title="{% trans "mark this answer as favorite (click again to undo)" %}" /> {% else %} {% if answer.accepted %} <img id="answer-img-accept-{{ answer.id }}" class="answer-img-accept" - src="/content/images/vote-accepted{% if answer.accepted %}-on{% endif %}.png" + src="{% blockresource %}/content/images/vote-accepted{% if answer.accepted %}-on{% endif %}.png{% endblockresource %}" alt="{% trans "the author of the question has selected this answer as correct" %}" title="{% trans "the author of the question has selected this answer as correct" %}" /> {% endif %} @@ -313,116 +282,83 @@ <div class="answer-body"> {{ answer.html|safe }} </div> - <div class="answer-controls" style="clear:both;"> - <table width="100%"> - <tr> - <td width="400px" style="vertical-align:top"> - {% if request.user|can_edit_post:answer %} - <span class="action-link"><a href="{% url edit_answer answer.id %}">{% trans "edit" %}</a></span> - <span class="action-link-separator">|</span> - {% endif %} - {% if request.user|can_delete_post:answer %} - <span class="action-link"> - <a id="answer-delete-link-{{answer.id}}"> - {% if answer.deleted %} - {% trans "undelete" %} - {% endif %} - {% if not answer.deleted %} - {% trans "delete" %} - {% endif %} - </a> - </span> - <span class="action-link-separator">|</span> - {% endif %} - <span class="linksopt"> - <a href="#{{ answer.id }}" title="{% trans "answer permanent link" %}"> - {% trans "permanent link" %} - </a> - </span> - <span class="action-link-separator">|</span> - <span id="answer-offensive-flag-{{ answer.id }}" class="offensive-flag" - title="{% trans "report as offensive (i.e containing spam, advertising, malicious text, etc.)" %}"> - <a>{% trans "flag offensive" %}</a> - <span class="darkred">{% if request.user|can_view_offensive_flags %}{% if answer.offensive_flag_count %}({{ answer.offensive_flag_count }}){% endif %}{% endif %}</span></span> - </td> - <td width="110px" style="vertical-align:top"> - {% if answer.last_edited_by %} - <div class="question-edit" > - <table width="200px" > - <tr> - <td colspan="2"> - {% trans "updated" %}<a href="{% url answer_revisions answer.id %}"><strong title="{{answer.last_edited_at }}">{% diff_date answer.last_edited_at %}</strong></a> - </td> - </tr> - {% if answer.wiki %} - <tr> - <td width="40px" style="vertical-align:bottom"> - {% gravatar answer.last_edited_by 32 %} - </td> - <td style="width:160px; vertical-align:top"> - <div><a href="{% url users %}{{ answer.last_edited_by.id }}/{{ answer.last_edited_by.username }}">{{ answer.last_edited_by.username }}</a></div> - - </td> - </tr> - {% else %} - {% ifequal answer.last_edited_by answer.author %} - <tr> - <td> </td> - <td> </td> - </tr> - {% else %} - <tr> - <td width="40px" style="vertical-align:bottom"> - {% gravatar answer.last_edited_by 32 %} - </td> - <td style="width:160px; vertical-align:top"> - <div><a href="{% url users %}{{ answer.last_edited_by.id }}/{{ answer.last_edited_by.username }}">{{ answer.last_edited_by.username }}</a></div> - <div> - {% get_score_badge answer.last_edited_by %} - </div> - </td> - </tr> - {% endifequal %} - {% endif %} - </table> - </div> + <div class="answer-controls post-controls"> + {% joinitems using '<span class="action-link-separator">|</span>' %} + <span class="linksopt"> + <a href="#{{ answer.id }}" title="{% trans "answer permanent link" %}"> + {% trans "permanent link" %} + </a> + </span> + {% separator %} + {% if request.user|can_edit_post:answer %} + <span class="action-link"><a href="{% url edit_answer answer.id %}">{% trans 'edit' %}</a></span> + {% endif %} + {% separator %} + + {% if request.user|can_flag_offensive %} + <span id="answer-offensive-flag-{{ answer.id }}" class="offensive-flag" + title="{% trans "report as offensive (i.e containing spam, advertising, malicious text, etc.)" %}"> + <a>{% trans "flag offensive" %}</a> + {% if request.user|can_view_offensive_flags and answer.offensive_flag_count %} + <span class="darkred">({{ answer.offensive_flag_count }})</span> {% endif %} - - </td> - <td style="vertical-align:top"> - {% if answer.wiki %} - <span class="wiki-category">{% trans "community wiki" %}</span> - <div style="margin-bottom:10px"></div> + </span> + {% endif %} + {% separator %} + {% if request.user|can_delete_post:answer %} + {% spaceless %} + <span class="action-link"> + <a id="answer-delete-link-{{answer.id}}"> + {% if answer.deleted %}{% trans "undelete" %}{% else %}{% trans "delete" %}{% endif %}</a> + </span> + {% endspaceless %} + {% endif %} + {% endjoinitems %} + </div> + <div class="post-update-info-container"> + {% post_contributor_info answer "original_author" %} + {% post_contributor_info answer "last_updater" %} + </div> + <div class="comments-container" id="comments-container-answer-{{answer.id}}"> + {% for comment in answer.get_comments|slice:":5" %} + <p id="comment-{{comment.id}}" class="comment"> + {{comment.comment}} + - <a class="comment-user" href="{{comment.user.get_profile_url}}">{{comment.user}}</a> + {% spaceless %} + <span class="comment-age">({% diff_date comment.added_at %})</span> + {% if request.user|can_delete_comment:comment %} + <img class="delete-icon" + src="{% href "/content/images/close-small.png" %}" + title="{% trans "delete this comment" %}"/> + {% endif %} + {% endspaceless %} + </p> + {% endfor %} + </div> + <div class="post-comments" style="margin-bottom:20px"> + <input id="can-post-comments-answer-{{answer.id}}" type="hidden" value="{{ request.user|can_add_comments:answer}}"/> + {% if request.user|can_add_comments:answer or answer.comment_count > 5 %} + <a id="comments-link-answer-{{answer.id}}" class="comments-link"> + {% if request.user|can_add_comments:answer %} + {% trans "add comment" %} + {% endif %} + {% if answer.comment_count > 5 %} + {% if request.user|can_add_comments:answer %}/ + {% blocktrans count answer.get_comments|slice:"5:"|length as counter %} + see <strong>one</strong> more + {% plural %} + see <strong>{{counter}}</strong> more + {% endblocktrans %} {% else %} - <div class="answer-mark"> - <table width="200px"> - <tr> - <td colspan="2"> - {% trans "asked" %} <strong title="{{answer.added_at}}">{% diff_date answer.added_at %}</strong> - </td> - </tr> - <tr> - <td width="40px" style="vertical-align:bottom"> - {% gravatar answer.author 32 %} - </td> - <td style="width:160px; vertical-align:top"> - <div><a href="{% url users %}{{ answer.author.id }}/{{ answer.author.username }}">{{ answer.author }}</a></div> - <div> - {% get_score_badge answer.author %} - </div> - </td> - </tr> - </table> - </div> + {% blocktrans count answer.get_comments|slice:"5:"|length as counter %} + see <strong>one</strong> more comment + {% plural %} + see <strong>{{counter}}</strong> more comments + {% endblocktrans %} {% endif %} - - </td> - </tr> - - </table> - + {% endif %}</a> + {% endif %} </div> - </div> <div id="comment-{{ answer.id }}" class="post-comments" > <input id="can-post-comments-answer-{{answer.id}}" type="hidden" value="{{ request.user|can_add_comments }}"/> @@ -440,46 +376,101 @@ <div class="paginator-container-left"> {% cnprog_paginator context %} </div> + {% endif %} + <form id="fmanswer" action="{% url answer question.id %}" method="post"> + {% if request.user.is_authenticated %} + <p style="padding-left:3px"> + {{ answer.email_notify }} + <label for="question-subscribe-updates"> + {% ifequal request.user.get_q_sel_email_feed_frequency 'n' %} + {% trans "Notify me once a day when there are any new answers" %} + {% else %} + {% ifequal request.user.get_q_sel_email_feed_frequency 'd' %} + {% trans "Notify me once a day when there are any new answers" %} + {% else %} + {% ifequal request.user.get_q_sel_email_feed_frequency 'w' %} + {% trans "Notify me weekly when there are any new answers" %} + {% endifequal %} + {% endifequal %} + {% endifequal %} + </label> + {% blocktrans with request.user.get_profile_url as profile_url %} + You can always adjust frequency of email updates from your {{profile_url}} + {% endblocktrans %} + </p> {% else %} - <div class="line"></div> - {% endifnotequal %} + <p style="padding-left:3px"> + <input class="nomargin" type="checkbox" disabled="disabled" /> + <label>{% trans "once you sign in you will be able to subscribe for any updates here" %}</label> + </p> + {% endif %} <div style="clear:both"> </div> {% if not question.closed %} - <div style="padding:10px 0 0 0;"> - <div class="headNormal">{% trans "Your answer" %}:</div> - </div> - {% if not request.user.is_authenticated %} - <div class="message">{% trans "you can answer anonymously and then login" %}</div> - {% endif %} - - <div id="description" class="" > - <div id="wmd-button-bar" class="wmd-panel"></div> - {{ answer.text }} - <div class="preview-toggle"> - <table width="100%"> - <tr> - <td> - <span id="pre-collapse" - title="{% trans "Toggle the real time Markdown editor preview" %}">{% trans "toggle preview" %}</span> - </td> - <td style="text-align:right;"> - {{ answer.wiki }} <span style="font-weight:normal;cursor:help" title="{{answer.wiki.help_text}}">{{ answer.wiki.label_tag }} </span> - </td> - </tr> - - </table> + <div style="padding:10px 0 0 0;"> + {% spaceless %} + <div class="headNormal"> + {% if answers %} + {% trans "Your answer" %} + {% else %} + {% trans "Be the first one to answer this question!" %} + {% endif %} + </div> + {% endspaceless %} </div> - <div id="previewer" class="wmd-preview"></div> - {{ answer.text.errors }} - </div> - <input type="submit" value="{% trans "Answer the question" %}" class="submit"/><span class="form-error"></span> - {% if request.user.is_authenticated %} - {{ answer.email_notify }} <label for="question-subscribe-updates">{% trans "Notify me daily if there are any new answers." %}</label> + {% if not request.user.is_authenticated %} + <div class="message">{% trans "you can answer anonymously and then login" %}</div> {% else %} - <input type="checkbox" disabled="disabled" /><label>{% trans "once you sign in you will be able to subscribe for any updates here" %}</label> + <p class="message"> + {% ifequal request.user question.author %} + {% trans "answer your own question only to give an answer" %} + {% else %} + {% trans "please only give an answer, no discussions" %} + {% endifequal %} + </p> {% endif %} + + <div id="description" class="" > + <div id="wmd-button-bar" class="wmd-panel"></div> + {{ answer.text }} + <div class="preview-toggle"> + <table width="100%"> + <tr> + <td> + <span id="pre-collapse" + title="{% trans "Toggle the real time Markdown editor preview" %}"> + {% trans "toggle preview" %} + </span> + </td> + {% if settings.WIKI_ON %} + <td style="text-align:right;"> + {{ answer.wiki }} + <span style="font-weight:normal;cursor:help" + title="{{answer.wiki.help_text}}"> + {{ answer.wiki.label_tag }} + </span> + </td> + {% endif %} + </tr> + + </table> + </div> + <div id="previewer" class="wmd-preview"></div> + {{ answer.text.errors }} + </div> + <p><span class="form-error"></span></p> + <input type="submit" + {% if user.is_anonymous %} + value="{% trans "Login/Signup to Post Your Answer" %}" + {% else %} + {% if user == question.author %} + value="{% trans "Answer Your Own Question" %}" + {% else %} + value="{% trans "Answer the question" %}" + {% endif %} + {% endif %} + class="submit" style="float:left"/> {% endif %} </form> </div> @@ -495,17 +486,17 @@ {% for tag in tags %} <a href="{% url forum.views.tag tag.name|urlencode %}" title="{% trans "see questions tagged"%}'{{tag.name}}'{% trans "using tags" %}" - rel="tag">{{ tag.name }}</a> <span class="tag-number">✕{{ tag.used_count|intcomma }}</span><br/> + rel="tag">{{ tag.name }}</a> <span class="tag-number">×{{ tag.used_count|intcomma }}</span><br/> {% endfor %} </p> <p> - {% trans "question asked" %}: <strong title="{{ question.added_at }}">{{ question.added_at|timesince }} {% trans "ago" %}</strong> + {% trans "question asked" %}: <strong title="{{ question.added_at }}">{% diff_date question.added_at %}</strong> </p> <p> {% trans "question was seen" %}: <strong>{{ question.view_count|intcomma }} {% trans "times" %}</strong> </p> <p> - {% trans "last updated" %}: <strong title="{{ question.last_activity_at }}">{{ question.last_activity_at|timesince }} {% trans "ago" %}</strong> + {% trans "last updated" %}: <strong title="{{ question.last_activity_at }}">{% diff_date question.last_activity_at %}</strong> </p> </div> @@ -514,7 +505,7 @@ <div class="questions-related"> {% for question in similar_questions %} <p> - <a href="{{ question.get_absolute_url }}">{{ question.get_question_title }}</a> + <a href="{{ question.get_absolute_url }}">{{ question.get_question_title }}</a> </p> {% endfor %} </div> diff --git a/templates/question_edit.html b/templates/question_edit.html index a7460b65..7a10b6ae 100644 --- a/templates/question_edit.html +++ b/templates/question_edit.html @@ -1,14 +1,15 @@ {% extends "base.html" %} <!-- question_edit.html --> {% load i18n %} +{% load extra_tags %} {% block title %}{% spaceless %}{% trans "Edit question" %}{% endspaceless %}{% endblock %} {% block forejs %} - <script type='text/javascript' src='/content/js/com.cnprog.editor.js'></script> - <script type='text/javascript' src='/content/js/com.cnprog.post.js'></script> - <script type='text/javascript' src='/content/js/jquery.validate.pack.js'></script> - <script type='text/javascript' src='/content/js/wmd/showdown.js'></script> - <script type='text/javascript' src='/content/js/wmd/wmd.js'></script> - <link rel="stylesheet" type="text/css" href="/content/js/wmd/wmd.css" /> + <script type='text/javascript' src='{% href "/content/js/com.cnprog.editor.js" %}'></script> + <script type='text/javascript' src='{% href "/content/js/com.cnprog.post.js" %}'></script> + <script type='text/javascript' src='{% href "/content/js/jquery.validate.pack.js" %}'></script> + <script type='text/javascript' src='{% href "/content/js/wmd/showdown.js" %}'></script> + <script type='text/javascript' src='{% href "/content/js/wmd/wmd.js" %}'></script> + <link rel="stylesheet" type="text/css" href="{% href "/content/js/wmd/wmd.css" %}" /> <script type="text/javascript"> //todo move javascript out $().ready(function(){ @@ -21,7 +22,7 @@ //toggle preview of editor var display = true; - var txt = "[{% trans "hide preview"}%]"; + var txt = "[{% trans "hide preview" %}]"; $('#pre-collapse').text(txt); $('#pre-collapse').bind('click', function(){ txt = display ? "[{% trans "show preview" %}]" : "[{% trans "hide preview" %}]"; @@ -90,9 +91,11 @@ <td> <span id="pre-collapse" title="{% trans "Toggle the real time Markdown editor preview" %}">{% trans "toggle preview" %}</span> </td> + {% if settings.WIKI_ON %} <td style="text-align:right;"> {{ form.wiki }} <span style="color:#000;cursor:help" title="{{form.wiki.help_text}}">{{ form.wiki.label_tag }} </span> </td> + {% endif %} </tr> </table> diff --git a/templates/question_edit_tips.html b/templates/question_edit_tips.html index 85614595..4cabea79 100644 --- a/templates/question_edit_tips.html +++ b/templates/question_edit_tips.html @@ -13,8 +13,9 @@ {% trans "be clear and concise" %} </li> </ul> - <a href="{% url faq %}" target="_blank" title="{% trans "see frequently asked questions" %}" style="float:right;position:relative">{% trans "faq" %} »</a> - <br> + <p class='info-box-follow-up-links'> + <a href="{% url faq %}" target="_blank" title="{% trans "see frequently asked questions" %}">{% trans "faq" %} »</a> + </p> </div> </div> @@ -45,6 +46,8 @@ {% trans "basic HTML tags are also supported" %} </li> </ul> - <a href="http://en.wikipedia.org/wiki/Markdown" target="_blank" style="float:right;position:relative">{% trans "learn more about Markdown" %} »</a> + <p class='info-box-follow-up-links'> + <a href="http://en.wikipedia.org/wiki/Markdown" target="_blank">{% trans "learn more about Markdown" %} »</a> + </p> </div> <!-- end question_edit_tips.html --> diff --git a/templates/question_retag.html b/templates/question_retag.html index 66e51c8c..b7957962 100644 --- a/templates/question_retag.html +++ b/templates/question_retag.html @@ -1,10 +1,11 @@ {% extends "base.html" %} <!-- question_retag.html --> +{% load extra_tags %} {% block title %}{% spaceless %}{% trans "Change tags" %}{% endspaceless %}{% endblock %} {% block forejs %} - <script type='text/javascript' src='/content/js/com.cnprog.editor.js'></script> - <script type='text/javascript' src='/content/js/com.cnprog.post.js'></script> - <script type='text/javascript' src='/content/js/jquery.validate.pack.js'></script> + <script type='text/javascript' src='{% href "/content/js/com.cnprog.editor.js" %}'></script> + <script type='text/javascript' src='{% href "/content/js/com.cnprog.post.js" %}'></script> + <script type='text/javascript' src='{% href "/content/js/jquery.validate.pack.js" %}'></script> <script type="text/javascript"> $().ready(function(){ @@ -84,14 +85,18 @@ <li> {% trans "tags help us keep Questions organized" %} </li> + {% comment %} <li> 修改完整问题需要用户的积分达到一定条件(比如:积分 >= 3000分,自己发布的问题除外),而用户积分达到比较低的时候,就可以修改问题的标签(比如:积分 >= 500, 这里指所有问题的标签)。 </li> + {% endcomment %} <li> {% trans "tag editors receive special awards from the community" %} </li> </ul> - <a href="{% url faq %}" style="float:right;position:relative">faq »</a> + <p class='info-box-follow-up-links'> + <a href="{% url faq %}">faq »</a> + </p> </div> {% endblock %} diff --git a/templates/question_summary_list_roll.html b/templates/question_summary_list_roll.html new file mode 100644 index 00000000..7312dca9 --- /dev/null +++ b/templates/question_summary_list_roll.html @@ -0,0 +1,55 @@ + <div class="qstA"> + <h2> + <a href="{{ question.get_absolute_url }}">{{ question.get_question_title }}</a> + </h2> + <div class="stat"> + <table> + <tr> + <td><span class="num">{{ question.answer_count|intcomma }}</span> </td> + <td><span class="num">{{ question.score|intcomma }}</span> </td> + <td><span class="num">{{ question.view_count|cnprog_intword|safe }}</span> </td> + </tr> + <tr> + <td><span class="unit">{% trans "answers" %}</span></td> + <td><span class="unit">{% trans "votes" %}</span></td> + <td><span class="unit">{% trans "views" %}</span></td> + </tr> + </table> + </div> + + <div class="summary"> + {{ question.summary }}... + </div> + + {% ifequal tab_id 'active'%} + {% if question.wiki and settings.WIKI_ON %} + <span class="from wiki">{% trans "community wiki" %}</span> + <span class="date" title="{{ question.added_at }}">{% diff_date question.added_at %}</span> + {% else %} + <div class="from"> + {% comment %}{% gravatar question.last_activity_by 24 %}{% endcomment %} + <span class="author"><a href="{{ question.last_activity_by.get_profile_url }}">{{ question.last_activity_by }}</a></span> + <span class="score">{% get_score_badge question.last_activity_by %} </span> + <span class="date" title="{{ question.last_activity_at }}">{% diff_date question.last_activity_at %}</span> + </div> + {% endif %} + {% else %} + {% if question.wiki and settings.WIKI_ON %} + <span class="from wiki">{% trans "community wiki" %}</span> + <span class="date" title="{{ question.added_at }}">{% diff_date question.added_at %}</span> + {% else %} + <div class="from"> + {% comment %}{% gravatar question.author 24 %}{% endcomment %} + <span class="author"><a href="{{ question.author.get_profile_url }}">{{ question.author }}</a></span> + <span class="score">{% get_score_badge question.author %} </span> + <span class="date" title="{{ question.added_at }}">{% diff_date question.added_at %}</span> + </div> + {% endif %} + {% endifequal %} + + <div class="tags"> + {% for tag in question.tagname_list %} + <a href="{% url forum.views.tag tag|urlencode %}" title="{% trans "see questions tagged" %}'{{ tag }}'{% trans "using tags" %}" rel="tag">{{ tag }}</a> + {% endfor %} + </div> + </div> diff --git a/templates/questions.html b/templates/questions.html index 6121d876..f74256cf 100644 --- a/templates/questions.html +++ b/templates/questions.html @@ -4,23 +4,49 @@ {% load i18n %} {% load humanize %} {% load extra_filters %} +{% load smart_if %} {% block title %}{% spaceless %}{% trans "Questions" %}{% endspaceless %}{% endblock %} {% block forejs %} <script type="text/javascript"> - $().ready(function(){ - var tab_id = "{{ tab_id }}"; - $("#"+tab_id).attr('className',"on"); - $("#nav_questions").attr('className',"on"); - Hilite.exact = false; - Hilite.elementid = "listA"; - Hilite.debug_referrer = location.href; - }); - - </script> + var tags = {{ tags_autocomplete|safe }}; + $().ready(function(){ + var tab_id = "{{ tab_id }}"; + $("#"+tab_id).attr('className',"on"); + var on_tab = {% if is_unanswered %}'#nav_unanswered'{% else %}'#nav_questions'{% endif %}; + $(on_tab).attr('className','on'); + Hilite.exact = false; + Hilite.elementid = "listA"; + Hilite.debug_referrer = location.href; + }); + </script> + <script type='text/javascript' src='{% href "/content/js/com.cnprog.editor.js" %}'></script> + <script type='text/javascript' src='{% href "/content/js/com.cnprog.tag_selector.js" %}'></script> {% endblock %} {% block content %} <div class="tabBar"> +<<<<<<< HEAD:templates/questions.html <div class="headQuestions">{% if searchtag %}{% trans "Found by tags" %}{% else %}{% if searchtitle %}{% trans "Found by title" %}{% else %}{% trans "All questions" %}{% endif %}{% endif %}</div> +======= + <div class="headQuestions"> + {% if searchtag %} + {% trans "Found by tags" %} + {% else %} + {% if searchtitle %} + {% if settings.USE_SPHINX_SEARCH %} + {% trans "Search results" %} + {% else %} + {% trans "Found by title" %} + {% endif %} + {% else %} + {% if is_unanswered %} + {% trans "Unanswered questions" %} + {% else %} + {% trans "All questions" %} + {% endif %} + {% endif %} + {% endif %} + </div> +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/questions.html <div class="tabsA"> <a id="latest" href="?sort=latest" class="off" title="{% trans "most recently asked questions" %}">{% trans "newest" %}</a> <a id="active" href="?sort=active" class="off" title="{% trans "most recently updated questions" %}">{% trans "active" %}</a> @@ -30,22 +56,34 @@ </div> <div id="listA"> {% for question in questions.object_list %} - <div class="qstA"> + <div class="qstA" + {% if request.user.is_authenticated %} + {% if question.interesting_score > 0 %} + style="background:#ffff99;" + {% else %} + {% if not request.user.hide_ignored_questions %} + {% if question.ignored_score > 0 %} + style="background:#f3f3f3;" + {% endif %} + {% endif %} + {% endif %} + {% endif %} + > <h2> <a href="{{ question.get_absolute_url }}">{{ question.get_question_title }}</a> </h2> <div class="stat"> <table> - <tr> + <tr> <td><span class="num">{{ question.answer_count|intcomma }}</span> </td> <td><span class="num">{{ question.score|intcomma }}</span> </td> <td><span class="num">{{ question.view_count|cnprog_intword|safe }}</span> </td> - </tr> + </tr> <tr> <td><span class="unit">{% trans "answers" %}</span></td> <td><span class="unit">{% trans "votes" %}</span></td> <td><span class="unit">{% trans "views" %}</span></td> - </tr> + </tr> </table> </div> @@ -54,7 +92,7 @@ </div> {% ifequal tab_id 'active'%} - {% if question.wiki %} + {% if question.wiki and settings.WIKI_ON %} <span class="from wiki">{% trans "community wiki" %}</span> <span class="date" title="{{ question.added_at }}">{% diff_date question.added_at %}</span> {% else %} @@ -66,15 +104,33 @@ </div> {% endif %} {% else %} - {% if question.wiki %} + {% if question.wiki and settings.WIKI_ON %} <span class="from wiki">{% trans "community wiki" %}</span> <span class="date" title="{{ question.added_at }}">{% diff_date question.added_at %}</span> {% else %} <div class="from"> {% comment %}{% gravatar question.author 24 %}{% endcomment %} - <span class="author"><a href="{{ question.author.get_profile_url }}">{{ question.author }}</a></span> - <span class="score">{% get_score_badge question.author %} </span> - <span class="date" title="{{ question.added_at }}">{% diff_date question.added_at %}</span> + {% if question.last_activity_at != question.added_at %} + {% if question.author.id != question.last_activity_by.id %} + {% trans "Posted:" %} + <span class="author"><a href="{{ question.author.get_profile_url }}">{{ question.author }}</a></span> + <span class="score">{% get_score_badge question.author %} </span> + / {% trans "Updated:" %} + <span class="author"><a href="{{ question.last_activity_by.get_profile_url }}">{{ question.last_activity_by }}</a></span> + <span class="score">{% get_score_badge question.last_activity_by %} </span> + <span class="date" title="{{ question.last_activity_at }}">{% diff_date question.last_activity_at %}</span> + {% else %} + {% trans "Updated:" %} + <span class="author"><a href="{{ question.last_activity_by.get_profile_url }}">{{ question.last_activity_by }}</a></span> + <span class="score">{% get_score_badge question.last_activity_by %} </span> + <span class="date" title="{{ question.last_activity_at }}">{% diff_date question.last_activity_at %}</span> + {% endif %} + {% else %} + {% trans "Posted:" %} + <span class="author"><a href="{{ question.author.get_profile_url }}">{{ question.author }}</a></span> + <span class="score">{% get_score_badge question.author %} </span> + <span class="date" title="{{ question.added_at }}">{% diff_date question.added_at %}</span> + {% endif %} </div> {% endif %} {% endifequal %} @@ -87,26 +143,29 @@ {%trans "Category: "%}<a href="{% url forum.views.category question.category|urlencode %}">{{ question.category}}</a> </div> {% endfor %} + {% if searchtitle %} + {% if questions_count == 0 %} + <p class="evenMore" style="padding-top:30px;text-align:center;"> + {% trans "Did not find anything?" %} + {% else %} + <p class="evenMore" style="padding-left:9px"> + {% trans "Did not find what you were looking for?" %} + {% endif %} + <a href="{% url ask %}">{% trans "Please, post your question!" %}</a> + </p> + {% endif %} </div> - {% endblock %} {% block tail %} - - <div class="pager"> - {% cnprog_paginator context %} - - </div> - <div class="pagesize"> - {% cnprog_pagesize context %} - </div> - + <div class="pager">{% cnprog_paginator context %}</div> + <div class="pagesize">{% cnprog_pagesize context %}</div> {% endblock %} {% block sidebar %} <div class="boxC"> - <p> {% if searchtag %} +<<<<<<< HEAD:templates/questions.html {% blocktrans count questions_count as cnt with questions_count|intcomma as q_num and searchtag as tagname %} have total {{q_num}} questions tagged {{tagname}} {% plural %} @@ -127,7 +186,46 @@ {% endblocktrans %} {% endif %} {% endif %} - <br/> + <p> +======= + {% blocktrans count questions_count as cnt with questions_count|intcomma as q_num and searchtag as tagname %} + have total {{q_num}} questions tagged {{tagname}} + {% plural %} + have total {{q_num}} questions tagged {{tagname}} + {% endblocktrans %} + {% else %} + {% if searchtitle %} + {% if settings.USE_SPHINX_SEARCH %} + {% blocktrans count questions_count as cnt with questions_count|intcomma as q_num %} + have total {{q_num}} questions containing {{searchtitle}} in full text + {% plural %} + have total {{q_num}} questions containing {{searchtitle}} in full text + {% endblocktrans %} + {% else %} + {% blocktrans count questions_count as cnt with questions_count|intcomma as q_num %} + have total {{q_num}} questions containing {{searchtitle}} + {% plural %} + have total {{q_num}} questions containing {{searchtitle}} + {% endblocktrans %} + {% endif %} + {% else %} + {% if is_unanswered %} + {% blocktrans count questions as cnt with questions_count|intcomma as q_num %} + have total {{q_num}} unanswered questions + {% plural %} + have total {{q_num}} unanswered questions + {% endblocktrans %} + {% else %} + {% blocktrans count questions as cnt with questions_count|intcomma as q_num %} + have total {{q_num}} questions + {% plural %} + have total {{q_num}} questions + {% endblocktrans %} + {% endif %} + {% endif %} + {% endif %} + <p class="nomargin"> +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/questions.html {% ifequal tab_id "latest" %} {% trans "latest questions info" %} {% endifequal %} @@ -146,19 +244,27 @@ {% trans "Questions are sorted by the <strong>number of votes</strong>." %} {% trans "Most voted questions are shown first." %} {% endifequal %} - - - </p> + </p> </div> +{% if request.user.is_authenticated %} +{% include "tag_selector.html" %} +{% endif %} <div class="boxC"> <h3 class="subtitle">{% trans "Related tags" %}</h3> <div class="tags"> {% for tag in tags %} +<<<<<<< HEAD:templates/questions.html <a rel="tag" title="{% trans "see questions tagged" %}'{{ tag.name }}'{% trans "using tags" %}" href="{% url forum.views.tag tag.name|urlencode %}">{{ tag.name }}</a> - <span class="tag-number">✕ {{ tag.used_count|intcomma }}</span> + <span class="tag-number">× {{ tag.used_count|intcomma }}</span> <br /> {% endfor %} <br /> +======= + <a rel="tag" title="{% blocktrans with tag.name as tag_name %}see questions tagged '{{ tag_name }}'{% endblocktrans %}" href="{% url forum.views.tag tag.name|urlencode %}">{{ tag.name }}</a> + <span class="tag-number">× {{ tag.used_count|intcomma }}</span> + <br /> + {% endfor %} +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/questions.html </div> </div> diff --git a/templates/revisions_answer.html b/templates/revisions_answer.html index 23606dc9..974e589c 100644 --- a/templates/revisions_answer.html +++ b/templates/revisions_answer.html @@ -6,8 +6,8 @@ {% load humanize %} {% block title %}{% spaceless %}{% trans "Revision history" %}{% endspaceless %}{% endblock %} {% block forejs %} - <script type='text/javascript' src='/content/js/com.cnprog.editor.js'></script> - <script type='text/javascript' src='/content/js/com.cnprog.post.js'></script> + <script type='text/javascript' src='{% href "/content/js/com.cnprog.editor.js" %}'></script> + <script type='text/javascript' src='{% href "/content/js/com.cnprog.post.js" %}'></script> <script type="text/javascript"> //todo - take this out into .js file $().ready(function(){ @@ -24,7 +24,9 @@ var arrow = $("#rev-arrow-" + id); var visible = arrow.attr("src").indexOf("hide") > -1; - arrow.attr("src", "/content/images/expander-arrow-" + (visible ? "show" : "hide") + ".gif"); + var path = $.i18n._('/') + "content/images/expander-arrow-" + + (visible ? "show" : "hide") + ".gif" + "?v={{settings.RESOURCE_REVISION}}"; + arrow.attr("src", path); $("#rev-body-" + id).slideToggle("fast"); } @@ -43,11 +45,14 @@ <div class="header-controls"> <table width="100%"> <tr> - <td width="20" style="vertical-align:middle"><img id="rev-arrow-{{ revision.revision }}" src="/content/images/expander-arrow-show.gif"></td> + <td width="20" style="vertical-align:middle"><img id="rev-arrow-{{ revision.revision }}" + src="{% href "/content/images/expander-arrow-show.gif" %}" + alt="{% trans "click to hide/show revision" %}"/> + </td> <td width="30px" style="vertical-align:middle"><span class="revision-number" title="{% trans "revision" %} {{ revision.revision }}">{{ revision.revision }}</span></td> <td width="200px" style="vertical-align:middle"> {% if revision.summary %} - <div class="summary"><span>{{ revision.summary }}<span></div> + <div class="summary"><span>{{ revision.summary }}</span></div> {% endif %} {% if request.user|can_edit_post:post %} <a href="{% url edit_answer post.id %}?revision={{ revision.revision }}">{% trans "edit" %}</a> @@ -56,31 +61,7 @@ </td> <td align="right"> <div class="revision-mark" > - <table width="100%" style="text-align:left" > - <tr > - <td colspan="2" style="padding:3px 0 3px 0"> - {% ifequal revision.revision 1 %} - {% trans "asked" %} <strong title="{{ post.added_at }}">{% diff_date post.added_at %}</strong> - {% else %} - {% trans "updated" %} <strong title="{{ post.last_edited_at }}">{% diff_date revision.revised_at %}</strong> - {% endifequal %} - </td> - - </tr> - <tr> - <td width="35px" style="vertical-align:bottom; "> - {% gravatar revision.author 32 %} - </td> - <td style="width:120px; vertical-align:top"> - <div style="height:18px"> - <a href="{% url users %}{{ revision.author.id }}/{{ revision.author.username }}">{{ revision.author.username }}</a></div> - <div> - {% get_score_badge revision.author %} - </div> - </td> - </tr> - - </table> + {% post_contributor_info revision %} </div> </td> </tr> diff --git a/templates/revisions_question.html b/templates/revisions_question.html index b76ced24..83512e4a 100644 --- a/templates/revisions_question.html +++ b/templates/revisions_question.html @@ -7,8 +7,8 @@ {% load humanize %} {% block title %}{% spaceless %}{% trans "Revision history" %}{% endspaceless %}{% endblock %} {% block forejs %} - <script type='text/javascript' src='/content/js/com.cnprog.editor.js'></script> - <script type='text/javascript' src='/content/js/com.cnprog.post.js'></script> + <script type='text/javascript' src='{% href "/content/js/com.cnprog.editor.js" %}'></script> + <script type='text/javascript' src='{% href "/content/js/com.cnprog.post.js" %}'></script> <script type="text/javascript"> //todo - take this out into .js file $().ready(function(){ @@ -25,7 +25,9 @@ var arrow = $("#rev-arrow-" + id); var visible = arrow.attr("src").indexOf("hide") > -1; - arrow.attr("src", "/content/images/expander-arrow-" + (visible ? "show" : "hide") + ".gif"); + var path = $.i18n._('/') + "content/images/expander-arrow-" + + (visible ? "show" : "hide") + ".gif" + "?v={{settings.RESOURCE_REVISION}}"; + arrow.attr("src", path); $("#rev-body-" + id).slideToggle("fast"); } </script> @@ -43,11 +45,14 @@ <div class="header-controls"> <table width="100%"> <tr> - <td width="20" style="vertical-align:middle"><img id="rev-arrow-{{ revision.revision }}" src="/content/images/expander-arrow-show.gif"></td> + <td width="20" style="vertical-align:middle"><img id="rev-arrow-{{ revision.revision }}" + src="{% href "/content/images/expander-arrow-show.gif" %}" + alt="{% trans "click to hide/show revision" %}"/> + </td> <td width="30px" style="vertical-align:middle"><span class="revision-number" title="{% trans "revision" %} {{ revision.revision }}">{{ revision.revision }}</span></td> <td width="200px" style="vertical-align:middle"> {% if revision.summary %} - <div class="summary"><span>{{ revision.summary }}<span></div> + <div class="summary"><span>{{ revision.summary }}</span></div> {% endif %} {% if request.user|can_edit_post:post %} <a href="{% url edit_question post.id %}?revision={{ revision.revision }}">{% trans "edit" %}</a> @@ -56,31 +61,7 @@ </td> <td align="right"> <div class="revision-mark" > - <table width="100%" style="text-align:left" > - <tr > - <td colspan="2" style="padding:3px 0 3px 0"> - {% ifequal revision.revision 1 %} - {% trans "asked" %}<strong title="{{ post.added_at }}">{% diff_date post.added_at %}</strong> - {% else %} - {% trans "updated" %}<strong title="{{ post.last_edited_at }}">{% diff_date revision.revised_at %}</strong> - {% endifequal %} - </td> - - </tr> - <tr> - <td width="35px" style="vertical-align:bottom; "> - {% gravatar revision.author 32 %} - </td> - <td style="width:120px; vertical-align:top"> - <div style="height:18px"> - <a href="{% url users %}{{ revision.author.id }}/{{ revision.author.username }}">{{ revision.author.username }}</a></div> - <div> - {% get_score_badge revision.author %} - </div> - </td> - </tr> - - </table> + {% post_contributor_info revision %} </div> </td> </tr> diff --git a/templates/tag_selector.html b/templates/tag_selector.html new file mode 100644 index 00000000..6edc5cc8 --- /dev/null +++ b/templates/tag_selector.html @@ -0,0 +1,42 @@ +{% load i18n %} +{% load extra_tags %} +<div id="tagSelector" class="boxC"> + <h3 class="subtitle">{% trans "Interesting tags" %}</h3> + <div class="tags interesting marked-tags"> + {% for tag_name in interesting_tag_names %} + {% spaceless %} + <span class="deletable-tag" id="interesting-tag-{{tag_name}}"> + <a rel="tag" + title="{% blocktrans with tag as tagname %}see questions tagged '{{ tag_name }}'{% endblocktrans %}" + href="{% url tag_questions tag_name|urlencode %}">{{tag_name}}</a> + <img class="delete-icon" + src="{% href "/content/images/close-small-dark.png" %}" + title="{% blocktrans %}remove '{{tag_name}}' from the list of interesting tags{% endblocktrans %}"/> + </span> + {% endspaceless %} + {% endfor %} + </div> + <input id="interestingTagInput" autocomplete="off" type="text"/> + <input id="interestingTagAdd" type="submit" value="{% trans "Add" %}"/> + <h3 class="subtitle">{% trans "Ignored tags" %}</h3> + <div class="tags ignored marked-tags"> + {% for tag_name in ignored_tag_names %} + {% spaceless %} + <span class="deletable-tag" id="ignored-tag-{{tag_name}}"> + <a rel="tag" + title="{% blocktrans with tag as tagname %}see questions tagged '{{ tag_name }}'{% endblocktrans %}" + href="{% url tag_questions tag_name|urlencode %}">{{tag_name}}</a> + <img class="delete-icon" + src="{% href "/content/images/close-small-dark.png" %}" + title="{% blocktrans %}remove '{{tag_name}}' from the list of ignored tags{% endblocktrans %}"/> + </span> + {% endspaceless %} + {% endfor %} + </div> + <input id="ignoredTagInput" autocomplete="off" type="text"/> + <input id="ignoredTagAdd" type="submit" value="{% trans "Add" %}"/> + <p id="hideIgnoredTagsControl"> + <input id="hideIgnoredTagsCb" type="checkbox" {% if request.user.hide_ignored_questions %}checked="checked"{% endif %} /> + <label id="hideIgnoredTagsLabel" for="hideIgnoredTags">{% trans "keep ingored questions hidden" %}</label> + <p> +</div> diff --git a/templates/tags.html b/templates/tags.html index f558594a..1bde187f 100644 --- a/templates/tags.html +++ b/templates/tags.html @@ -49,7 +49,7 @@ <a href="{% url forum.views.tag tag|urlencode %}" title="{% trans "see questions tagged" %}'{{ tag }}'{% trans "using tags" %}" rel="tag"> {{ tag }} </a> - <span class="tag-number">× {{ tag.used_count|intcomma }}</span> + <span class="tag-number">× {{ tag.used_count|intcomma }}</span> <br/> </li> diff --git a/templates/unanswered.html b/templates/unanswered.html index a56d3ae3..80a23631 100644 --- a/templates/unanswered.html +++ b/templates/unanswered.html @@ -4,6 +4,7 @@ {% load i18n %} {% load humanize %} {% load extra_filters %} +{% load smart_if %} {% block title %}{% spaceless %}{% trans "Unanswered questions" %}{% endspaceless %}{% endblock %} {% block forejs %} <script type="text/javascript"> @@ -17,65 +18,82 @@ <div class="tabBar"> <span class="headQuestions">{% trans "Unanswered questions" %}</span> <div class="tabsA"> - <a id="latest" href="?sort=latest" class="on" title="{% trans "most recently asked questions" %}">{% trans "newest" %}</a> + <a id="latest" href="{% url unanswered %}?sort=latest" class="on" title="{% trans "most recently asked questions" %}">{% trans "newest" %}</a> </div> </div> - <div id="listA"> {% for question in questions.object_list %} <div class="qstA"> - <h2><a href="{{ question.get_absolute_url }}">{{ question.get_question_title }}</a></h2> + <h2> + <a href="{{ question.get_absolute_url }}">{{ question.get_question_title }}</a> + </h2> <div class="stat"> <table> - <tr> + <tr> <td><span class="num">{{ question.answer_count|intcomma }}</span> </td> <td><span class="num">{{ question.score|intcomma }}</span> </td> <td><span class="num">{{ question.view_count|cnprog_intword|safe }}</span> </td> - </tr> + </tr> <tr> <td><span class="unit">{% trans "answers" %}</span></td> <td><span class="unit">{% trans "votes" %}</span></td> <td><span class="unit">{% trans "views" %}</span></td> - </tr> + </tr> </table> </div> + <div class="summary"> {{ question.summary }}... </div> - + {% ifequal tab_id 'active'%} - {% if question.wiki %} + {% if question.wiki and settings.WIKI_ON %} <span class="from wiki">{% trans "community wiki" %}</span> <span class="date" title="{{ question.added_at }}">{% diff_date question.added_at %}</span> {% else %} <div class="from"> - {% comment %}{% gravatar question.last_activity_by 24 %}{% endcomment %} + {% comment %}{% gravatar question.last_activity_by 24 %}{% endcomment %} <span class="author"><a href="{{ question.last_activity_by.get_profile_url }}">{{ question.last_activity_by }}</a></span> <span class="score">{% get_score_badge question.last_activity_by %} </span> <span class="date" title="{{ question.last_activity_at }}">{% diff_date question.last_activity_at %}</span> </div> {% endif %} {% else %} - {% if question.wiki %} + {% if question.wiki and settings.WIKI_ON %} <span class="from wiki">{% trans "community wiki" %}</span> <span class="date" title="{{ question.added_at }}">{% diff_date question.added_at %}</span> {% else %} <div class="from"> - {% comment %}{% gravatar question.author 24 %}{% endcomment %} - <span class="author"><a href="{{ question.author.get_profile_url }}">{{ question.author }}</a></span> - <span class="score">{% get_score_badge question.author %} </span> - <span class="date" title="{{ question.added_at }}">{% diff_date question.added_at %}</span> + {% comment %}{% gravatar question.author 24 %}{% endcomment %} + {% if question.last_activity_at != question.added_at %} + {% if question.author.id != question.last_activity_by.id %} + {% trans "Posted:" %} + <span class="author"><a href="{{ question.author.get_profile_url }}">{{ question.author }}</a></span> + <span class="score">{% get_score_badge question.author %} </span> + / {% trans "Updated:" %} + <span class="author"><a href="{{ question.last_activity_by.get_profile_url }}">{{ question.last_activity_by }}</a></span> + <span class="score">{% get_score_badge question.last_activity_by %} </span> + <span class="date" title="{{ question.last_activity_at }}">{% diff_date question.last_activity_at %}</span> + {% else %} + {% trans "Updated:" %} + <span class="author"><a href="{{ question.last_activity_by.get_profile_url }}">{{ question.last_activity_by }}</a></span> + <span class="score">{% get_score_badge question.last_activity_by %} </span> + <span class="date" title="{{ question.last_activity_at }}">{% diff_date question.last_activity_at %}</span> + {% endif %} + {% else %} + {% trans "Posted:" %} + <span class="author"><a href="{{ question.author.get_profile_url }}">{{ question.author }}</a></span> + <span class="score">{% get_score_badge question.author %} </span> + <span class="date" title="{{ question.added_at }}">{% diff_date question.added_at %}</span> + {% endif %} </div> {% endif %} {% endifequal %} <div class="tags"> - {% for tag in question.tagname_list %} - <a href="{% url forum.views.tag tag|urlencode %}" - title="{% trans "see questions tagged" %}'{{ tag }}'{% trans "using tags" %}" - rel="tag">{{ tag }} - </a> - {% endfor %} + {% for tag in question.tagname_list %} + <a href="{% url forum.views.tag tag|urlencode %}" title="{% trans "see questions tagged" %}'{{ tag }}'{% trans "using tags" %}" rel="tag">{{ tag }}</a> + {% endfor %} </div> </div> {% endfor %} @@ -93,10 +111,8 @@ {% block sidebar %} <div class="boxC"> - <p> - {% blocktrans with questions_count|intcomma as num_q %}have {{num_q}} unanswered questions{% endblocktrans %} - <!---<p>问题按 <strong>问题创建时间</strong> 排序。最新加入的问题将显示在最前面。</p>--> - </p> + {% blocktrans with questions_count|intcomma as num_q %}have {{num_q}} unanswered questions{% endblocktrans %} + <!---<p>问题按 <strong>问题创建时间</strong> 排序。最新加入的问题将显示在最前面。</p>--> </div> <div class="boxC"> <h3 class="subtitle">{% trans "Related tags" %}</h3> @@ -104,7 +120,7 @@ <div class="tags"> {% for tag in tags %} <a rel="tag" title="{% trans "see questions tagged"%}'{{ tag.name }}'{% trans "using tags" %}" href="{% url forum.views.tag tag.name|urlencode %}">{{ tag.name }}</a> - <span class="tag-number"> ✕ {{ tag.used_count|intcomma }}</span> + <span class="tag-number"> × {{ tag.used_count|intcomma }}</span> <br/> {% endfor %} <br/> diff --git a/templates/upfiles/1245715031297631.png b/templates/upfiles/1245715031297631.png Binary files differdeleted file mode 100755 index 89a6aed4..00000000 --- a/templates/upfiles/1245715031297631.png +++ /dev/null diff --git a/templates/upfiles/12457157052552259.png b/templates/upfiles/12457157052552259.png Binary files differdeleted file mode 100755 index 89a6aed4..00000000 --- a/templates/upfiles/12457157052552259.png +++ /dev/null diff --git a/templates/user.html b/templates/user.html index efca80e6..6e4098e9 100644 --- a/templates/user.html +++ b/templates/user.html @@ -1,6 +1,7 @@ {% extends "base_content.html" %} <!-- user.html --> {% load extra_tags %} +{% load extra_filters %} {% load humanize %} {% block title %}{% spaceless %}{{ page_title }}{% endspaceless %}{% endblock %} {% block forestyle%} @@ -10,17 +11,22 @@ </style> {% endblock %} {% block forejs %} - <script type="text/javascript"> - $().ready(function(){ - {% ifequal view_user request.user%} - $("#nav_profile").attr('className',"on"); - {% else %} - $("#nav_users").attr('className',"on"); - {% endifequal %} - }); - </script> - {% block userjs %} - {% endblock %} + {% if request.user|can_moderate_users %} + <script type='text/javascript' src='{% href "/content/js/com.cnprog.admin.js" %}'></script> + <script type='text/javascript' src='{% href "/content/js/jquery.form.js" %}'></script> + {% endif %} + <script type="text/javascript"> + var viewUserID = {{view_user.id}}; + $().ready(function(){ + {% ifequal view_user request.user%} + $("#nav_profile").attr('className',"on"); + {% else %} + $("#nav_users").attr('className',"on"); + {% endifequal %} + }); + </script> + {% block userjs %} + {% endblock %} {% endblock %} {% block content %} <div id="mainbar-full"> @@ -28,6 +34,6 @@ {% include "user_tabs.html" %} {% block usercontent %} {% endblock %} - {% include "user_footer.html" %} + {%comment%}{% include "user_footer.html" %}{%endcomment%} </div> {% endblock %}<!-- end user.html --> diff --git a/templates/user_edit.html b/templates/user_edit.html index b49cea31..5886c071 100644 --- a/templates/user_edit.html +++ b/templates/user_edit.html @@ -24,11 +24,11 @@ {% if request.user.email %} {% gravatar request.user 128 %} {% else %} - <img src="/content/images/nophoto.png"> + <img src="{% href "/content/images/nophoto.png" %}"> {% endif %} <div style="padding:20px 0 0 20px;font-weight:bold;font-size:150%"> <a href="http://www.gravatar.com/" target="_blank" - title="gravatar {% trans "image associated with your email address" %}">{% trans "avatar" %}</a> + title="gravatar {% trans "image associated with your email address" %}">{% blocktrans %}avatar, see {{gravatar_faq_url}}{% endblocktrans %}</a> </div> </div> @@ -39,6 +39,10 @@ <th width="100px"></th> <th></th> </tr> + <tr style="height:35px"> + <td>{{ form.username.label_tag }}:</td> + <td>{{ form.username }} <span class="form-error"></span> {{ form.username.errors }} </td> + </tr> <tr style="height:35px"> <td>{{ form.email.label_tag }}:</td> diff --git a/templates/user_email_subscriptions.html b/templates/user_email_subscriptions.html new file mode 100644 index 00000000..10440529 --- /dev/null +++ b/templates/user_email_subscriptions.html @@ -0,0 +1,29 @@ +{% extends "user.html" %} +<!-- user_email_subscriptions.html --> +{% load i18n %} +{% load extra_tags %} +{% load humanize %} + +{% block usercontent %} + <h2>{% trans "Email subscription settings" %}</h2> + <p class="message">{% trans "email subscription settings info" %}</p> + <div class='inline-block'> + {% if action_status %} + <p class="action-status"><span>{{action_status}}</span></p> + {% endif %} + <form method="POST"> + {% include "edit_user_email_feeds_form.html" %} +<<<<<<< HEAD:templates/user_email_subscriptions.html +======= + <table class='form-as-table'> + {{tag_filter_selection_form}} + </table> +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/user_email_subscriptions.html + <div class="submit-row text-align-right"> + <input type="submit" class="submit" name="save" value="{% trans "Update" %}"/> + <input type="submit" class="submit" name="stop_email" value="{% trans "Stop sending email" %}"/> + </div> + </form> + </div> +{% endblock %} +<!-- end user_email_subscriptions.html --> diff --git a/templates/user_favorites.html b/templates/user_favorites.html index 185423c6..9db01e9a 100644 --- a/templates/user_favorites.html +++ b/templates/user_favorites.html @@ -2,7 +2,6 @@ <!-- user_favorites.html --> {% load extra_tags %} {% load humanize %} - {% block usercontent %} {% include "users_questions.html" %} {% endblock %} diff --git a/templates/user_info.html b/templates/user_info.html index e56fb143..4ebcddd6 100644 --- a/templates/user_info.html +++ b/templates/user_info.html @@ -2,12 +2,11 @@ {% load extra_tags %} {% load extra_filters %} {% load humanize %} +{% load smart_if %} {% load i18n %} <div id="subheader" class="headUser"> {{view_user.username}} - </div> - <table class="user-info-table"> <tr> <td width="180" style="vertical-align:middle;text-align:center;"> @@ -20,20 +19,41 @@ <tr> <td align="center"> <div class="scoreNumber">{{view_user.reputation|intcomma}}</div> - <p><b style="color:#777;">{% trans "reputation" %}</b></p> + <p><b style="color:#777;">{% trans "karma" %}</b></p> </td> </tr> </table> </td> <td width="360" style="vertical-align: top;"> <table class="user-details"> + {% if view_user != request.user and request.user|can_moderate_users %} <tr> - <th width="130" align="left"><strong>{% trans "Registered user" %}</strong></th> - <th width="230" align="right"> - {% if request.user|can_view_user_edit:view_user %} - <span class="user-edit-link"><a href="{% url users %}{{ view_user.id }}/{% trans "edit/" %}">{% trans "update profile" %}</a></span> - {% endif %} - </th> + <td class="admin" align="left" colspan="2"> + <h3>{% trans "Moderate this user" %}</h3> + <form id="moderate_user_form" method="post"> + {{moderate_user_form.as_p}} + </form> + <p id="action_status"></p> + </td> + </tr> + {% endif %} + {% if request.user|can_view_user_edit:view_user %} + <tr> + <td class="user-profile-tool-links" align="left" colspan="2"> + {% joinitems using ' | ' %} + {% if request.user|can_view_user_edit:view_user %} + <span class="user-edit-link"><a href="{% url users %}{{ view_user.id }}/{% trans "edit/" %}">{% trans "update profile" %}</a></span> + {% endif %} + {% separator %} + {% if request.user.has_usable_password %} + <a href="{% url user_changepw %}">change password</a> + {% endif %} + {% endjoinitems %} + </td> + </tr> + {% endif %} + <tr> + <th colspan="2" align="left"><h3>{% trans "Registered user" %}</h3></th> </tr> {% if view_user.real_name %} <tr> @@ -43,12 +63,12 @@ {% endif %} <tr> <td>{% trans "member for" %}</td> - <td>{{ view_user.date_joined|timesince }}</td> + <td><strong>{% diff_date view_user.date_joined %}</strong></td> </tr> {% if view_user.last_seen %} <tr> <td>{% trans "last seen" %}</td> - <td><strong title="{{ view_user.last_seen }}">{{ view_user.last_seen|timesince }} {% trans "ago" %}</strong></td> + <td><strong title="{{ view_user.last_seen }}">{% diff_date view_user.last_seen %}</strong></td> </tr> {% endif %} {% if view_user.website %} diff --git a/templates/user_preferences.html b/templates/user_preferences.html deleted file mode 100644 index 00129c28..00000000 --- a/templates/user_preferences.html +++ /dev/null @@ -1,24 +0,0 @@ -{% extends "user.html" %} -<!-- user_preferences.html --> -{% load i18n %} -{% load extra_tags %} -{% load humanize %} - -{% block usercontent %} - <div style="padding:5px;"> - <fieldset> - <legend><b>{% trans "Connect with Twitter" %}</b></legend> - <!-- todo: form action needs to be fixed --> - <form name="twittersync" action="/sdfgsdlgjkhsdfljh"> - <label for="name">{% trans "Twitter account name:" %}</label> - <input id="name" /><br/> - <label for="password">{% trans "Twitter password:" %}</label> - <input id="password" type="password"/><br/> - <input id="cbMessage" type="checkbox" />{% trans "Send my Questions to Twitter" %}<br/> - <input id="cbReply" type="checkbox" />{% trans "Send my Answers to Twitter" %}<br/> - <input type="submit" value="{% trans "Save" %}" /> - </form> - </fieldset> - </div> -{% endblock %} -<!-- end user_preferences.html --> diff --git a/templates/user_recent.html b/templates/user_recent.html index 7f6c9c8c..b704ab25 100644 --- a/templates/user_recent.html +++ b/templates/user_recent.html @@ -2,7 +2,6 @@ <!-- user_recent.html --> {% load extra_tags %} {% load humanize %} - {% block usercontent %} <div style="padding-top:5px;font-size:13px;"> {% for act in activities %} diff --git a/templates/user_reputation.html b/templates/user_reputation.html index 5b0e542b..16127140 100644 --- a/templates/user_reputation.html +++ b/templates/user_reputation.html @@ -1,10 +1,11 @@ {% extends "user.html" %} <!-- user_reputation.html --> {% load extra_tags %} +{% load extra_filters %} {% load humanize %} {% block userjs %} - <script type='text/javascript' src='/content/js/excanvas.pack.js'></script> - <script type='text/javascript' src='/content/js/jquery.flot.pack.js'></script> + <script type='text/javascript' src='{% href "/content/js/excanvas.pack.js" %}'></script> + <script type='text/javascript' src='{% href "/content/js/jquery.flot.pack.js" %}'></script> <script type="text/javascript"> $().ready(function(){ @@ -33,7 +34,7 @@ <div style="float:left;width:20px;color:red">{{ rep.negative }}</div> </div> - <a href="{% url questions %}{{ rep.question_id }}/{{ rep.title|slugify }}">{{ rep.title }}</a> <span class="small">({{ rep.reputed_at }})</span> + <a href="{% url question rep.question_id %}{{ rep.title|slugify }}">{{ rep.title }}</a> <span class="small">({{ rep.reputed_at }})</span> </p> {% endfor %} </div> diff --git a/templates/user_stats.html b/templates/user_stats.html index b9c33e9f..ecc39807 100644 --- a/templates/user_stats.html +++ b/templates/user_stats.html @@ -7,39 +7,74 @@ {% block usercontent %} <a name="questions"></a> - <h2><span class="count">{{questions|length}}</span> {% trans "User questions" %}</h2> + {% spaceless %} + <h2> + {% blocktrans count questions|length as counter %} + <span class="count">1</span> Question + {% plural %} + <span class="count">{{counter}}</span> Questions + {% endblocktrans %} + </h2> + {% endspaceless %} {% include "users_questions.html" %} <a name="answers"></a> - <h2><span class="count">{{answered_questions|length}}</span> {% trans "Answers" %}</h2> + {% spaceless %} + <h2> + {% blocktrans count answered_questions|length as counter %} + <span class="count">1</span> Answer + {% plural %} + <span class="count">{{counter}}</span> Answers + {% endblocktrans %} + </h2> + {% endspaceless %} <div class="user-stats-table"> {% for answered_question in answered_questions %} <div class="answer-summary"> <a title="{{answered_question.summary|collapse}}" - href="{% url questions %}{{answered_question.id}}/{{answered_question.title}}#{{answered_question.answer_id}}"> + href="{% url question answered_question.id %}{{answered_question.title|slugify}}#{{answered_question.answer_id}}"> <span class="answer-votes {% if answered_question.accepted %}answered-accepted{% endif %}" title="{% blocktrans with answered_question.vote_count as vote_count %}the answer has been voted for {{ vote_count }} times{% endblocktrans %} {% if answered_question.accepted %}{% trans "this answer has been selected as correct" %}{%endif%}"> {{ answered_question.vote_count }} </span> </a> <div class="answer-link"> - <a href="/questions/{{answered_question.id}}/{{answered_question.title}}#{{answered_question.answer_id}}">{{answered_question.title}}</a> {% if answered_question.comment_count %}<span - title="{% blocktrans with answered_question.comment_count as count %}the answer has been commented {{count}} times{% endblocktrans %}">({{answered_question.comment_count}})</span>{% endif %} + {% spaceless %} + <a href="{% url question answered_question.id %}{{answered_question.title|slugify}}#{{answered_question.answer_id}}">{{answered_question.title}}</a> + {% endspaceless %} + {% if answered_question.comment_count %} + <span> + {% blocktrans count answered_question.comment_count as comment_count %} + (one comment) + {% plural %} + the answer has been commented {{comment_count}} times + {% endblocktrans %} + </span> + {% endif %} </div> </div> {% endfor %} </div> + <br/> <a name="votes"></a> - <h2><span class="count">{{total_votes}}</span> {% trans "Votes" %}</h2> + {% spaceless %} + <h2> + {% blocktrans count total_votes as cnt %} + <span class="count">1</span> Vote + {% plural %} + <span class="count">{{cnt}}</span> Votes + {% endblocktrans %} + </h2> + {% endspaceless %} <div class="user-stats-table"> <table> <tr> <td width="60"> - <img style="cursor: default;" src="/content/images/vote-arrow-up-on.png" alt="{% trans "thumb up" %}" /> + <img style="cursor: default;" src="{% href "/content/images/vote-arrow-up-on.png" %}" alt="{% trans "thumb up" %}" /> <span title="{% trans "user has voted up this many times" %}" class="vote-count">{{up_votes}}</span> </td> <td width="60"> - <img style="cursor: default;" src="/content/images/vote-arrow-down-on.png" alt="{% trans "thumb down" %}" /> + <img style="cursor: default;" src="{% href "/content/images/vote-arrow-down-on.png" %}" alt="{% trans "thumb down" %}" /> <span title="{% trans "user voted down this many times" %}" class="vote-count">{{down_votes}}</span> </td> @@ -47,15 +82,24 @@ </table> </div> <a name="tags"></a> - <h2><span class="count">{{tags|length}}</span> {% trans "Tags" %}</h2> + {% spaceless %} + <h2> + {% blocktrans count user_tags|length as counter %} + <span class="count">1</span> Tag + {% plural %} + <span class="count">{{counter}}</span> Tags + {% endblocktrans %} + </h2> + {% endspaceless %} <div class="user-stats-table"> <table class="tags"> <tr> <td width="180" valign="top"> - {% for tag in tags%} + {% for tag in user_tags%} <a rel="tag" - title="{% blocktrans %}see other questions tagged '{{ tag }}' {% endblocktrans %}" - href="{% url forum.views.tag tag|urlencode %}">{{tag.name}}</a><span class="tag-number"> × {{ tag.used_count|intcomma }}</span><br/> + title="{% blocktrans with tag.name as tag_name %}see other questions with {{view_user}}'s contributions tagged '{{ tag_name }}' {% endblocktrans %}" + href="{% url forum.views.tag tag|urlencode %}?user={{view_user.username}}">{{tag.name}}</a> + <span class="tag-number">× {{ tag.user_tag_usage_count|intcomma }}</span><br/> {% if forloop.counter|divisibleby:"10" %} </td> <td width="180" valign="top"> @@ -66,7 +110,15 @@ </table> </div> <a name="badges"></a> - <h2><span class="count">{{total_awards}}</span> {% trans "Badges" %}</h2> + {% spaceless %} + <h2> + {% blocktrans count total_awards as counter %} + <span class="count">1</span> Badge + {% plural %} + <span class="count">{{counter}}</span> Badges + {% endblocktrans %} + </h2> + {% endspaceless %} <div class="user-stats-table"> <table> <tr> @@ -82,6 +134,5 @@ </tr> </table> </div> - {% endblock %} <!-- end user_stats.html --> diff --git a/templates/user_tabs.html b/templates/user_tabs.html index cb7e1d2f..908e8430 100644 --- a/templates/user_tabs.html +++ b/templates/user_tabs.html @@ -23,9 +23,9 @@ title="{% trans "questions that user selected as his/her favorite" %}" href="{% url users %}{{view_user.id}}/{{view_user.username}}?sort=favorites">{% trans "favorites" %}</a> {% if request.user|can_view_user_preferences:view_user %} - <a id="preferences" {% ifequal tab_name "preferences" %}class="on"{% endifequal %} - title="{% trans "user preference settings" %}" - href="{% url users %}{{view_user.id}}/{{view_user.username}}?sort=preferences">{% trans "settings" %}</a> + <a id="email_subscriptions" {% ifequal tab_name "email_subscriptions" %}class="on"{% endifequal %} + title="{% trans "email subscription settings" %}" + href="{% url users %}{{view_user.id}}/{{view_user.username}}?sort=email_subscriptions">{% trans "email subscriptions" %}</a> {% endif %} </div> </div> diff --git a/templates/user_votes.html b/templates/user_votes.html index afe5827f..94d7fcbd 100644 --- a/templates/user_votes.html +++ b/templates/user_votes.html @@ -1,6 +1,7 @@ {% extends "user.html" %} <!-- user_votes.html --> {% load extra_tags %} +{% load extra_filters %} {% load humanize %} {% load i18n %} @@ -11,16 +12,16 @@ <div style="width:150px;float:left">{% diff_date vote.voted_at 3 %}</div> <div style="width:30px;float:left"> {% ifequal vote.vote 1 %} - <img src="/content/images/vote-arrow-up-on.png" title="{% trans "upvote" %}"> + <img src="{% href "/content/images/vote-arrow-up-on.png" %}" title="{% trans "upvote" %}"> {% else %} - <img src="/content/images/vote-arrow-down-on.png" title="{% trans "downvote" %}"> + <img src="{% href "/content/images/vote-arrow-down-on.png" %}" title="{% trans "downvote" %}"> {% endifequal %} </div> <div style="float:left;overflow:hidden;width:750px"> {% ifequal vote.answer_id 0 %} - <span class="question-title-link"><a href="{% url questions %}{{ vote.question_id }}/{{ vote.title|slugify }}">{{ vote.title }}</a></span> + <span class="question-title-link"><a href="{% url question vote.question_id %}{{ vote.title|slugify }}">{{ vote.title }}</a></span> {% else %} - <span class="answer-title-link" ><a href="{% url questions %}{{ vote.question_id }}/{{ vote.title|slugify }}#{{ vote.answer_id }}">{{ vote.title }}</a></span> + <span class="answer-title-link" ><a href="{% url question vote.question_id %}{{ vote.title|slugify }}#{{ vote.answer_id }}">{{ vote.title }}</a></span> {% endifequal %} <div style="height:5px"></div> </div> diff --git a/templates/users.html b/templates/users.html index 966596fc..3a59b0c0 100644 --- a/templates/users.html +++ b/templates/users.html @@ -23,10 +23,10 @@ <div class="tabBar"> <div class="headUsers">{% trans "Users" %}</div> <div class="tabsA"> - <a id="sort_reputation" href="?sort=reputation" class="off" title="{% trans "reputation" %}">{% trans "reputation" %}</a> - <a id="sort_newest" href="?sort=newest" class="off" title="{% trans "recent" %}">{% trans "recent" %}</a> - <a id="sort_last" href="?sort=last" class="off" title="{% trans "oldest" %}">{% trans "oldest" %}</a> - <a id="sort_user" href="?sort=user" class="off" title="{% trans "by username" %}">{% trans "by username" %}</a> + <a id="sort_reputation" href="{% url users %}?sort=reputation" class="off" title="{% trans "reputation" %}">{% trans "reputation" %}</a> + <a id="sort_newest" href="{% url users %}?sort=newest" class="off" title="{% trans "recent" %}">{% trans "recent" %}</a> + <a id="sort_last" href="{% url users %}?sort=last" class="off" title="{% trans "oldest" %}">{% trans "oldest" %}</a> + <a id="sort_user" href="{% url users %}?sort=user" class="off" title="{% trans "by username" %}">{% trans "by username" %}</a> </div> </div> <div id="main-body" style="width:100%"> diff --git a/templates/users_questions.html b/templates/users_questions.html index 7a00d82d..b445a74c 100644 --- a/templates/users_questions.html +++ b/templates/users_questions.html @@ -4,20 +4,20 @@ {% load humanize %} {% load i18n %} <div class="user-stats-table"> - {% for question in questions%} + {% for question in questions %} {% if question.favourite_count %} {% if question.favorited_myself %} <div class="favorites-count"> <img title="{% trans "this questions was selected as favorite" %} {{question.favourite_count}} {% trans "number of times" %}" alt="{% trans "thumb-up on" %}" - src="/content/images/vote-favorite-on.png"/> + src="{% href "/content/images/vote-favorite-on.png" %}"/> <div><b>{{question.favourite_count|intcomma}}</b></div> </div> {% else %} <div class="favorites-count-off"> <img title="{% trans "this question was selected as favorite" %}{{question.favourite_count}} {% trans "number of times" %}" alt="{% trans "thumb-up off" %}" - src="/content/images/vote-favorite-off.png"/> + src="{% href "/content/images/vote-favorite-off.png" %}"/> <div><b>{{question.favourite_count|intcomma}}</b></div> </div> {% endif %} @@ -25,28 +25,26 @@ <div class="favorites-empty"> </div> {% endif %} <div id="question-summary-{{question.id}}" class="question-summary narrow"> - <a style="text-decoration: none;" href="{{ question.get_absolute_url}}"> - <span class="stats"> - <span class="votes"> - <span class="vote-count-post">{{question.vote_count|intcomma}}</span> + <a style="text-decoration: none;" href="{% url question id=question.id %}{{question.title|slugify}}"> + <div class="stats"> + <div class="votes"> + <span class="vote-count-post">{{question.vote_count|intcomma}}</span> {% trans "votes" %} - - </span> - <span title="{% if question.answer_accepted %}{% trans "this answer has been accepted to be correct" %}{% endif %}" class="status {% if question.answer_accepted %}answered-accepted{% endif %} {% ifequal question.answer_count 0 %}unanswered{% endifequal %}{% ifnotequal question.answer_count 0 %}answered{% endifnotequal %}"> + </div > + <div title="{% if question.answer_accepted %}{% trans "this answer has been accepted to be correct" %}{% endif %}" class="status {% if question.answer_accepted %}answered-accepted{% endif %} {% ifequal question.answer_count 0 %}unanswered{% endifequal %}{% ifnotequal question.answer_count 0 %}answered{% endifnotequal %}"> <span class="answer-count-post">{{question.answer_count|intcomma}}</span> {% trans "answers" %} - - </span> - <span class="views"> + </div> + <div class="views"> <span class="views-count-post">{{question.view_count|cnprog_intword|safe}}</span> {% trans "views" %} - </span> - </span> + </div> + </div> </a> <div class="summary"> - <h3> - <a title="{{question.summary}}" href="{% url question question.id%}{{question.title|slugify}}">{{question.title}}</a> - </h3> + <div class="question-title"> + <a title="{{question.summary}}" href="{% url question id=question.id %}{{question.title|slugify}}">{{question.title}}</a> + </div> <div class="tags"> {% convert2tagname_list question %} {% for tag in question.tagnames %} @@ -1,70 +1,7 @@ -import os.path from django.conf.urls.defaults import * -from django.contrib import admin -from forum.views import index -from forum import views as app -from forum.feed import RssLastestQuestionsFeed from django.utils.translation import ugettext as _ +import settings -admin.autodiscover() -feeds = { - 'rss': RssLastestQuestionsFeed -} - -APP_PATH = os.path.dirname(__file__) urlpatterns = patterns('', - (r'^$', 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<path>.*)$', 'django.views.static.serve', - {'document_root': os.path.join(APP_PATH, 'templates/content').replace('\\','/')} - ), - (r'^%s(?P<path>.*)$' % _('upfiles/'), 'django.views.static.serve', - {'document_root': os.path.join(APP_PATH, 'templates/upfiles').replace('\\','/')} - ), - (r'^%s' % _('account/'), include('django_authopenid.urls')), - (r'^%s/$' % _('signin/'), 'django_authopenid.views.signin'), - (r'^%s$' % _('signup/'), 'django_authopenid.views.signup'), - url(r'^%s%s$' % (_('email/'), _('change/')), 'django_authopenid.views.changeemail', name='user_changeemail'), - url(r'^%s%s$' % (_('email/'), _('sendkey/')), 'django_authopenid.views.send_email_key'), - url(r'^%s%s(?P<id>\d+)/(?P<key>[\dabcdef]{32})/$' % (_('email/'), _('verify/')), 'django_authopenid.views.verifyemail', name='user_verifyemail'), - 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<id>\d+)/%s$' % (_('answers/'), _('comments/')), app.answer_comments, name='answer_comments'), - url(r'^answers/(?P<id>\d+)/comments/$', app.answer_comments, name='answer_comments'), - url(r'^%s(?P<id>\d+)/%s$' % (_('answers/'), _('edit/')), app.edit_answer, name='edit_answer'), - url(r'^%s(?P<id>\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<id>\d+)/%s$' % (_('questions/'), _('edit/')), app.edit_question, name='edit_question'), - url(r'^%s(?P<id>\d+)/%s$' % (_('questions/'), _('close/')), app.close, name='close'), - url(r'^%s(?P<id>\d+)/%s$' % (_('questions/'), _('reopen/')), app.reopen, name='reopen'), - url(r'^%s(?P<id>\d+)/%s$' % (_('questions/'), _('answer/')), app.answer, name='answer'), - url(r'^%s(?P<id>\d+)/%s$' % (_('questions/'), _('vote/')), app.vote, name='vote'), - url(r'^%s(?P<id>\d+)/%s$' % (_('questions/'), _('revisions/')), app.question_revisions, name='question_revisions'), - url(r'^%s(?P<id>\d+)/%s$' % (_('questions/'), _('comments/')), app.question_comments, name='question_comments'), - url(r'^questions/(?P<id>\d+)/comments/$', app.question_comments, name='question_comments'), - url(r'^%s(?P<question_id>\d+)/%s(?P<comment_id>\d+)/%s$' % (_('questions/'), _('questions/'),_('delete/')), app.delete_question_comment, name='delete_question_comment'), - url(r'^%s(?P<answer_id>\d+)/%s(?P<comment_id>\d+)/%s$' % (_('answers/'), _('answers/'),_('delete/')), app.delete_answer_comment, name='delete_answer_comment'), - #place general question item in the end of other operations - url(r'^%s(?P<id>\d+)//*' % _('question/'), app.question, name='question'), - url(r'^%s$' % _('tags/'), app.tags, name='tags'), - url(r'^%s(?P<tag>[^/]+)/$' % _('tags/'), app.tag), - url(r'^%s$' % _('users/'),app.users, name='users'), - url(r'^%s(?P<id>\d+)/%s$' % (_('users/'), _('edit/')), app.edit_user, name='edit_user'), - url(r'^%s(?P<id>\d+)//*' % _('users/'), app.user, name='user'), - url(r'^%s$' % _('badges/'),app.badges, name='badges'), - url(r'^%s(?P<id>\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<url>.*)/$', '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<short_name>[^/]+)/$' % (_('books/'), _('ask/')), app.ask_book, name='ask_book'), - url(r'^%s(?P<short_name>[^/]+)/$' % _('books/'), app.book, name='book'), - url(r'^%s$' % _('search/'), app.search, name='search'), + (r'^%s' % settings.FORUM_SCRIPT_ALIAS, include('forum.urls')), ) diff --git a/user_messages/__init__.py b/user_messages/__init__.py new file mode 100644 index 00000000..0136c888 --- /dev/null +++ b/user_messages/__init__.py @@ -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/user_messages/context_processors.py b/user_messages/context_processors.py new file mode 100644 index 00000000..894f5801 --- /dev/null +++ b/user_messages/context_processors.py @@ -0,0 +1,52 @@ +""" +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 user_messages import get_and_delete_messages + +def user_messages (request): + """ + Returns session messages for the current session. + + """ + messages = request.user.get_and_delete_messages() + #if request.user.is_authenticated(): + #else: + # messages = LazyMessages(request) + return { 'user_messages': messages } + +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/user_messages/models.py b/user_messages/models.py new file mode 100644 index 00000000..b67ead6d --- /dev/null +++ b/user_messages/models.py @@ -0,0 +1,3 @@ +""" +blank models.py +""" diff --git a/utils/decorators.py b/utils/decorators.py new file mode 100644 index 00000000..e4e7acb3 --- /dev/null +++ b/utils/decorators.py @@ -0,0 +1,25 @@ +from django.http import HttpResponse, HttpResponseForbidden, Http404 +from django.utils import simplejson + +def ajax_login_required(view_func): + def wrap(request,*args,**kwargs): + if request.user.is_authenticated(): + return view_func(request,*args,**kwargs) + else: + json = simplejson.dumps({'login_required':True}) + return HttpResponseForbidden(json,mimetype='application/json') + return wrap + +def ajax_method(view_func): + def wrap(request,*args,**kwargs): + if not request.is_ajax(): + raise Http404 + retval = view_func(request,*args,**kwargs) + if isinstance(retval, HttpResponse): + retval.mimetype = 'application/json' + return retval + else: + json = simplejson.dumps(retval) + return HttpResponse(json,mimetype='application/json') + return wrap + diff --git a/utils/odict.py b/utils/odict.py new file mode 100644 index 00000000..2c8391d7 --- /dev/null +++ b/utils/odict.py @@ -0,0 +1,1399 @@ +# odict.py +# An Ordered Dictionary object +# Copyright (C) 2005 Nicola Larosa, Michael Foord +# E-mail: nico AT tekNico DOT net, fuzzyman AT voidspace DOT org DOT uk + +# This software is licensed under the terms of the BSD license. +# http://www.voidspace.org.uk/python/license.shtml +# Basically you're free to copy, modify, distribute and relicense it, +# So long as you keep a copy of the license with it. + +# Documentation at http://www.voidspace.org.uk/python/odict.html +# For information about bugfixes, updates and support, please join the +# Pythonutils mailing list: +# http://groups.google.com/group/pythonutils/ +# Comments, suggestions and bug reports welcome. + +"""A dict that keeps keys in insertion order""" +from __future__ import generators + +__author__ = ('Nicola Larosa <nico-NoSp@m-tekNico.net>,' + 'Michael Foord <fuzzyman AT voidspace DOT org DOT uk>') + +__docformat__ = "restructuredtext en" + +__revision__ = '$Id: odict.py 129 2005-09-12 18:15:28Z teknico $' + +__version__ = '0.2.2' + +__all__ = ['OrderedDict', 'SequenceOrderedDict'] + +import sys +INTP_VER = sys.version_info[:2] +if INTP_VER < (2, 2): + raise RuntimeError("Python v.2.2 or later required") + +import types, warnings + +class OrderedDict(dict): + """ + A class of dictionary that keeps the insertion order of keys. + + All appropriate methods return keys, items, or values in an ordered way. + + All normal dictionary methods are available. Update and comparison is + restricted to other OrderedDict objects. + + Various sequence methods are available, including the ability to explicitly + mutate the key ordering. + + __contains__ tests: + + >>> d = OrderedDict(((1, 3),)) + >>> 1 in d + 1 + >>> 4 in d + 0 + + __getitem__ tests: + + >>> OrderedDict(((1, 3), (3, 2), (2, 1)))[2] + 1 + >>> OrderedDict(((1, 3), (3, 2), (2, 1)))[4] + Traceback (most recent call last): + KeyError: 4 + + __len__ tests: + + >>> len(OrderedDict()) + 0 + >>> len(OrderedDict(((1, 3), (3, 2), (2, 1)))) + 3 + + get tests: + + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d.get(1) + 3 + >>> d.get(4) is None + 1 + >>> d.get(4, 5) + 5 + >>> d + OrderedDict([(1, 3), (3, 2), (2, 1)]) + + has_key tests: + + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d.has_key(1) + 1 + >>> d.has_key(4) + 0 + """ + + def __init__(self, init_val=(), strict=False): + """ + Create a new ordered dictionary. Cannot init from a normal dict, + nor from kwargs, since items order is undefined in those cases. + + If the ``strict`` keyword argument is ``True`` (``False`` is the + default) then when doing slice assignment - the ``OrderedDict`` you are + assigning from *must not* contain any keys in the remaining dict. + + >>> OrderedDict() + OrderedDict([]) + >>> OrderedDict({1: 1}) + Traceback (most recent call last): + TypeError: undefined order, cannot get items from dict + >>> OrderedDict({1: 1}.items()) + OrderedDict([(1, 1)]) + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d + OrderedDict([(1, 3), (3, 2), (2, 1)]) + >>> OrderedDict(d) + OrderedDict([(1, 3), (3, 2), (2, 1)]) + """ + self.strict = strict + dict.__init__(self) + if isinstance(init_val, OrderedDict): + self._sequence = init_val.keys() + dict.update(self, init_val) + elif isinstance(init_val, dict): + # we lose compatibility with other ordered dict types this way + raise TypeError('undefined order, cannot get items from dict') + else: + self._sequence = [] + self.update(init_val) + +### Special methods ### + + def __delitem__(self, key): + """ + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> del d[3] + >>> d + OrderedDict([(1, 3), (2, 1)]) + >>> del d[3] + Traceback (most recent call last): + KeyError: 3 + >>> d[3] = 2 + >>> d + OrderedDict([(1, 3), (2, 1), (3, 2)]) + >>> del d[0:1] + >>> d + OrderedDict([(2, 1), (3, 2)]) + """ + if isinstance(key, types.SliceType): + # FIXME: efficiency? + keys = self._sequence[key] + for entry in keys: + dict.__delitem__(self, entry) + del self._sequence[key] + else: + # do the dict.__delitem__ *first* as it raises + # the more appropriate error + dict.__delitem__(self, key) + self._sequence.remove(key) + + def __eq__(self, other): + """ + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d == OrderedDict(d) + True + >>> d == OrderedDict(((1, 3), (2, 1), (3, 2))) + False + >>> d == OrderedDict(((1, 0), (3, 2), (2, 1))) + False + >>> d == OrderedDict(((0, 3), (3, 2), (2, 1))) + False + >>> d == dict(d) + False + >>> d == False + False + """ + if isinstance(other, OrderedDict): + # FIXME: efficiency? + # Generate both item lists for each compare + return (self.items() == other.items()) + else: + return False + + def __lt__(self, other): + """ + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> c = OrderedDict(((0, 3), (3, 2), (2, 1))) + >>> c < d + True + >>> d < c + False + >>> d < dict(c) + Traceback (most recent call last): + TypeError: Can only compare with other OrderedDicts + """ + if not isinstance(other, OrderedDict): + raise TypeError('Can only compare with other OrderedDicts') + # FIXME: efficiency? + # Generate both item lists for each compare + return (self.items() < other.items()) + + def __le__(self, other): + """ + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> c = OrderedDict(((0, 3), (3, 2), (2, 1))) + >>> e = OrderedDict(d) + >>> c <= d + True + >>> d <= c + False + >>> d <= dict(c) + Traceback (most recent call last): + TypeError: Can only compare with other OrderedDicts + >>> d <= e + True + """ + if not isinstance(other, OrderedDict): + raise TypeError('Can only compare with other OrderedDicts') + # FIXME: efficiency? + # Generate both item lists for each compare + return (self.items() <= other.items()) + + def __ne__(self, other): + """ + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d != OrderedDict(d) + False + >>> d != OrderedDict(((1, 3), (2, 1), (3, 2))) + True + >>> d != OrderedDict(((1, 0), (3, 2), (2, 1))) + True + >>> d == OrderedDict(((0, 3), (3, 2), (2, 1))) + False + >>> d != dict(d) + True + >>> d != False + True + """ + if isinstance(other, OrderedDict): + # FIXME: efficiency? + # Generate both item lists for each compare + return not (self.items() == other.items()) + else: + return True + + def __gt__(self, other): + """ + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> c = OrderedDict(((0, 3), (3, 2), (2, 1))) + >>> d > c + True + >>> c > d + False + >>> d > dict(c) + Traceback (most recent call last): + TypeError: Can only compare with other OrderedDicts + """ + if not isinstance(other, OrderedDict): + raise TypeError('Can only compare with other OrderedDicts') + # FIXME: efficiency? + # Generate both item lists for each compare + return (self.items() > other.items()) + + def __ge__(self, other): + """ + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> c = OrderedDict(((0, 3), (3, 2), (2, 1))) + >>> e = OrderedDict(d) + >>> c >= d + False + >>> d >= c + True + >>> d >= dict(c) + Traceback (most recent call last): + TypeError: Can only compare with other OrderedDicts + >>> e >= d + True + """ + if not isinstance(other, OrderedDict): + raise TypeError('Can only compare with other OrderedDicts') + # FIXME: efficiency? + # Generate both item lists for each compare + return (self.items() >= other.items()) + + def __repr__(self): + """ + Used for __repr__ and __str__ + + >>> r1 = repr(OrderedDict((('a', 'b'), ('c', 'd'), ('e', 'f')))) + >>> r1 + "OrderedDict([('a', 'b'), ('c', 'd'), ('e', 'f')])" + >>> r2 = repr(OrderedDict((('a', 'b'), ('e', 'f'), ('c', 'd')))) + >>> r2 + "OrderedDict([('a', 'b'), ('e', 'f'), ('c', 'd')])" + >>> r1 == str(OrderedDict((('a', 'b'), ('c', 'd'), ('e', 'f')))) + True + >>> r2 == str(OrderedDict((('a', 'b'), ('e', 'f'), ('c', 'd')))) + True + """ + return '%s([%s])' % (self.__class__.__name__, ', '.join( + ['(%r, %r)' % (key, self[key]) for key in self._sequence])) + + def __setitem__(self, key, val): + """ + Allows slice assignment, so long as the slice is an OrderedDict + >>> d = OrderedDict() + >>> d['a'] = 'b' + >>> d['b'] = 'a' + >>> d[3] = 12 + >>> d + OrderedDict([('a', 'b'), ('b', 'a'), (3, 12)]) + >>> d[:] = OrderedDict(((1, 2), (2, 3), (3, 4))) + >>> d + OrderedDict([(1, 2), (2, 3), (3, 4)]) + >>> d[::2] = OrderedDict(((7, 8), (9, 10))) + >>> d + OrderedDict([(7, 8), (2, 3), (9, 10)]) + >>> d = OrderedDict(((0, 1), (1, 2), (2, 3), (3, 4))) + >>> d[1:3] = OrderedDict(((1, 2), (5, 6), (7, 8))) + >>> d + OrderedDict([(0, 1), (1, 2), (5, 6), (7, 8), (3, 4)]) + >>> d = OrderedDict(((0, 1), (1, 2), (2, 3), (3, 4)), strict=True) + >>> d[1:3] = OrderedDict(((1, 2), (5, 6), (7, 8))) + >>> d + OrderedDict([(0, 1), (1, 2), (5, 6), (7, 8), (3, 4)]) + + >>> a = OrderedDict(((0, 1), (1, 2), (2, 3)), strict=True) + >>> a[3] = 4 + >>> a + OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)]) + >>> a[::1] = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)]) + >>> a + OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)]) + >>> a[:2] = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)]) + Traceback (most recent call last): + ValueError: slice assignment must be from unique keys + >>> a = OrderedDict(((0, 1), (1, 2), (2, 3))) + >>> a[3] = 4 + >>> a + OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)]) + >>> a[::1] = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)]) + >>> a + OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)]) + >>> a[:2] = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)]) + >>> a + OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)]) + >>> a[::-1] = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)]) + >>> a + OrderedDict([(3, 4), (2, 3), (1, 2), (0, 1)]) + + >>> d = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)]) + >>> d[:1] = 3 + Traceback (most recent call last): + TypeError: slice assignment requires an OrderedDict + + >>> d = OrderedDict([(0, 1), (1, 2), (2, 3), (3, 4)]) + >>> d[:1] = OrderedDict([(9, 8)]) + >>> d + OrderedDict([(9, 8), (1, 2), (2, 3), (3, 4)]) + """ + if isinstance(key, types.SliceType): + if not isinstance(val, OrderedDict): + # FIXME: allow a list of tuples? + raise TypeError('slice assignment requires an OrderedDict') + keys = self._sequence[key] + # NOTE: Could use ``range(*key.indices(len(self._sequence)))`` + indexes = range(len(self._sequence))[key] + if key.step is None: + # NOTE: new slice may not be the same size as the one being + # overwritten ! + # NOTE: What is the algorithm for an impossible slice? + # e.g. d[5:3] + pos = key.start or 0 + del self[key] + newkeys = val.keys() + for k in newkeys: + if k in self: + if self.strict: + raise ValueError('slice assignment must be from ' + 'unique keys') + else: + # NOTE: This removes duplicate keys *first* + # so start position might have changed? + del self[k] + self._sequence = (self._sequence[:pos] + newkeys + + self._sequence[pos:]) + dict.update(self, val) + else: + # extended slice - length of new slice must be the same + # as the one being replaced + if len(keys) != len(val): + raise ValueError('attempt to assign sequence of size %s ' + 'to extended slice of size %s' % (len(val), len(keys))) + # FIXME: efficiency? + del self[key] + item_list = zip(indexes, val.items()) + # smallest indexes first - higher indexes not guaranteed to + # exist + item_list.sort() + for pos, (newkey, newval) in item_list: + if self.strict and newkey in self: + raise ValueError('slice assignment must be from unique' + ' keys') + self.insert(pos, newkey, newval) + else: + if key not in self: + self._sequence.append(key) + dict.__setitem__(self, key, val) + + def __getitem__(self, key): + """ + Allows slicing. Returns an OrderedDict if you slice. + >>> b = OrderedDict([(7, 0), (6, 1), (5, 2), (4, 3), (3, 4), (2, 5), (1, 6)]) + >>> b[::-1] + OrderedDict([(1, 6), (2, 5), (3, 4), (4, 3), (5, 2), (6, 1), (7, 0)]) + >>> b[2:5] + OrderedDict([(5, 2), (4, 3), (3, 4)]) + >>> type(b[2:4]) + <class '__main__.OrderedDict'> + """ + if isinstance(key, types.SliceType): + # FIXME: does this raise the error we want? + keys = self._sequence[key] + # FIXME: efficiency? + return OrderedDict([(entry, self[entry]) for entry in keys]) + else: + return dict.__getitem__(self, key) + + __str__ = __repr__ + + def __setattr__(self, name, value): + """ + Implemented so that accesses to ``sequence`` raise a warning and are + diverted to the new ``setkeys`` method. + """ + if name == 'sequence': + warnings.warn('Use of the sequence attribute is deprecated.' + ' Use the keys method instead.', DeprecationWarning) + # NOTE: doesn't return anything + self.setkeys(value) + else: + # FIXME: do we want to allow arbitrary setting of attributes? + # Or do we want to manage it? + object.__setattr__(self, name, value) + + def __getattr__(self, name): + """ + Implemented so that access to ``sequence`` raises a warning. + + >>> d = OrderedDict() + >>> d.sequence + [] + """ + if name == 'sequence': + warnings.warn('Use of the sequence attribute is deprecated.' + ' Use the keys method instead.', DeprecationWarning) + # NOTE: Still (currently) returns a direct reference. Need to + # because code that uses sequence will expect to be able to + # mutate it in place. + return self._sequence + else: + # raise the appropriate error + raise AttributeError("OrderedDict has no '%s' attribute" % name) + + def __deepcopy__(self, memo): + """ + To allow deepcopy to work with OrderedDict. + + >>> from copy import deepcopy + >>> a = OrderedDict([(1, 1), (2, 2), (3, 3)]) + >>> a['test'] = {} + >>> b = deepcopy(a) + >>> b == a + True + >>> b is a + False + >>> a['test'] is b['test'] + False + """ + from copy import deepcopy + return self.__class__(deepcopy(self.items(), memo), self.strict) + + +### Read-only methods ### + + def copy(self): + """ + >>> OrderedDict(((1, 3), (3, 2), (2, 1))).copy() + OrderedDict([(1, 3), (3, 2), (2, 1)]) + """ + return OrderedDict(self) + + def items(self): + """ + ``items`` returns a list of tuples representing all the + ``(key, value)`` pairs in the dictionary. + + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d.items() + [(1, 3), (3, 2), (2, 1)] + >>> d.clear() + >>> d.items() + [] + """ + return zip(self._sequence, self.values()) + + def keys(self): + """ + Return a list of keys in the ``OrderedDict``. + + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d.keys() + [1, 3, 2] + """ + return self._sequence[:] + + def values(self, values=None): + """ + Return a list of all the values in the OrderedDict. + + Optionally you can pass in a list of values, which will replace the + current list. The value list must be the same len as the OrderedDict. + + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d.values() + [3, 2, 1] + """ + return [self[key] for key in self._sequence] + + def iteritems(self): + """ + >>> ii = OrderedDict(((1, 3), (3, 2), (2, 1))).iteritems() + >>> ii.next() + (1, 3) + >>> ii.next() + (3, 2) + >>> ii.next() + (2, 1) + >>> ii.next() + Traceback (most recent call last): + StopIteration + """ + def make_iter(self=self): + keys = self.iterkeys() + while True: + key = keys.next() + yield (key, self[key]) + return make_iter() + + def iterkeys(self): + """ + >>> ii = OrderedDict(((1, 3), (3, 2), (2, 1))).iterkeys() + >>> ii.next() + 1 + >>> ii.next() + 3 + >>> ii.next() + 2 + >>> ii.next() + Traceback (most recent call last): + StopIteration + """ + return iter(self._sequence) + + __iter__ = iterkeys + + def itervalues(self): + """ + >>> iv = OrderedDict(((1, 3), (3, 2), (2, 1))).itervalues() + >>> iv.next() + 3 + >>> iv.next() + 2 + >>> iv.next() + 1 + >>> iv.next() + Traceback (most recent call last): + StopIteration + """ + def make_iter(self=self): + keys = self.iterkeys() + while True: + yield self[keys.next()] + return make_iter() + +### Read-write methods ### + + def clear(self): + """ + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d.clear() + >>> d + OrderedDict([]) + """ + dict.clear(self) + self._sequence = [] + + def pop(self, key, *args): + """ + No dict.pop in Python 2.2, gotta reimplement it + + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d.pop(3) + 2 + >>> d + OrderedDict([(1, 3), (2, 1)]) + >>> d.pop(4) + Traceback (most recent call last): + KeyError: 4 + >>> d.pop(4, 0) + 0 + >>> d.pop(4, 0, 1) + Traceback (most recent call last): + TypeError: pop expected at most 2 arguments, got 3 + """ + if len(args) > 1: + raise TypeError, ('pop expected at most 2 arguments, got %s' % + (len(args) + 1)) + if key in self: + val = self[key] + del self[key] + else: + try: + val = args[0] + except IndexError: + raise KeyError(key) + return val + + def popitem(self, i=-1): + """ + Delete and return an item specified by index, not a random one as in + dict. The index is -1 by default (the last item). + + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d.popitem() + (2, 1) + >>> d + OrderedDict([(1, 3), (3, 2)]) + >>> d.popitem(0) + (1, 3) + >>> OrderedDict().popitem() + Traceback (most recent call last): + KeyError: 'popitem(): dictionary is empty' + >>> d.popitem(2) + Traceback (most recent call last): + IndexError: popitem(): index 2 not valid + """ + if not self._sequence: + raise KeyError('popitem(): dictionary is empty') + try: + key = self._sequence[i] + except IndexError: + raise IndexError('popitem(): index %s not valid' % i) + return (key, self.pop(key)) + + def setdefault(self, key, defval = None): + """ + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d.setdefault(1) + 3 + >>> d.setdefault(4) is None + True + >>> d + OrderedDict([(1, 3), (3, 2), (2, 1), (4, None)]) + >>> d.setdefault(5, 0) + 0 + >>> d + OrderedDict([(1, 3), (3, 2), (2, 1), (4, None), (5, 0)]) + """ + if key in self: + return self[key] + else: + self[key] = defval + return defval + + def update(self, from_od): + """ + Update from another OrderedDict or sequence of (key, value) pairs + + >>> d = OrderedDict(((1, 0), (0, 1))) + >>> d.update(OrderedDict(((1, 3), (3, 2), (2, 1)))) + >>> d + OrderedDict([(1, 3), (0, 1), (3, 2), (2, 1)]) + >>> d.update({4: 4}) + Traceback (most recent call last): + TypeError: undefined order, cannot get items from dict + >>> d.update((4, 4)) + Traceback (most recent call last): + TypeError: cannot convert dictionary update sequence element "4" to a 2-item sequence + """ + if isinstance(from_od, OrderedDict): + for key, val in from_od.items(): + self[key] = val + elif isinstance(from_od, dict): + # we lose compatibility with other ordered dict types this way + raise TypeError('undefined order, cannot get items from dict') + else: + # FIXME: efficiency? + # sequence of 2-item sequences, or error + for item in from_od: + try: + key, val = item + except TypeError: + raise TypeError('cannot convert dictionary update' + ' sequence element "%s" to a 2-item sequence' % item) + self[key] = val + + def rename(self, old_key, new_key): + """ + Rename the key for a given value, without modifying sequence order. + + For the case where new_key already exists this raise an exception, + since if new_key exists, it is ambiguous as to what happens to the + associated values, and the position of new_key in the sequence. + + >>> od = OrderedDict() + >>> od['a'] = 1 + >>> od['b'] = 2 + >>> od.items() + [('a', 1), ('b', 2)] + >>> od.rename('b', 'c') + >>> od.items() + [('a', 1), ('c', 2)] + >>> od.rename('c', 'a') + Traceback (most recent call last): + ValueError: New key already exists: 'a' + >>> od.rename('d', 'b') + Traceback (most recent call last): + KeyError: 'd' + """ + if new_key == old_key: + # no-op + return + if new_key in self: + raise ValueError("New key already exists: %r" % new_key) + # rename sequence entry + value = self[old_key] + old_idx = self._sequence.index(old_key) + self._sequence[old_idx] = new_key + # rename internal dict entry + dict.__delitem__(self, old_key) + dict.__setitem__(self, new_key, value) + + def setitems(self, items): + """ + This method allows you to set the items in the dict. + + It takes a list of tuples - of the same sort returned by the ``items`` + method. + + >>> d = OrderedDict() + >>> d.setitems(((3, 1), (2, 3), (1, 2))) + >>> d + OrderedDict([(3, 1), (2, 3), (1, 2)]) + """ + self.clear() + # FIXME: this allows you to pass in an OrderedDict as well :-) + self.update(items) + + def setkeys(self, keys): + """ + ``setkeys`` all ows you to pass in a new list of keys which will + replace the current set. This must contain the same set of keys, but + need not be in the same order. + + If you pass in new keys that don't match, a ``KeyError`` will be + raised. + + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d.keys() + [1, 3, 2] + >>> d.setkeys((1, 2, 3)) + >>> d + OrderedDict([(1, 3), (2, 1), (3, 2)]) + >>> d.setkeys(['a', 'b', 'c']) + Traceback (most recent call last): + KeyError: 'Keylist is not the same as current keylist.' + """ + # FIXME: Efficiency? (use set for Python 2.4 :-) + # NOTE: list(keys) rather than keys[:] because keys[:] returns + # a tuple, if keys is a tuple. + kcopy = list(keys) + kcopy.sort() + self._sequence.sort() + if kcopy != self._sequence: + raise KeyError('Keylist is not the same as current keylist.') + # NOTE: This makes the _sequence attribute a new object, instead + # of changing it in place. + # FIXME: efficiency? + self._sequence = list(keys) + + def setvalues(self, values): + """ + You can pass in a list of values, which will replace the + current list. The value list must be the same len as the OrderedDict. + + (Or a ``ValueError`` is raised.) + + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d.setvalues((1, 2, 3)) + >>> d + OrderedDict([(1, 1), (3, 2), (2, 3)]) + >>> d.setvalues([6]) + Traceback (most recent call last): + ValueError: Value list is not the same length as the OrderedDict. + """ + if len(values) != len(self): + # FIXME: correct error to raise? + raise ValueError('Value list is not the same length as the ' + 'OrderedDict.') + self.update(zip(self, values)) + +### Sequence Methods ### + + def index(self, key): + """ + Return the position of the specified key in the OrderedDict. + + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d.index(3) + 1 + >>> d.index(4) + Traceback (most recent call last): + ValueError: list.index(x): x not in list + """ + return self._sequence.index(key) + + def insert(self, index, key, value): + """ + Takes ``index``, ``key``, and ``value`` as arguments. + + Sets ``key`` to ``value``, so that ``key`` is at position ``index`` in + the OrderedDict. + + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d.insert(0, 4, 0) + >>> d + OrderedDict([(4, 0), (1, 3), (3, 2), (2, 1)]) + >>> d.insert(0, 2, 1) + >>> d + OrderedDict([(2, 1), (4, 0), (1, 3), (3, 2)]) + >>> d.insert(8, 8, 1) + >>> d + OrderedDict([(2, 1), (4, 0), (1, 3), (3, 2), (8, 1)]) + """ + if key in self: + # FIXME: efficiency? + del self[key] + self._sequence.insert(index, key) + dict.__setitem__(self, key, value) + + def reverse(self): + """ + Reverse the order of the OrderedDict. + + >>> d = OrderedDict(((1, 3), (3, 2), (2, 1))) + >>> d.reverse() + >>> d + OrderedDict([(2, 1), (3, 2), (1, 3)]) + """ + self._sequence.reverse() + + def sort(self, *args, **kwargs): + """ + Sort the key order in the OrderedDict. + + This method takes the same arguments as the ``list.sort`` method on + your version of Python. + + >>> d = OrderedDict(((4, 1), (2, 2), (3, 3), (1, 4))) + >>> d.sort() + >>> d + OrderedDict([(1, 4), (2, 2), (3, 3), (4, 1)]) + """ + self._sequence.sort(*args, **kwargs) + +class Keys(object): + # FIXME: should this object be a subclass of list? + """ + Custom object for accessing the keys of an OrderedDict. + + Can be called like the normal ``OrderedDict.keys`` method, but also + supports indexing and sequence methods. + """ + + def __init__(self, main): + self._main = main + + def __call__(self): + """Pretend to be the keys method.""" + return self._main._keys() + + def __getitem__(self, index): + """Fetch the key at position i.""" + # NOTE: this automatically supports slicing :-) + return self._main._sequence[index] + + def __setitem__(self, index, name): + """ + You cannot assign to keys, but you can do slice assignment to re-order + them. + + You can only do slice assignment if the new set of keys is a reordering + of the original set. + """ + if isinstance(index, types.SliceType): + # FIXME: efficiency? + # check length is the same + indexes = range(len(self._main._sequence))[index] + if len(indexes) != len(name): + raise ValueError('attempt to assign sequence of size %s ' + 'to slice of size %s' % (len(name), len(indexes))) + # check they are the same keys + # FIXME: Use set + old_keys = self._main._sequence[index] + new_keys = list(name) + old_keys.sort() + new_keys.sort() + if old_keys != new_keys: + raise KeyError('Keylist is not the same as current keylist.') + orig_vals = [self._main[k] for k in name] + del self._main[index] + vals = zip(indexes, name, orig_vals) + vals.sort() + for i, k, v in vals: + if self._main.strict and k in self._main: + raise ValueError('slice assignment must be from ' + 'unique keys') + self._main.insert(i, k, v) + else: + raise ValueError('Cannot assign to keys') + + ### following methods pinched from UserList and adapted ### + def __repr__(self): return repr(self._main._sequence) + + # FIXME: do we need to check if we are comparing with another ``Keys`` + # object? (like the __cast method of UserList) + def __lt__(self, other): return self._main._sequence < other + def __le__(self, other): return self._main._sequence <= other + def __eq__(self, other): return self._main._sequence == other + def __ne__(self, other): return self._main._sequence != other + def __gt__(self, other): return self._main._sequence > other + def __ge__(self, other): return self._main._sequence >= other + # FIXME: do we need __cmp__ as well as rich comparisons? + def __cmp__(self, other): return cmp(self._main._sequence, other) + + def __contains__(self, item): return item in self._main._sequence + def __len__(self): return len(self._main._sequence) + def __iter__(self): return self._main.iterkeys() + def count(self, item): return self._main._sequence.count(item) + def index(self, item, *args): return self._main._sequence.index(item, *args) + def reverse(self): self._main._sequence.reverse() + def sort(self, *args, **kwds): self._main._sequence.sort(*args, **kwds) + def __mul__(self, n): return self._main._sequence*n + __rmul__ = __mul__ + def __add__(self, other): return self._main._sequence + other + def __radd__(self, other): return other + self._main._sequence + + ## following methods not implemented for keys ## + def __delitem__(self, i): raise TypeError('Can\'t delete items from keys') + def __iadd__(self, other): raise TypeError('Can\'t add in place to keys') + def __imul__(self, n): raise TypeError('Can\'t multiply keys in place') + def append(self, item): raise TypeError('Can\'t append items to keys') + def insert(self, i, item): raise TypeError('Can\'t insert items into keys') + def pop(self, i=-1): raise TypeError('Can\'t pop items from keys') + def remove(self, item): raise TypeError('Can\'t remove items from keys') + def extend(self, other): raise TypeError('Can\'t extend keys') + +class Items(object): + """ + Custom object for accessing the items of an OrderedDict. + + Can be called like the normal ``OrderedDict.items`` method, but also + supports indexing and sequence methods. + """ + + def __init__(self, main): + self._main = main + + def __call__(self): + """Pretend to be the items method.""" + return self._main._items() + + def __getitem__(self, index): + """Fetch the item at position i.""" + if isinstance(index, types.SliceType): + # fetching a slice returns an OrderedDict + return self._main[index].items() + key = self._main._sequence[index] + return (key, self._main[key]) + + def __setitem__(self, index, item): + """Set item at position i to item.""" + if isinstance(index, types.SliceType): + # NOTE: item must be an iterable (list of tuples) + self._main[index] = OrderedDict(item) + else: + # FIXME: Does this raise a sensible error? + orig = self._main.keys[index] + key, value = item + if self._main.strict and key in self and (key != orig): + raise ValueError('slice assignment must be from ' + 'unique keys') + # delete the current one + del self._main[self._main._sequence[index]] + self._main.insert(index, key, value) + + def __delitem__(self, i): + """Delete the item at position i.""" + key = self._main._sequence[i] + if isinstance(i, types.SliceType): + for k in key: + # FIXME: efficiency? + del self._main[k] + else: + del self._main[key] + + ### following methods pinched from UserList and adapted ### + def __repr__(self): return repr(self._main.items()) + + # FIXME: do we need to check if we are comparing with another ``Items`` + # object? (like the __cast method of UserList) + def __lt__(self, other): return self._main.items() < other + def __le__(self, other): return self._main.items() <= other + def __eq__(self, other): return self._main.items() == other + def __ne__(self, other): return self._main.items() != other + def __gt__(self, other): return self._main.items() > other + def __ge__(self, other): return self._main.items() >= other + def __cmp__(self, other): return cmp(self._main.items(), other) + + def __contains__(self, item): return item in self._main.items() + def __len__(self): return len(self._main._sequence) # easier :-) + def __iter__(self): return self._main.iteritems() + def count(self, item): return self._main.items().count(item) + def index(self, item, *args): return self._main.items().index(item, *args) + def reverse(self): self._main.reverse() + def sort(self, *args, **kwds): self._main.sort(*args, **kwds) + def __mul__(self, n): return self._main.items()*n + __rmul__ = __mul__ + def __add__(self, other): return self._main.items() + other + def __radd__(self, other): return other + self._main.items() + + def append(self, item): + """Add an item to the end.""" + # FIXME: this is only append if the key isn't already present + key, value = item + self._main[key] = value + + def insert(self, i, item): + key, value = item + self._main.insert(i, key, value) + + def pop(self, i=-1): + key = self._main._sequence[i] + return (key, self._main.pop(key)) + + def remove(self, item): + key, value = item + try: + assert value == self._main[key] + except (KeyError, AssertionError): + raise ValueError('ValueError: list.remove(x): x not in list') + else: + del self._main[key] + + def extend(self, other): + # FIXME: is only a true extend if none of the keys already present + for item in other: + key, value = item + self._main[key] = value + + def __iadd__(self, other): + self.extend(other) + + ## following methods not implemented for items ## + + def __imul__(self, n): raise TypeError('Can\'t multiply items in place') + +class Values(object): + """ + Custom object for accessing the values of an OrderedDict. + + Can be called like the normal ``OrderedDict.values`` method, but also + supports indexing and sequence methods. + """ + + def __init__(self, main): + self._main = main + + def __call__(self): + """Pretend to be the values method.""" + return self._main._values() + + def __getitem__(self, index): + """Fetch the value at position i.""" + if isinstance(index, types.SliceType): + return [self._main[key] for key in self._main._sequence[index]] + else: + return self._main[self._main._sequence[index]] + + def __setitem__(self, index, value): + """ + Set the value at position i to value. + + You can only do slice assignment to values if you supply a sequence of + equal length to the slice you are replacing. + """ + if isinstance(index, types.SliceType): + keys = self._main._sequence[index] + if len(keys) != len(value): + raise ValueError('attempt to assign sequence of size %s ' + 'to slice of size %s' % (len(name), len(keys))) + # FIXME: efficiency? Would be better to calculate the indexes + # directly from the slice object + # NOTE: the new keys can collide with existing keys (or even + # contain duplicates) - these will overwrite + for key, val in zip(keys, value): + self._main[key] = val + else: + self._main[self._main._sequence[index]] = value + + ### following methods pinched from UserList and adapted ### + def __repr__(self): return repr(self._main.values()) + + # FIXME: do we need to check if we are comparing with another ``Values`` + # object? (like the __cast method of UserList) + def __lt__(self, other): return self._main.values() < other + def __le__(self, other): return self._main.values() <= other + def __eq__(self, other): return self._main.values() == other + def __ne__(self, other): return self._main.values() != other + def __gt__(self, other): return self._main.values() > other + def __ge__(self, other): return self._main.values() >= other + def __cmp__(self, other): return cmp(self._main.values(), other) + + def __contains__(self, item): return item in self._main.values() + def __len__(self): return len(self._main._sequence) # easier :-) + def __iter__(self): return self._main.itervalues() + def count(self, item): return self._main.values().count(item) + def index(self, item, *args): return self._main.values().index(item, *args) + + def reverse(self): + """Reverse the values""" + vals = self._main.values() + vals.reverse() + # FIXME: efficiency + self[:] = vals + + def sort(self, *args, **kwds): + """Sort the values.""" + vals = self._main.values() + vals.sort(*args, **kwds) + self[:] = vals + + def __mul__(self, n): return self._main.values()*n + __rmul__ = __mul__ + def __add__(self, other): return self._main.values() + other + def __radd__(self, other): return other + self._main.values() + + ## following methods not implemented for values ## + def __delitem__(self, i): raise TypeError('Can\'t delete items from values') + def __iadd__(self, other): raise TypeError('Can\'t add in place to values') + def __imul__(self, n): raise TypeError('Can\'t multiply values in place') + def append(self, item): raise TypeError('Can\'t append items to values') + def insert(self, i, item): raise TypeError('Can\'t insert items into values') + def pop(self, i=-1): raise TypeError('Can\'t pop items from values') + def remove(self, item): raise TypeError('Can\'t remove items from values') + def extend(self, other): raise TypeError('Can\'t extend values') + +class SequenceOrderedDict(OrderedDict): + """ + Experimental version of OrderedDict that has a custom object for ``keys``, + ``values``, and ``items``. + + These are callable sequence objects that work as methods, or can be + manipulated directly as sequences. + + Test for ``keys``, ``items`` and ``values``. + + >>> d = SequenceOrderedDict(((1, 2), (2, 3), (3, 4))) + >>> d + SequenceOrderedDict([(1, 2), (2, 3), (3, 4)]) + >>> d.keys + [1, 2, 3] + >>> d.keys() + [1, 2, 3] + >>> d.setkeys((3, 2, 1)) + >>> d + SequenceOrderedDict([(3, 4), (2, 3), (1, 2)]) + >>> d.setkeys((1, 2, 3)) + >>> d.keys[0] + 1 + >>> d.keys[:] + [1, 2, 3] + >>> d.keys[-1] + 3 + >>> d.keys[-2] + 2 + >>> d.keys[0:2] = [2, 1] + >>> d + SequenceOrderedDict([(2, 3), (1, 2), (3, 4)]) + >>> d.keys.reverse() + >>> d.keys + [3, 1, 2] + >>> d.keys = [1, 2, 3] + >>> d + SequenceOrderedDict([(1, 2), (2, 3), (3, 4)]) + >>> d.keys = [3, 1, 2] + >>> d + SequenceOrderedDict([(3, 4), (1, 2), (2, 3)]) + >>> a = SequenceOrderedDict() + >>> b = SequenceOrderedDict() + >>> a.keys == b.keys + 1 + >>> a['a'] = 3 + >>> a.keys == b.keys + 0 + >>> b['a'] = 3 + >>> a.keys == b.keys + 1 + >>> b['b'] = 3 + >>> a.keys == b.keys + 0 + >>> a.keys > b.keys + 0 + >>> a.keys < b.keys + 1 + >>> 'a' in a.keys + 1 + >>> len(b.keys) + 2 + >>> 'c' in d.keys + 0 + >>> 1 in d.keys + 1 + >>> [v for v in d.keys] + [3, 1, 2] + >>> d.keys.sort() + >>> d.keys + [1, 2, 3] + >>> d = SequenceOrderedDict(((1, 2), (2, 3), (3, 4)), strict=True) + >>> d.keys[::-1] = [1, 2, 3] + >>> d + SequenceOrderedDict([(3, 4), (2, 3), (1, 2)]) + >>> d.keys[:2] + [3, 2] + >>> d.keys[:2] = [1, 3] + Traceback (most recent call last): + KeyError: 'Keylist is not the same as current keylist.' + + >>> d = SequenceOrderedDict(((1, 2), (2, 3), (3, 4))) + >>> d + SequenceOrderedDict([(1, 2), (2, 3), (3, 4)]) + >>> d.values + [2, 3, 4] + >>> d.values() + [2, 3, 4] + >>> d.setvalues((4, 3, 2)) + >>> d + SequenceOrderedDict([(1, 4), (2, 3), (3, 2)]) + >>> d.values[::-1] + [2, 3, 4] + >>> d.values[0] + 4 + >>> d.values[-2] + 3 + >>> del d.values[0] + Traceback (most recent call last): + TypeError: Can't delete items from values + >>> d.values[::2] = [2, 4] + >>> d + SequenceOrderedDict([(1, 2), (2, 3), (3, 4)]) + >>> 7 in d.values + 0 + >>> len(d.values) + 3 + >>> [val for val in d.values] + [2, 3, 4] + >>> d.values[-1] = 2 + >>> d.values.count(2) + 2 + >>> d.values.index(2) + 0 + >>> d.values[-1] = 7 + >>> d.values + [2, 3, 7] + >>> d.values.reverse() + >>> d.values + [7, 3, 2] + >>> d.values.sort() + >>> d.values + [2, 3, 7] + >>> d.values.append('anything') + Traceback (most recent call last): + TypeError: Can't append items to values + >>> d.values = (1, 2, 3) + >>> d + SequenceOrderedDict([(1, 1), (2, 2), (3, 3)]) + + >>> d = SequenceOrderedDict(((1, 2), (2, 3), (3, 4))) + >>> d + SequenceOrderedDict([(1, 2), (2, 3), (3, 4)]) + >>> d.items() + [(1, 2), (2, 3), (3, 4)] + >>> d.setitems([(3, 4), (2 ,3), (1, 2)]) + >>> d + SequenceOrderedDict([(3, 4), (2, 3), (1, 2)]) + >>> d.items[0] + (3, 4) + >>> d.items[:-1] + [(3, 4), (2, 3)] + >>> d.items[1] = (6, 3) + >>> d.items + [(3, 4), (6, 3), (1, 2)] + >>> d.items[1:2] = [(9, 9)] + >>> d + SequenceOrderedDict([(3, 4), (9, 9), (1, 2)]) + >>> del d.items[1:2] + >>> d + SequenceOrderedDict([(3, 4), (1, 2)]) + >>> (3, 4) in d.items + 1 + >>> (4, 3) in d.items + 0 + >>> len(d.items) + 2 + >>> [v for v in d.items] + [(3, 4), (1, 2)] + >>> d.items.count((3, 4)) + 1 + >>> d.items.index((1, 2)) + 1 + >>> d.items.index((2, 1)) + Traceback (most recent call last): + ValueError: list.index(x): x not in list + >>> d.items.reverse() + >>> d.items + [(1, 2), (3, 4)] + >>> d.items.reverse() + >>> d.items.sort() + >>> d.items + [(1, 2), (3, 4)] + >>> d.items.append((5, 6)) + >>> d.items + [(1, 2), (3, 4), (5, 6)] + >>> d.items.insert(0, (0, 0)) + >>> d.items + [(0, 0), (1, 2), (3, 4), (5, 6)] + >>> d.items.insert(-1, (7, 8)) + >>> d.items + [(0, 0), (1, 2), (3, 4), (7, 8), (5, 6)] + >>> d.items.pop() + (5, 6) + >>> d.items + [(0, 0), (1, 2), (3, 4), (7, 8)] + >>> d.items.remove((1, 2)) + >>> d.items + [(0, 0), (3, 4), (7, 8)] + >>> d.items.extend([(1, 2), (5, 6)]) + >>> d.items + [(0, 0), (3, 4), (7, 8), (1, 2), (5, 6)] + """ + + def __init__(self, init_val=(), strict=True): + OrderedDict.__init__(self, init_val, strict=strict) + self._keys = self.keys + self._values = self.values + self._items = self.items + self.keys = Keys(self) + self.values = Values(self) + self.items = Items(self) + self._att_dict = { + 'keys': self.setkeys, + 'items': self.setitems, + 'values': self.setvalues, + } + + def __setattr__(self, name, value): + """Protect keys, items, and values.""" + if not '_att_dict' in self.__dict__: + object.__setattr__(self, name, value) + else: + try: + fun = self._att_dict[name] + except KeyError: + OrderedDict.__setattr__(self, name, value) + else: + fun(value) + +if __name__ == '__main__': + if INTP_VER < (2, 3): + raise RuntimeError("Tests require Python v.2.3 or later") + # turn off warnings for tests + warnings.filterwarnings('ignore') + # run the code tests in doctest format + import doctest + m = sys.modules.get('__main__') + globs = m.__dict__.copy() + globs.update({ + 'INTP_VER': INTP_VER, + }) + doctest.testmod(m, globs=globs) + |