summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--INSTALL282
-rw-r--r--LICENSE28
-rw-r--r--TODO7
-rw-r--r--cnprog.wsgi7
-rw-r--r--context.py1
-rw-r--r--cron/send_email_alerts4
-rw-r--r--development.log2
-rw-r--r--forum/const.py5
-rw-r--r--forum/feed.py4
-rw-r--r--forum/forms.py27
-rw-r--r--forum/management/commands/send_email_alerts.py150
-rw-r--r--forum/managers.py11
-rw-r--r--forum/models.py93
-rw-r--r--forum/sitemap.py11
-rw-r--r--forum/templatetags/extra_filters.py11
-rw-r--r--forum/templatetags/extra_tags.py9
-rw-r--r--forum/urls.py21
-rw-r--r--forum/views.py575
-rw-r--r--locale/en/LC_MESSAGES/django.po910
-rw-r--r--middleware/__init__.py~HEAD (renamed from log/cnprog.log)0
-rw-r--r--middleware/anon_user.py8
-rw-r--r--settings.py15
-rw-r--r--settings_local.py.dist41
-rw-r--r--sphinx/sphinx.conf127
-rw-r--r--sql_scripts/091208_upgrade_evgeny.sql1
-rw-r--r--sql_scripts/091208_upgrade_evgeny_1.sql1
-rw-r--r--sql_scripts/update_2009_01_25_001.sql4
-rw-r--r--sql_scripts/update_2009_02_26_001.sql38
-rw-r--r--sql_scripts/update_2009_04_10_001.sql6
-rw-r--r--templates/about.html26
-rw-r--r--templates/authopenid/complete.html4
-rw-r--r--templates/authopenid/external_legacy_login_info.html5
-rw-r--r--templates/base.html6
-rw-r--r--templates/base_content.html11
-rw-r--r--templates/content/images/close-small-dark.pngbin0 -> 226 bytes
-rw-r--r--templates/content/js/com.cnprog.admin.js4
-rw-r--r--templates/content/js/com.cnprog.i18n.js3
-rw-r--r--templates/content/js/com.cnprog.post.js78
-rw-r--r--templates/content/js/com.cnprog.tag_selector.js168
-rw-r--r--templates/content/js/com.cnprog.utils.js8
-rw-r--r--templates/content/js/compress.bat11
-rw-r--r--templates/content/js/flot-build.bat6
-rw-r--r--templates/content/js/wmd/wmd.js4
-rw-r--r--templates/content/style/style.css62
-rw-r--r--templates/index.html18
-rw-r--r--templates/question.html25
-rw-r--r--templates/question_edit.html2
-rw-r--r--templates/questions.html131
-rw-r--r--templates/tag_selector.html42
-rw-r--r--templates/user_edit.html4
-rw-r--r--templates/user_email_subscriptions.html6
-rw-r--r--templates/user_reputation.html3
-rw-r--r--templates/user_stats.html10
-rw-r--r--templates/user_votes.html5
-rw-r--r--utils/decorators.py25
-rw-r--r--utils/odict.py1399
57 files changed, 3814 insertions, 653 deletions
diff --git a/.gitignore b/.gitignore
index b217d5cc..69133c46 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,5 @@
*.pyc
*.swp
*.log
-settings_local.py
nbproject
+settings_local.py
diff --git a/INSTALL b/INSTALL
index 35683147..7de10871 100644
--- a/INSTALL
+++ b/INSTALL
@@ -1,35 +1,297 @@
-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
-----------------------------------------------
-1. Python2.5, MySQL, Django v1.0+
+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
2. Python-openid v2.2
http://openidenabled.com/python-openid/
-
-3. django-authopenid(Included in project already)
-http://code.google.com/p/django-authopenid/
+sudo easy_install python-openid
4. html5lib
http://code.google.com/p/html5lib/
Used for HTML sanitizer
+sudo easy_install html5lib
5. Markdown2
http://code.google.com/p/python-markdown2/
+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
-----------------------------------------------
-1. Copy settings_local.py.dist to settings_local.py and
+0. Make sure you have all above python libraries installed.
+
+ 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. Database
-2. Prepare your database by using the same database/account
+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. Running CNPROG on the development server
-3. Run "python manager.py runserver" to startup django
+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>
-4. There are some demo scripts under sql_scripts folder,
+ #this is your wsgi script described in the prev section
+ WSGIScriptAlias / /path/to/cnprog/cnprog.wsgi
+
+ #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
diff --git a/LICENSE b/LICENSE
index cb24f678..803781c5 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,14 +1,14 @@
-Copyright (C) 2009. Chen Gang
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see <http://www.gnu.org/licenses/>.
+Copyright (C) 2009. Chen Gang
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
diff --git a/TODO b/TODO
index 3769fa08..372e714f 100644
--- a/TODO
+++ b/TODO
@@ -1,4 +1,3 @@
-*check change email function - there is a strange 'password' field in form
-*make reusable question-block template for index questions unanswered - get rid of copy-paste
-*unused votes count in user profile not working - left that commented out in templates/user_info.html
-*badge award notification messages need to be set fixed at place where badges are awarded
+* 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
index a3d332f2..bd3745ee 100644
--- a/cnprog.wsgi
+++ b/cnprog.wsgi
@@ -1,7 +1,8 @@
+#example wsgi setup script
import os
import sys
-sys.path.append('/var/www/vhosts/default/htdocs')
-sys.path.append('/var/www/vhosts/default/htdocs/forum')
-os.environ['DJANGO_SETTINGS_MODULE'] = 'forum.settings'
+sys.path.append('/path/above_forum')
+sys.path.append('/path/above_forum/forum_dir')
+os.environ['DJANGO_SETTINGS_MODULE'] = 'forum_dir.settings'
import django.core.handlers.wsgi
application = django.core.handlers.wsgi.WSGIHandler()
diff --git a/context.py b/context.py
index 680d1c3c..26d326a7 100644
--- a/context.py
+++ b/context.py
@@ -14,6 +14,7 @@ def application_settings(context):
'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/forum/const.py b/forum/const.py
index 9b9230c0..76fd4a24 100644
--- a/forum/const.py
+++ b/forum/const.py
@@ -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 373f8a87..ad1d5cbd 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 5b181d48..ad2c5bac 100644
--- a/forum/forms.py
+++ b/forum/forms.py
@@ -4,7 +4,7 @@ from django import forms
from models import *
from const import *
from django.utils.translation import ugettext as _
-from django_authopenid.forms import NextUrlField
+from django_authopenid.forms import NextUrlField, UserNameField
import settings
class TitleField(forms.CharField):
@@ -195,6 +195,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}))
@@ -203,6 +204,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
@@ -230,6 +232,23 @@ class EditUserForm(forms.Form):
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')))
@@ -245,9 +264,6 @@ class EditUserEmailFeedsForm(forms.Form):
'answered_by_me':'n',
'individually_selected':'n',
}
- all_questions = forms.ChoiceField(choices=DWN,initial='w',
- widget=forms.RadioSelect,
- label=_('Entire forum'),)
asked_by_me = forms.ChoiceField(choices=DWN,initial='w',
widget=forms.RadioSelect,
label=_('Asked by me'))
@@ -257,6 +273,9 @@ class EditUserEmailFeedsForm(forms.Form):
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()])
diff --git a/forum/management/commands/send_email_alerts.py b/forum/management/commands/send_email_alerts.py
index a25e4343..283d5683 100644
--- a/forum/management/commands/send_email_alerts.py
+++ b/forum/management/commands/send_email_alerts.py
@@ -2,11 +2,20 @@ from django.core.management.base import NoArgsCommand
from django.db import connection
from django.db.models import Q, F
from forum.models import *
+<<<<<<< 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):
@@ -18,10 +27,17 @@ class Command(NoArgsCommand):
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,
@@ -35,16 +51,28 @@ class Command(NoArgsCommand):
).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
@@ -52,6 +80,7 @@ class Command(NoArgsCommand):
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
@@ -97,14 +126,108 @@ class Command(NoArgsCommand):
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):
+<<<<<<< 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')
@@ -113,6 +236,7 @@ class Command(NoArgsCommand):
% {'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']:
@@ -124,6 +248,32 @@ class Command(NoArgsCommand):
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]}
diff --git a/forum/managers.py b/forum/managers.py
index 795d382e..06fae761 100644
--- a/forum/managers.py
+++ b/forum/managers.py
@@ -7,6 +7,7 @@ from forum.models import *
from urllib import quote, unquote
class QuestionManager(models.Manager):
+<<<<<<< HEAD:forum/managers.py
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
@@ -26,6 +27,8 @@ class QuestionManager(models.Manager):
def get_questions(self, orderby):
questions = self.filter(deleted=False).order_by(orderby)
return questions
+=======
+>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:forum/managers.py
def update_tags(self, question, tagnames, user):
"""
@@ -92,12 +95,20 @@ class QuestionManager(models.Manager):
Questions with the individual tags will be added to list if above questions are not full.
"""
#print datetime.datetime.now()
+<<<<<<< HEAD:forum/managers.py
from forum.models import Question
questions = list(Question.objects.filter(tagnames = question.tagnames, deleted=False).all())
tags_list = question.tags.all()
for tag in tags_list:
extend_questions = Question.objects.filter(tags__id = tag.id, deleted=False)[:50]
+=======
+ questions = list(self.filter(tagnames = question.tagnames, deleted=False).all())
+
+ tags_list = question.tags.all()
+ for tag in tags_list:
+ extend_questions = self.filter(tags__id = tag.id, deleted=False)[:50]
+>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:forum/managers.py
for item in extend_questions:
if item not in questions and len(questions) < 10:
questions.append(item)
diff --git a/forum/models.py b/forum/models.py
index 39058bea..f1876588 100644
--- a/forum/models.py
+++ b/forum/models.py
@@ -3,6 +3,10 @@ import datetime
import hashlib
from urllib import quote_plus, urlencode
from django.db import models, IntegrityError
+<<<<<<< HEAD:forum/models.py
+=======
+from django.utils.http import urlquote as django_urlquote
+>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:forum/models.py
from django.utils.html import strip_tags
from django.core.urlresolvers import reverse
from django.contrib.auth.models import User
@@ -12,8 +16,18 @@ 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
+<<<<<<< HEAD:forum/models.py
import django.dispatch
import settings
+=======
+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
+>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:forum/models.py
from forum.managers import *
from const import *
@@ -96,6 +110,17 @@ class Comment(models.Model):
class Meta:
ordering = ('-added_at',)
db_table = u'comment'
+<<<<<<< HEAD:forum/models.py
+=======
+
+ 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?')
+
+>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:forum/models.py
def __unicode__(self):
return self.comment
@@ -184,8 +209,27 @@ class Question(models.Model):
votes = generic.GenericRelation(Vote)
flagged_items = generic.GenericRelation(FlaggedItem)
+<<<<<<< HEAD:forum/models.py
objects = QuestionManager()
+=======
+ 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?')
+
+>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:forum/models.py
def save(self, **kwargs):
"""
Overridden to manually manage addition of tags when the object
@@ -196,6 +240,13 @@ class Question(models.Model):
"""
initial_addition = (self.id is None)
super(Question, self).save(**kwargs)
+<<<<<<< HEAD:forum/models.py
+=======
+ try:
+ ping_google()
+ except Exception:
+ logging.debug('problem pinging google did you register you sitemap with google?')
+>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:forum/models.py
if initial_addition:
tags = Tag.objects.get_or_create_multiple(self.tagname_list(),
self.author)
@@ -210,7 +261,11 @@ class Question(models.Model):
return u','.join([unicode(tag) for tag in self.tagname_list()])
def get_absolute_url(self):
+<<<<<<< HEAD:forum/models.py
return '%s%s' % (reverse('question', args=[self.id]), slugify(self.title))
+=======
+ return '%s%s' % (reverse('question', args=[self.id]), django_urlquote(slugify(self.title)))
+>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:forum/models.py
def has_favorite_by_user(self, user):
if not user.is_authenticated():
@@ -332,6 +387,15 @@ class FavoriteQuestion(models.Model):
def __unicode__(self):
return '[%s] favorited at %s' %(self.user, self.added_at)
+<<<<<<< HEAD:forum/models.py
+=======
+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)
+
+>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:forum/models.py
class QuestionRevision(models.Model):
"""A revision of a Question."""
question = models.ForeignKey(Question, related_name='revisions')
@@ -435,6 +499,16 @@ class Answer(models.Model):
get_comments = get_object_comments
get_last_update_info = post_get_last_update_info
+<<<<<<< HEAD:forum/models.py
+=======
+ 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?')
+
+>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:forum/models.py
def get_user_vote(self, user):
votes = self.votes.filter(user=user)
if votes.count() > 0:
@@ -449,7 +523,11 @@ class Answer(models.Model):
return self.question.title
def get_absolute_url(self):
+<<<<<<< HEAD:forum/models.py
return '%s%s#%s' % (reverse('question', args=[self.question.id]), slugify(self.question.title), self.id)
+=======
+ return '%s%s#%s' % (reverse('question', args=[self.question.id]), django_urlquote(slugify(self.question.title)), self.id)
+>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:forum/models.py
class Meta:
db_table = u'answer'
@@ -590,7 +668,11 @@ class Book(models.Model):
questions = models.ManyToManyField(Question, related_name='book', db_table='book_question')
def get_absolute_url(self):
+<<<<<<< HEAD:forum/models.py
return reverse('book', args=[self.short_name])
+=======
+ return reverse('book', args=[django_urlquote(slugify(self.short_name))])
+>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:forum/models.py
def __unicode__(self):
return self.title
@@ -680,6 +762,17 @@ 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)
+<<<<<<< HEAD:forum/models.py
+=======
+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'
+ )
+ )
+>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:forum/models.py
# custom signal
tags_updated = django.dispatch.Signal(providing_args=["question"])
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 3644fdc3..865cd33d 100644
--- a/forum/templatetags/extra_filters.py
+++ b/forum/templatetags/extra_filters.py
@@ -1,4 +1,8 @@
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
@@ -91,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 8bd0e128..8ed79d3c 100644
--- a/forum/templatetags/extra_tags.py
+++ b/forum/templatetags/extra_tags.py
@@ -251,7 +251,11 @@ def diff_date(date, limen=2):
return _('2 days ago')
elif days == 1:
return _('yesterday')
+<<<<<<< HEAD:forum/templatetags/extra_tags.py
elif minutes > 60:
+=======
+ elif minutes >= 60:
+>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:forum/templatetags/extra_tags.py
return ungettext('%(hr)d hour ago','%(hr)d hours ago',hours) % {'hr':hours}
else:
return ungettext('%(min)d min ago','%(min)d mins ago',minutes) % {'min':minutes}
@@ -332,8 +336,13 @@ class BlockResourceNode(template.Node):
for item in self.items:
bit = item.render(context)
out += bit
+<<<<<<< HEAD:forum/templatetags/extra_tags.py
out = out.replace(' ','')
return os.path.normpath(out) + '?v=%d' % settings.RESOURCE_REVISION
+=======
+ out = os.path.normpath(out) + '?v=%d' % settings.RESOURCE_REVISION
+ return out.replace(' ','')
+>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:forum/templatetags/extra_tags.py
@register.tag(name='blockresource')
def blockresource(parser,token):
diff --git a/forum/urls.py b/forum/urls.py
index a08fe716..62e70161 100644
--- a/forum/urls.py
+++ b/forum/urls.py
@@ -3,16 +3,21 @@ 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',
@@ -39,6 +44,7 @@ urlpatterns = patterns('',
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'},\
@@ -50,7 +56,20 @@ urlpatterns = patterns('',
#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(?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'),
diff --git a/forum/views.py b/forum/views.py
index e4ccaa16..296745f9 100644
--- a/forum/views.py
+++ b/forum/views.py
@@ -15,12 +15,15 @@ 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
#from lxml.html.diff import htmldiff
from forum.diff import textDiff as htmldiff
@@ -50,7 +53,7 @@ answer_type = ContentType.objects.get_for_model(Answer)
comment_type = ContentType.objects.get_for_model(Comment)
question_revision_type = ContentType.objects.get_for_model(QuestionRevision)
answer_revision_type = ContentType.objects.get_for_model(AnswerRevision)
-repute_type =ContentType.objects.get_for_model(Repute)
+repute_type = ContentType.objects.get_for_model(Repute)
question_type_id = question_type.id
answer_type_id = answer_type.id
comment_type_id = comment_type.id
@@ -61,7 +64,7 @@ def _get_tags_cache_json():
tags = Tag.objects.filter(deleted=False).all()
tags_list = []
for tag in tags:
- dic = {'n': tag.name, 'c': tag.used_count }
+ dic = {'n': tag.name, 'c': tag.used_count}
tags_list.append(dic)
tags = simplejson.dumps(tags_list)
return tags
@@ -87,14 +90,26 @@ def index(request):
}
view_id, orderby = _get_and_remember_questions_sort_method(request, view_dic, 'latest')
- questions = Question.objects.get_questions_by_pagesize(orderby, INDEX_PAGE_SIZE)
+ 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', {
+ 'interesting_tag_names': interesting_tag_names,
+ 'tags_autocomplete': tags_autocomplete,
+ 'ignored_tag_names': ignored_tag_names,
"questions" : questions,
"tab_id" : view_id,
"tags" : tags,
@@ -145,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"
# Set flag to False by default. If it is equal to True, then need to be saved.
pagesize_changed = False
@@ -160,18 +175,61 @@ def questions(request, tagname=None, unanswered=False):
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
@@ -179,13 +237,26 @@ def questions(request, tagname=None, unanswered=False):
related_tags = Tag.objects.get_tags_by_questions(questions.object_list)
else:
related_tags = None
+ 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)
+
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,
@@ -546,7 +617,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
@@ -641,8 +711,8 @@ def edit_answer(request, id):
if revision_form.is_valid():
# Replace with those from the selected revision
form = EditAnswerForm(answer,
- AnswerRevision.objects.get(answer=answer,
- revision=revision_form.cleaned_data['revision']))
+ AnswerRevision.objects.get(answer=answer,
+ revision=revision_form.cleaned_data['revision']))
else:
form = EditAnswerForm(answer, latest_revision, request.POST)
else:
@@ -659,11 +729,11 @@ def edit_answer(request, id):
Answer.objects.filter(id=answer.id).update(**updated_fields)
revision = AnswerRevision(
- answer = answer,
- author = request.user,
- revised_at = edited_at,
- text = form.cleaned_data['text']
- )
+ answer=answer,
+ author=request.user,
+ revised_at=edited_at,
+ text=form.cleaned_data['text']
+ )
if form.cleaned_data['summary']:
revision.summary = form.cleaned_data['summary']
@@ -680,14 +750,15 @@ def edit_answer(request, id):
revision_form = RevisionForm(answer, latest_revision)
form = EditAnswerForm(answer, latest_revision)
return render_to_response('answer_edit.html', {
- 'answer': answer,
- 'revision_form': revision_form,
- 'form' : form,
- }, context_instance=RequestContext(request))
+ 'answer': answer,
+ 'revision_form': revision_form,
+ 'form': form,
+ }, context_instance=RequestContext(request))
QUESTION_REVISION_TEMPLATE = ('<h1>%(title)s</h1>\n'
- '<div class="text">%(html)s</div>\n'
- '<div class="tags">%(tags)s</div>')
+ '<div class="text">%(html)s</div>\n'
+ '<div class="tags">%(tags)s</div>')
+
def question_revisions(request, id):
post = get_object_or_404(Question, id=id)
revisions = list(post.revisions.all())
@@ -706,13 +777,13 @@ def question_revisions(request, id):
'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(' ')]),
+ for tag in revisions[0].tagnames.split(' ')]),
}
revisions[i].summary = _('initial version')
return render_to_response('revisions_question.html', {
- 'post': post,
- 'revisions': revisions,
- }, context_instance=RequestContext(request))
+ 'post': post,
+ 'revisions': revisions,
+ }, context_instance=RequestContext(request))
ANSWER_REVISION_TEMPLATE = ('<div class="text">%(html)s</div>')
def answer_revisions(request, id):
@@ -729,9 +800,9 @@ def answer_revisions(request, id):
revisions[i].diff = revisions[i].text
revisions[i].summary = _('initial version')
return render_to_response('revisions_answer.html', {
- 'post': post,
- 'revisions': revisions,
- }, context_instance=RequestContext(request))
+ 'post': post,
+ 'revisions': revisions,
+ }, context_instance=RequestContext(request))
def answer(request, id):
question = get_object_or_404(Question, id=id)
@@ -744,25 +815,25 @@ def answer(request, id):
if request.user.is_authenticated():
create_new_answer(
- question=question,
- author=request.user,
- added_at=update_time,
- wiki=wiki,
- text=text,
- email_notify=form.cleaned_data['email_notify']
- )
+ question=question,
+ author=request.user,
+ added_at=update_time,
+ wiki=wiki,
+ text=text,
+ email_notify=form.cleaned_data['email_notify']
+ )
else:
request.session.flush()
html = sanitize_html(markdowner.convert(text))
summary = strip_tags(html)[:120]
anon = AnonymousAnswer(
- question = question,
- wiki = wiki,
- text = text,
- summary = summary,
- session_key = request.session.session_key,
- ip_addr = request.META['REMOTE_ADDR'],
- )
+ question=question,
+ wiki=wiki,
+ text=text,
+ summary=summary,
+ session_key=request.session.session_key,
+ ip_addr=request.META['REMOTE_ADDR'],
+ )
anon.save()
return HttpResponseRedirect(reverse('user_signin_new_answer'))
@@ -793,22 +864,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' : reverse('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)
@@ -1042,6 +1112,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')
@@ -1073,22 +1179,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')
@@ -1130,6 +1236,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'])
@@ -1147,9 +1254,9 @@ def edit_user(request, id):
else:
form = EditUserForm(user)
return render_to_response('user_edit.html', {
- 'form' : form,
- 'gravatar_faq_url' : reverse('faq') + '#gravatar',
- }, 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)
@@ -1216,32 +1323,52 @@ def user_stats(request, user_id, user_view):
'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]
- if settings.DJANGO_VERSION < 1.1:
+
+ 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', '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',
+ '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.query.group_by = ['badge_id']
- else:
+ 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', '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',
+ '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()
- from django.db.models import Count
- awards = awards.annotate(count = Count('badge__id'))
+ 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']
if auth.can_moderate_users(request.user):
moderate_user_form = ModerateUserForm(instance=user)
@@ -1249,22 +1376,23 @@ def user_stats(request, user_id, user_view):
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,
- "tags" : tags,
- "awards": awards,
- "total_awards" : total_awards,
- }, context_instance=RequestContext(request))
+ '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)
@@ -1314,7 +1442,7 @@ def user_recent(request, user_id, user_view):
)
if len(questions) > 0:
questions = [(Event(q['active_at'], q['activity_type'], q['title'], '', '0', \
- q['question_id'])) for q in questions]
+ q['question_id'])) for q in questions]
activities.extend(questions)
# answers
@@ -1341,7 +1469,7 @@ def user_recent(request, user_id, user_view):
)
if len(answers) > 0:
answers = [(Event(q['active_at'], q['activity_type'], q['title'], '', q['answer_id'], \
- q['question_id'])) for q in answers]
+ q['question_id'])) for q in answers]
activities.extend(answers)
# question comments
@@ -1369,7 +1497,7 @@ def user_recent(request, user_id, user_view):
if len(comments) > 0:
comments = [(Event(q['added_at'], q['activity_type'], q['title'], '', '0', \
- q['question_id'])) for q in comments]
+ q['question_id'])) for q in comments]
activities.extend(comments)
# answer comments
@@ -1400,7 +1528,7 @@ def user_recent(request, user_id, user_view):
if len(comments) > 0:
comments = [(Event(q['added_at'], q['activity_type'], q['title'], '', q['answer_id'], \
- q['question_id'])) for q in comments]
+ q['question_id'])) for q in comments]
activities.extend(comments)
# question revisions
@@ -1429,7 +1557,7 @@ def user_recent(request, user_id, user_view):
if len(revisions) > 0:
revisions = [(Event(q['added_at'], q['activity_type'], q['title'], q['summary'], '0', \
- q['question_id'])) for q in revisions]
+ q['question_id'])) for q in revisions]
activities.extend(revisions)
# answer revisions
@@ -1462,7 +1590,7 @@ def user_recent(request, user_id, user_view):
if len(revisions) > 0:
revisions = [(Event(q['added_at'], q['activity_type'], q['title'], q['summary'], \
- q['answer_id'], q['question_id'])) for q in revisions]
+ q['answer_id'], q['question_id'])) for q in revisions]
activities.extend(revisions)
# accepted answers
@@ -1514,12 +1642,12 @@ 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))
+ "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):
"""
@@ -1529,7 +1657,7 @@ 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 = reverse('questions') + u'%s/%s#%s' % (question_id, title, answer_id)
+ self.titlelink = reverse('question', args=[question_id]) + u'%s#%s' % (slugify(title), answer_id)
self.time = time
self.userlink = reverse('users') + u'%s/%s/' % (user_id, username)
self.username = username
@@ -1541,30 +1669,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]
@@ -1573,27 +1702,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'],
@@ -1739,16 +1868,27 @@ def user_votes(request, user_id, user_view):
def user_reputation(request, user_id, user_view):
user = get_object_or_404(User, id=user_id)
- reputation = Repute.objects.extra(
- select={'positive': 'sum(positive)', 'negative': 'sum(negative)', 'question_id':'question_id',
- 'title': 'question.title'},
- tables=['repute', 'question'],
- order_by=['-reputed_at'],
- where=['user_id=%s AND question_id=question.id'],
- params=[user.id]
- ).values('positive', 'negative', 'question_id', 'title', 'reputed_at', 'reputation')
-
- reputation.query.group_by = ['question_id']
+ try:
+ from django.db.models import Sum
+ reputation = Repute.objects.extra(
+ select={'question_id':'question_id',
+ 'title': 'question.title'},
+ tables=['repute', 'question'],
+ order_by=['-reputed_at'],
+ where=['user_id=%s AND question_id=question.id'],
+ params=[user.id]
+ ).values('question_id', 'title', 'reputed_at', 'reputation')
+ reputation = reputation.annotate(positive=Sum("positive"), negative=Sum("negative"))
+ except ImportError:
+ reputation = Repute.objects.extra(
+ select={'positive':'sum(positive)', 'negative':'sum(negative)', 'question_id':'question_id',
+ 'title': 'question.title'},
+ tables=['repute', 'question'],
+ order_by=['-reputed_at'],
+ where=['user_id=%s AND question_id=question.id'],
+ params=[user.id]
+ ).values('positive', 'negative', 'question_id', 'title', 'reputed_at', 'reputation')
+ reputation.query.group_by = ['question_id']
rep_list = []
for rep in Repute.objects.filter(user=user).order_by('reputed_at'):
@@ -1757,14 +1897,14 @@ def user_reputation(request, user_id, user_view):
reps = ','.join(rep_list)
reps = '[%s]' % reps
- 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,
- "reputation" : reputation,
- "reps" : reps
- }, 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,
+ "reputation": reputation,
+ "reps": reps
+ }, context_instance=RequestContext(request))
def user_favorites(request, user_id, user_view):
user = get_object_or_404(User, id=user_id)
@@ -1816,34 +1956,39 @@ def user_favorites(request, user_id, user_view):
"view_user" : user
}, context_instance=RequestContext(request))
-
def user_email_subscriptions(request, user_id, user_view):
user = get_object_or_404(User, id=user_id)
if request.method == 'POST':
- form = EditUserEmailFeedsForm(request.POST)
- if form.is_valid():
+ 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:
- saved = form.save(user)
- if saved:
+ feeds_saved = email_feeds_form.save(user)
+ if feeds_saved:
action_status = _('changes saved')
elif 'stop_email' in request.POST:
- saved = form.reset().save(user)
+ email_stopped = email_feeds_form.reset().save(user)
initial_values = EditUserEmailFeedsForm.NO_EMAIL_INITIAL
- form = EditUserEmailFeedsForm(initial=initial_values)
- if saved:
+ email_feeds_form = EditUserEmailFeedsForm(initial=initial_values)
+ if email_stopped:
action_status = _('email updates canceled')
- if not saved:
- action_status = None
else:
- form = EditUserEmailFeedsForm()
- form.set_initial_values(user)
+ 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':form,
+ 'email_feeds_form':email_feeds_form,
+ 'tag_filter_selection_form':tag_filter_form,
'action_status':action_status,
}, context_instance=RequestContext(request))
@@ -1985,7 +2130,7 @@ def upload(request):
if not file_name_suffix in settings.ALLOW_FILE_TYPES:
raise FileTypeNotAllow
- # genetate new file name
+ # 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)
@@ -2022,7 +2167,7 @@ def book(request, short_name, unanswered=False):
"""
books = Book.objects.extra(where=['short_name = %s'], params=[short_name])
match_count = len(books)
- if match_count == 0 :
+ if match_count == 0:
raise Http404
else:
# the book info
@@ -2193,10 +2338,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/locale/en/LC_MESSAGES/django.po b/locale/en/LC_MESSAGES/django.po
index 18eb61b1..3f554733 100644
--- a/locale/en/LC_MESSAGES/django.po
+++ b/locale/en/LC_MESSAGES/django.po
@@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2009-11-10 19:00-0800\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"
@@ -60,87 +60,95 @@ msgstr ""
msgid "this email is already used by someone else, please choose another"
msgstr ""
-#: django_authopenid/forms.py:163 django_authopenid/views.py:117
+#: django_authopenid/forms.py:163 django_authopenid/views.py:118
msgid "i-names are not supported"
msgstr ""
-#: django_authopenid/forms.py:239
+#: django_authopenid/forms.py:219
+msgid "Account with this name already exists on the forum"
+msgstr ""
+
+#: django_authopenid/forms.py:220
+msgid "can't have two logins to the same account yet, sorry."
+msgstr ""
+
+#: django_authopenid/forms.py:242
msgid "Please enter valid username and password (both are case-sensitive)."
msgstr ""
-#: django_authopenid/forms.py:242 django_authopenid/forms.py:292
+#: django_authopenid/forms.py:245 django_authopenid/forms.py:295
msgid "This account is inactive."
msgstr ""
-#: django_authopenid/forms.py:244
+#: django_authopenid/forms.py:247
msgid "Login failed."
msgstr ""
-#: django_authopenid/forms.py:246
+#: django_authopenid/forms.py:249
msgid "Please enter username and password"
msgstr ""
-#: django_authopenid/forms.py:248
+#: django_authopenid/forms.py:251
msgid "Please enter your password"
msgstr ""
-#: django_authopenid/forms.py:250
+#: django_authopenid/forms.py:253
msgid "Please enter user name"
msgstr ""
-#: django_authopenid/forms.py:288
+#: django_authopenid/forms.py:291
msgid ""
"Please enter a valid username and password. Note that "
"both fields are case-sensitive."
msgstr ""
-#: django_authopenid/forms.py:310
+#: django_authopenid/forms.py:313
msgid "choose password"
msgstr "Password"
-#: django_authopenid/forms.py:311
+#: django_authopenid/forms.py:314
msgid "password is required"
msgstr ""
-#: django_authopenid/forms.py:314
+#: django_authopenid/forms.py:317
msgid "retype password"
msgstr "Password <i>(please retype)</i>"
-#: django_authopenid/forms.py:315
+#: django_authopenid/forms.py:318
msgid "please, retype your password"
msgstr ""
-#: django_authopenid/forms.py:316
+#: django_authopenid/forms.py:319
msgid "sorry, entered passwords did not match, please try again"
msgstr ""
-#: django_authopenid/forms.py:341
+#: django_authopenid/forms.py:344
msgid "Current password"
msgstr ""
-#: django_authopenid/forms.py:343
+#: django_authopenid/forms.py:346
msgid "New password"
msgstr ""
-#: django_authopenid/forms.py:345
+#: django_authopenid/forms.py:348
msgid "Retype new password"
msgstr ""
-#: django_authopenid/forms.py:356
+#: django_authopenid/forms.py:359
msgid ""
"Old password is incorrect. Please enter the correct "
"password."
msgstr ""
-#: django_authopenid/forms.py:368
+#: django_authopenid/forms.py:371
msgid "new passwords do not match"
msgstr ""
-#: django_authopenid/forms.py:432
+#: django_authopenid/forms.py:435
msgid "Your user name (<i>required</i>)"
msgstr ""
-#: django_authopenid/forms.py:447
+#: django_authopenid/forms.py:450
msgid "Incorrect username."
msgstr "sorry, there is no such user name"
@@ -218,87 +226,87 @@ msgstr ""
msgid "openid/"
msgstr ""
-#: django_authopenid/urls.py:30 forum/urls.py:43 forum/urls.py:47
+#: django_authopenid/urls.py:30 forum/urls.py:44 forum/urls.py:48
msgid "delete/"
msgstr ""
-#: django_authopenid/views.py:123
+#: django_authopenid/views.py:124
#, python-format
msgid "OpenID %(openid_url)s is invalid"
msgstr ""
-#: django_authopenid/views.py:531
+#: django_authopenid/views.py:532
msgid "Welcome email subject line"
msgstr "Welcome to the Q&A forum"
-#: django_authopenid/views.py:626
+#: django_authopenid/views.py:627
msgid "Password changed."
msgstr ""
-#: django_authopenid/views.py:638 django_authopenid/views.py:644
+#: 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='%(details_url)s'>here</a>."
-#: django_authopenid/views.py:665
+#: django_authopenid/views.py:666
msgid "Email verification subject line"
msgstr "Verification Email from Q&A forum"
-#: django_authopenid/views.py:751
+#: django_authopenid/views.py:752
msgid "your email was not changed"
msgstr ""
-#: django_authopenid/views.py:798 django_authopenid/views.py:950
+#: 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:802 django_authopenid/views.py:957
+#: 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:810
+#: django_authopenid/views.py:811
msgid "Email Changed."
msgstr ""
-#: django_authopenid/views.py:885
+#: django_authopenid/views.py:886
msgid "This OpenID is already associated with another account."
msgstr ""
-#: django_authopenid/views.py:890
+#: django_authopenid/views.py:891
#, python-format
msgid "OpenID %s is now associated with your account."
msgstr ""
-#: django_authopenid/views.py:960
+#: django_authopenid/views.py:961
msgid "Account deleted."
msgstr ""
-#: django_authopenid/views.py:1003
+#: django_authopenid/views.py:1004
msgid "Request for new password"
msgstr ""
-#: django_authopenid/views.py:1016
+#: django_authopenid/views.py:1017
msgid "A new password and the activation link were sent to your email address."
msgstr ""
-#: django_authopenid/views.py:1046
+#: django_authopenid/views.py:1047
#, python-format
msgid ""
"Could not change password. Confirmation key '%s' is not "
"registered."
msgstr ""
-#: django_authopenid/views.py:1055
+#: django_authopenid/views.py:1056
msgid ""
"Can not change password. User don't exist anymore in our "
"database."
msgstr ""
-#: django_authopenid/views.py:1064
+#: django_authopenid/views.py:1065
#, python-format
msgid "Password changed for %s. You may now sign in."
msgstr ""
@@ -351,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 ""
@@ -443,10 +463,8 @@ msgstr ""
msgid "latest questions"
msgstr ""
-#: forum/feed.py:19 forum/urls.py:32 forum/urls.py:33 forum/urls.py:34
-#: forum/urls.py:35 forum/urls.py:36 forum/urls.py:37 forum/urls.py:38
-#: forum/urls.py:39 forum/urls.py:40 forum/urls.py:41 forum/urls.py:43
-msgid "questions/"
+#: forum/feed.py:19 forum/urls.py:52
+msgid "question/"
msgstr ""
#: forum/forms.py:16 templates/answer_edit_tips.html:35
@@ -471,7 +489,7 @@ msgstr ""
msgid "question content must be > 10 characters"
msgstr ""
-#: forum/forms.py:47 templates/header.html:29 templates/header.html.py:63
+#: forum/forms.py:47 templates/header.html:28 templates/header.html.py:62
msgid "tags"
msgstr ""
@@ -498,11 +516,11 @@ msgid ""
"characters '.-_#'"
msgstr ""
-#: forum/forms.py:79 templates/index.html:59 templates/index.html.py:71
+#: 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:59
-#: templates/questions.html.py:71 templates/unanswered.html:51
+#: 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 ""
@@ -544,119 +562,139 @@ msgid "this email does not have to be linked to gravatar"
msgstr ""
#: forum/forms.py:198
-msgid "Real name"
+msgid "Screen name"
msgstr ""
#: forum/forms.py:199
-msgid "Website"
+msgid "Real name"
msgstr ""
#: forum/forms.py:200
-msgid "Location"
+msgid "Website"
msgstr ""
#: forum/forms.py:201
+msgid "Location"
+msgstr ""
+
+#: forum/forms.py:202
msgid "Date of birth"
msgstr ""
-#: forum/forms.py:201
+#: forum/forms.py:202
msgid "will not be shown, used to calculate age, format: YYYY-MM-DD"
msgstr ""
-#: forum/forms.py:202 templates/authopenid/settings.html:21
+#: forum/forms.py:203 templates/authopenid/settings.html:21
msgid "Profile"
msgstr ""
-#: forum/forms.py:229 forum/forms.py:230
+#: forum/forms.py:231 forum/forms.py:232
msgid "this email has already been registered, please use another one"
msgstr ""
-#: forum/forms.py:234 forum/forms.py:235
+#: forum/forms.py:238
+msgid "Choose email tag filter"
+msgstr ""
+
+#: forum/forms.py:245 forum/forms.py:246
msgid "weekly"
msgstr ""
-#: forum/forms.py:234 forum/forms.py:235
+#: forum/forms.py:245 forum/forms.py:246
msgid "no email"
msgstr ""
-#: forum/forms.py:235
+#: forum/forms.py:246
msgid "daily"
msgstr ""
-#: forum/forms.py:250 forum/models.py:46
-msgid "Entire forum"
-msgstr ""
-
-#: forum/forms.py:253
+#: forum/forms.py:261
msgid "Asked by me"
msgstr ""
-#: forum/forms.py:256
+#: forum/forms.py:264
msgid "Answered by me"
msgstr ""
-#: forum/forms.py:259
+#: forum/forms.py:267
msgid "Individually selected"
msgstr ""
-#: forum/models.py:47
+#: 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:48
+#: forum/models.py:53
msgid "Questions that I answered"
msgstr ""
-#: forum/models.py:49
+#: forum/models.py:54
msgid "Individually selected questions"
msgstr ""
-#: forum/models.py:52
+#: forum/models.py:57
msgid "Weekly"
msgstr ""
-#: forum/models.py:53
+#: forum/models.py:58
msgid "Daily"
msgstr ""
-#: forum/models.py:54
+#: forum/models.py:59
msgid "No email"
msgstr ""
-#: forum/models.py:289
+#: forum/models.py:301
#, python-format
msgid "%(author)s modified the question"
msgstr ""
-#: forum/models.py:293
+#: forum/models.py:305
#, python-format
msgid "%(people)s posted %(new_answer_count)s new answers"
msgstr ""
-#: forum/models.py:298
+#: forum/models.py:310
#, python-format
msgid "%(people)s commented the question"
msgstr ""
-#: forum/models.py:303
+#: forum/models.py:315
#, python-format
msgid "%(people)s commented answers"
msgstr ""
-#: forum/models.py:305
+#: forum/models.py:317
#, python-format
msgid "%(people)s commented an answer"
msgstr ""
-#: forum/models.py:493 templates/badges.html:53
+#: forum/models.py:348
+msgid "interesting"
+msgstr ""
+
+#: forum/models.py:348
+msgid "ignored"
+msgstr ""
+
+#: forum/models.py:511 templates/badges.html:53
msgid "gold"
msgstr ""
-#: forum/models.py:494 templates/badges.html:61
+#: forum/models.py:512 templates/badges.html:61
msgid "silver"
msgstr ""
-#: forum/models.py:495 templates/badges.html:68
+#: forum/models.py:513 templates/badges.html:68
msgid "bronze"
msgstr ""
@@ -680,15 +718,15 @@ msgstr ""
msgid "logout/"
msgstr ""
-#: forum/urls.py:29 forum/urls.py:30 forum/urls.py:31 forum/urls.py:47
+#: 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:43 forum/urls.py:47
+#: 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:56
+#: forum/urls.py:30 forum/urls.py:35 forum/urls.py:70
#: templates/user_info.html:45
msgid "edit/"
msgstr ""
@@ -697,7 +735,13 @@ msgstr ""
msgid "revisions/"
msgstr ""
-#: forum/urls.py:33 forum/urls.py:66
+#: 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 ""
@@ -721,55 +765,94 @@ msgstr ""
msgid "vote/"
msgstr ""
-#: forum/urls.py:51
-msgid "question/"
+#: forum/urls.py:42
+msgid "command/"
msgstr ""
-#: forum/urls.py:52 forum/urls.py:53
+#: forum/urls.py:53 forum/urls.py:54
msgid "tags/"
msgstr ""
-#: forum/urls.py:54 forum/urls.py:56 forum/urls.py:57
+#: 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:55
+#: forum/urls.py:69
msgid "moderate-user/"
msgstr ""
-#: forum/urls.py:58 forum/urls.py:59
+#: 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/urls.py:60
+#: 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:60
+#: forum/urls.py:74
msgid "markread/"
msgstr ""
-#: forum/urls.py:62
+#: forum/urls.py:76
msgid "nimda/"
msgstr ""
-#: forum/urls.py:64
+#: forum/urls.py:78
msgid "upload/"
msgstr ""
-#: forum/urls.py:65 forum/urls.py:66 forum/urls.py:67
+#: forum/urls.py:79 forum/urls.py:80 forum/urls.py:81
msgid "books/"
msgstr ""
-#: forum/urls.py:68
+#: 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:69
+#: forum/urls.py:83
msgid "feedback/"
msgstr ""
-#: forum/urls.py:70
+#: forum/urls.py:84
msgid "account/"
msgstr ""
@@ -857,118 +940,144 @@ msgstr ""
msgid "profile - email subscriptions"
msgstr ""
-#: forum/views.py:126
+#: forum/views.py:141
msgid "Q&A forum feedback"
msgstr ""
-#: forum/views.py:127
+#: forum/views.py:142
msgid "Thanks for the feedback!"
msgstr ""
-#: forum/views.py:135
+#: forum/views.py:150
msgid "We look forward to hearing your feedback! Please, give it next time :)"
msgstr ""
-#: forum/views.py:1014
+#: forum/views.py:1150
#, python-format
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='%(details_url)s'>more details here</a>"
-#: forum/views.py:1022
+#: forum/views.py:1158
msgid "email update frequency has been set to daily"
msgstr ""
-#: forum/views.py:1826
+#: forum/views.py:2032
msgid "changes saved"
msgstr ""
-#: forum/views.py:1832
+#: forum/views.py:2038
msgid "email updates canceled"
msgstr ""
-#: forum/views.py:1999
+#: 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:2001
+#: forum/views.py:2209
msgid "allowed file types are 'jpg', 'jpeg', 'gif', 'bmp', 'png', 'tiff'"
msgstr ""
-#: forum/views.py:2003
+#: forum/views.py:2211
#, python-format
msgid "maximum upload file size is %sK"
msgstr ""
-#: forum/views.py:2005
+#: 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:71
+#: forum/management/commands/send_email_alerts.py:160
msgid "email update message subject"
msgstr "news from Q&A forum"
-#: forum/management/commands/send_email_alerts.py:72
+#: 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>"
+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:81
+#: 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>"
+"<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:160 forum/templatetags/extra_tags.py:189
-#: templates/header.html:34
+#: forum/templatetags/extra_tags.py:163 forum/templatetags/extra_tags.py:192
+#: templates/header.html:33
msgid "badges"
msgstr ""
-#: forum/templatetags/extra_tags.py:161 forum/templatetags/extra_tags.py:188
+#: forum/templatetags/extra_tags.py:164 forum/templatetags/extra_tags.py:191
msgid "reputation points"
msgstr "karma"
-#: forum/templatetags/extra_tags.py:244
+#: forum/templatetags/extra_tags.py:247
msgid "%b %d at %H:%M"
msgstr ""
-#: forum/templatetags/extra_tags.py:246
+#: forum/templatetags/extra_tags.py:249
msgid "%b %d '%y at %H:%M"
msgstr ""
-#: forum/templatetags/extra_tags.py:248
+#: forum/templatetags/extra_tags.py:251
msgid "2 days ago"
msgstr ""
-#: forum/templatetags/extra_tags.py:250
+#: forum/templatetags/extra_tags.py:253
msgid "yesterday"
msgstr ""
-#: forum/templatetags/extra_tags.py:252
+#: 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:254
+#: 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
msgid "Sorry, could not find the page you requested."
msgstr ""
@@ -1010,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"
@@ -1026,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"
@@ -1035,13 +1154,46 @@ msgstr ""
msgid "About"
msgstr ""
+#: templates/about.html:21
+msgid ""
+"<strong>CNPROG <span class=\"orange\">Q&amp;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&amp;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:25 templates/answer_edit.html.py:28
#: templates/ask.html:26 templates/ask.html.py:29 templates/question.html:45
-#: templates/question.html.py:48 templates/question_edit.html:28
+#: templates/question.html.py:48 templates/question_edit.html:25
+#: templates/question_edit.html.py:28
msgid "hide preview"
msgstr ""
@@ -1049,15 +1201,17 @@ msgstr ""
#: 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&amp;A community will be recognized with the variety of badges."
#: templates/answer_edit.html:48 templates/question_edit.html:66
-#: templates/question_retag.html:53 templates/revisions_answer.html:36
-#: templates/revisions_question.html:36
+#: templates/question_retag.html:53 templates/revisions_answer.html:38
+#: templates/revisions_question.html:38
msgid "back"
msgstr ""
#: templates/answer_edit.html:53 templates/question_edit.html:71
-#: templates/revisions_answer.html:50 templates/revisions_question.html:50
+#: templates/revisions_answer.html:52 templates/revisions_question.html:52
msgid "revision"
msgstr ""
@@ -1083,7 +1237,7 @@ msgstr ""
#: templates/answer_edit.html:73 templates/close.html:29
#: templates/feedback.html:50 templates/question_edit.html:119
#: templates/question_retag.html:75 templates/reopen.html:30
-#: templates/user_edit.html:83 templates/authopenid/changeemail.html:40
+#: templates/user_edit.html:87 templates/authopenid/changeemail.html:40
msgid "Cancel"
msgstr ""
@@ -1315,8 +1469,8 @@ msgstr ""
msgid "number of times"
msgstr ""
-#: templates/book.html:105 templates/index.html:47
-#: templates/question_summary_list_roll.html:14 templates/questions.html:47
+#: 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 ""
@@ -1325,17 +1479,17 @@ msgstr ""
msgid "the answer has been accepted to be correct"
msgstr ""
-#: templates/book.html:115 templates/index.html:48
-#: templates/question_summary_list_roll.html:15 templates/questions.html:48
+#: 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:103
+#: templates/book.html:125 templates/index.html:105
#: templates/question.html:480 templates/question_summary_list_roll.html:52
-#: templates/questions.html:103 templates/questions.html.py:171
-#: templates/tags.html:49 templates/unanswered.html:95
-#: templates/unanswered.html.py:122 templates/users_questions.html:52
+#: 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 ""
@@ -1343,7 +1497,7 @@ msgstr ""
msgid "subscribe to book RSS feed"
msgstr ""
-#: templates/book.html:147 templates/index.html:152
+#: templates/book.html:147 templates/index.html:156
msgid "subscribe to the questions feed"
msgstr ""
@@ -1453,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 ""
@@ -1465,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 ""
@@ -1580,11 +1734,11 @@ msgstr ""
"Please <a href='%(ask_question_url)s'>ask</a> your question, help make our "
"community better!"
-#: templates/faq.html:128 templates/header.html:28 templates/header.html.py:62
+#: templates/faq.html:128 templates/header.html:27 templates/header.html.py:61
msgid "questions"
msgstr ""
-#: templates/faq.html:128 templates/index.html:157
+#: templates/faq.html:128 templates/index.html:161
msgid "."
msgstr ""
@@ -1646,32 +1800,28 @@ msgstr ""
msgid "Message body:"
msgstr ""
-#: templates/footer.html:8 templates/header.html:14 templates/index.html:117
+#: 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:118
+#: 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:22
+#: templates/footer.html:21
msgid "give feedback"
msgstr ""
@@ -1683,31 +1833,31 @@ msgstr ""
msgid "login"
msgstr ""
-#: templates/header.html:22
+#: templates/header.html:21
msgid "back to home page"
msgstr ""
-#: templates/header.html:30 templates/header.html.py:64
+#: templates/header.html:29 templates/header.html.py:63
msgid "users"
msgstr ""
-#: templates/header.html:32
+#: templates/header.html:31
msgid "books"
msgstr ""
-#: templates/header.html:35
+#: templates/header.html:34
msgid "unanswered questions"
msgstr "unanswered"
-#: templates/header.html:39
+#: templates/header.html:38
msgid "my profile"
msgstr ""
-#: templates/header.html:43
+#: templates/header.html:42
msgid "ask a question"
msgstr ""
-#: templates/header.html:58
+#: templates/header.html:57
msgid "search"
msgstr ""
@@ -1715,115 +1865,114 @@ msgstr ""
msgid "Home"
msgstr ""
-#: templates/index.html:23 templates/questions.html:8
+#: templates/index.html:25 templates/questions.html:8
msgid "Questions"
msgstr ""
-#: templates/index.html:25
+#: templates/index.html:27
msgid "last updated questions"
msgstr ""
-#: templates/index.html:25 templates/questions.html:26
+#: templates/index.html:27 templates/questions.html:47
#: templates/unanswered.html:21
msgid "newest"
msgstr ""
-#: templates/index.html:26 templates/questions.html:28
+#: templates/index.html:28 templates/questions.html:49
msgid "hottest questions"
msgstr ""
-#: templates/index.html:26 templates/questions.html:28
+#: templates/index.html:28 templates/questions.html:49
msgid "hottest"
msgstr ""
-#: templates/index.html:27 templates/questions.html:29
+#: templates/index.html:29 templates/questions.html:50
msgid "most voted questions"
msgstr ""
-#: templates/index.html:27 templates/questions.html:29
+#: templates/index.html:29 templates/questions.html:50
msgid "most voted"
msgstr ""
-#: templates/index.html:28
+#: templates/index.html:30
msgid "all questions"
msgstr ""
-#: templates/index.html:46 templates/question_summary_list_roll.html:13
-#: templates/questions.html:46 templates/unanswered.html:38
+#: 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:78 templates/index.html.py:92
-#: templates/questions.html:78 templates/questions.html.py:92
+#: 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:81 templates/index.html.py:86
-#: templates/questions.html:81 templates/questions.html.py:86
+#: 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:103 templates/question.html:480
-#: templates/question_summary_list_roll.html:52 templates/questions.html:103
-#: templates/questions.html.py:171 templates/tags.html:49
-#: templates/unanswered.html:95 templates/unanswered.html.py:122
-#: templates/users_questions.html:52
+#: 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:114
+#: templates/index.html:116
msgid "welcome to website"
msgstr "Welcome to Q&amp;A forum"
-#: templates/index.html:123
+#: templates/index.html:127
msgid "Recent tags"
msgstr ""
-#: templates/index.html:128 templates/question.html:135
+#: templates/index.html:132 templates/question.html:135
#, python-format
msgid "see questions tagged '%(tagname)s'"
msgstr ""
-#: templates/index.html:131 templates/index.html.py:157
+#: templates/index.html:135 templates/index.html.py:161
msgid "popular tags"
msgstr "tags"
-#: templates/index.html:136
+#: templates/index.html:140
msgid "Recent awards"
msgstr "Recent badges"
-#: templates/index.html:142
+#: templates/index.html:146
msgid "given to"
msgstr ""
-#: templates/index.html:147
+#: templates/index.html:151
msgid "all awards"
msgstr "all badges"
-#: templates/index.html:152
+#: templates/index.html:156
msgid "subscribe to last 30 questions by RSS"
msgstr ""
-#: templates/index.html:157
+#: templates/index.html:161
msgid "Still looking for more? See"
msgstr ""
-#: templates/index.html:157
+#: templates/index.html:161
msgid "complete list of questions"
msgstr "list of all questions"
-#: templates/index.html:157 templates/authopenid/signup.html:18
+#: templates/index.html:161 templates/authopenid/signup.html:18
msgid "or"
msgstr ""
-#: templates/index.html:157
+#: templates/index.html:161
msgid "Please help us answer"
msgstr ""
-#: templates/index.html:157
+#: templates/index.html:161
msgid "list of unanswered questions"
msgstr "unanswered questions"
@@ -1836,8 +1985,8 @@ 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 forum"
-"but will not sign you off from your OpenID provider.</p><p>If you wish to sign off "
+"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:20
@@ -1881,17 +2030,19 @@ msgid_plural ""
msgstr[0] ""
msgstr[1] ""
-#: templates/post_contributor_info.html:19 templates/revisions_answer.html:66
-#: templates/revisions_question.html:66
+#: templates/post_contributor_info.html:19
msgid "asked"
msgstr ""
-#: templates/post_contributor_info.html:21
+#: templates/post_contributor_info.html:22
msgid "answered"
msgstr ""
-#: templates/post_contributor_info.html:37 templates/revisions_answer.html:68
-#: templates/revisions_question.html:68
+#: templates/post_contributor_info.html:24
+msgid "posted"
+msgstr ""
+
+#: templates/post_contributor_info.html:45
msgid "updated"
msgstr ""
@@ -1902,10 +2053,9 @@ msgstr ""
#: templates/privacy.html:15
msgid "general message about privacy"
msgstr ""
-"Respecting users privacy is an important core principle "
-"of this Q&amp;A forum. "
-"Information on this page details how this forum protects your privacy, "
-"and what type of information is collected."
+"Respecting users privacy is an important core principle of this Q&amp;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"
@@ -1914,9 +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."
+"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"
@@ -1925,8 +2075,8 @@ 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 "
+"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
@@ -1936,15 +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."
+"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."
+"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"
@@ -1953,9 +2103,9 @@ 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. "
+"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:77 templates/question.html.py:78
#: templates/question.html:94 templates/question.html.py:96
@@ -1981,31 +2131,31 @@ msgid "remove favorite mark from this question (click again to restore mark)"
msgstr ""
#: templates/question.html:140 templates/question.html.py:294
-#: templates/revisions_answer.html:56 templates/revisions_question.html:56
+#: templates/revisions_answer.html:58 templates/revisions_question.html:58
msgid "edit"
msgstr ""
-#: templates/question.html:144 templates/question.html.py:301
-msgid "delete"
-msgstr ""
-
-#: templates/question.html:149
+#: templates/question.html:145
msgid "reopen"
msgstr ""
-#: templates/question.html:153
+#: templates/question.html:149
msgid "close"
msgstr ""
-#: templates/question.html:159 templates/question.html.py:308
+#: 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:160 templates/question.html.py:309
+#: templates/question.html:156 templates/question.html.py:300
msgid "flag offensive"
msgstr ""
+#: templates/question.html:164 templates/question.html.py:311
+msgid "delete"
+msgstr ""
+
#: templates/question.html:182 templates/question.html.py:331
msgid "delete this comment"
msgstr ""
@@ -2115,7 +2265,7 @@ msgstr ""
msgid "permanent link"
msgstr "link"
-#: templates/question.html:301
+#: templates/question.html:311
msgid "undelete"
msgstr ""
@@ -2287,40 +2437,61 @@ msgstr ""
msgid "tag editors receive special awards from the community"
msgstr ""
-#: templates/questions.html:24
+#: templates/questions.html:29
msgid "Found by tags"
msgstr "Tagged questions"
-#: templates/questions.html:24
+#: templates/questions.html:33
+msgid "Search results"
+msgstr ""
+
+#: templates/questions.html:35
msgid "Found by title"
msgstr ""
-#: templates/questions.html:24
+#: 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:26 templates/unanswered.html:21
+#: templates/questions.html:47 templates/unanswered.html:21
msgid "most recently asked questions"
msgstr ""
-#: templates/questions.html:27
+#: templates/questions.html:48
msgid "most recently updated questions"
msgstr ""
-#: templates/questions.html:27
+#: templates/questions.html:48
msgid "active"
msgstr ""
-#: templates/questions.html:126
+#: 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 "
@@ -2330,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:133
+#: templates/questions.html:171
+#, 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] ""
+"\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"
-"\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\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\n"
+" "
msgstr[0] ""
"\n"
"<div class=\"questions-count\">%(q_num)s</div><p>question with title "
@@ -2351,55 +2547,81 @@ msgstr[1] ""
"containing <strong><span class=\"darkred\">%(searchtitle)s</span></strong></"
"p>"
-#: templates/questions.html:139
+#: 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:148
+"<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:152
+#: templates/questions.html:205
msgid "Questions are sorted by the <strong>time of last update</strong>."
msgstr ""
-#: templates/questions.html:153
+#: 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:157
+#: 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:158
+#: templates/questions.html:211
msgid "Most answered questions are shown first."
msgstr " "
-#: templates/questions.html:162
+#: templates/questions.html:215
msgid "Questions are sorted by the <strong>number of votes</strong>."
msgstr ""
-#: templates/questions.html:163
+#: templates/questions.html:216
msgid "Most voted questions are shown first."
msgstr ""
-#: templates/questions.html:168 templates/unanswered.html:118
+#: 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 ""
@@ -2428,15 +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:48 templates/revisions_question.html:48
+#: 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 ""
@@ -2469,10 +2717,6 @@ msgstr ""
msgid "Nothing found"
msgstr ""
-#: templates/unanswered.html:8 templates/unanswered.html.py:19
-msgid "Unanswered questions"
-msgstr ""
-
#: templates/unanswered.html:114
#, python-format
msgid "have %(num_q)s unanswered questions"
@@ -2501,7 +2745,7 @@ msgstr "<a href='%(gravatar_faq_url)s'>gravatar</a>"
msgid "Registered user"
msgstr ""
-#: templates/user_edit.html:82 templates/user_email_subscriptions.html:17
+#: templates/user_edit.html:86 templates/user_email_subscriptions.html:20
msgid "Update"
msgstr ""
@@ -2518,7 +2762,7 @@ msgstr ""
"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:18
+#: templates/user_email_subscriptions.html:21
msgid "Stop sending email"
msgstr "Stop Email"
@@ -2662,10 +2906,11 @@ msgstr[1] ""
#: 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:114
+#: templates/user_stats.html:115
#, python-format
msgid ""
"\n"
@@ -2762,12 +3007,11 @@ msgstr ""
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&amp;"
-"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."
+"span> Valid email address is required on this Q&amp;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:31
msgid "Your new Email"
@@ -2795,8 +3039,8 @@ 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&amp;A</span>. If you would like "
-"to use <strong>another email</strong>, please <a href='%(change_email_url)"
+"email on <span class=\"orange\">Q&amp;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:57
@@ -2917,14 +3161,13 @@ msgid ""
" "
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&amp;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>"
+"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&amp;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
@@ -2943,6 +3186,13 @@ msgstr ""
#: 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:55
msgid "Sorry, looks like we have some errors:"
@@ -2962,6 +3212,15 @@ msgstr ""
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&amp;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&amp;A</span> forum sends up to <strong>one "
"email digest per week</strong> - only when there is anything new.<br/>If "
@@ -2971,23 +3230,27 @@ msgstr ""
msgid "create account"
msgstr "Signup"
-#: templates/authopenid/complete.html:100
+#: templates/authopenid/complete.html:92
+msgid "create account"
+msgstr "Signup"
+
+#: templates/authopenid/complete.html:101
msgid "Existing account"
msgstr ""
-#: templates/authopenid/complete.html:101
+#: templates/authopenid/complete.html:102
msgid "user name"
msgstr ""
-#: templates/authopenid/complete.html:102
+#: templates/authopenid/complete.html:103
msgid "password"
msgstr ""
-#: templates/authopenid/complete.html:107
+#: templates/authopenid/complete.html:108
msgid "Register"
msgstr ""
-#: templates/authopenid/complete.html:108 templates/authopenid/signin.html:140
+#: templates/authopenid/complete.html:109 templates/authopenid/signin.html:140
msgid "Forgot your password?"
msgstr ""
@@ -3079,6 +3342,20 @@ msgstr ""
#: 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/>&bull; submit your "
+"user name below and check your email<br/>&bull; <strong>follow the "
+"activation link</strong> for the new password - sent to you by email and "
+"login with the suggested password<br/>&bull; at this you might want to "
+"change your password to something you can remember better"
+
+#: templates/authopenid/sendpw.html:21
+msgid "Reset password"
+msgstr "Send me a new password"
+
+#: templates/authopenid/external_legacy_login_info.html:12
+msgid "how to login with password through external login website"
+msgstr ""
#: templates/authopenid/sendpw.html:4 templates/authopenid/sendpw.html.py:7
msgid "Send new password"
@@ -3274,3 +3551,18 @@ msgstr ""
#: templates/authopenid/signup.html:19
msgid "return to OpenID login"
msgstr ""
+
+#~ msgid ""
+#~ "\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/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
index c7ff05bc..8422d89b 100644
--- a/middleware/anon_user.py
+++ b/middleware/anon_user.py
@@ -1,6 +1,8 @@
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):
@@ -24,3 +26,9 @@ class ConnectToSessionMessagesMiddleware(object):
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/settings.py b/settings.py
index daada933..45685788 100644
--- a/settings.py
+++ b/settings.py
@@ -2,19 +2,6 @@
# Django settings for lanai project.
import os.path
import sys
-sys.path.insert(0,'/home/fadeev/incoming/Django-1.1.1')
-
-#DEBUG SETTINGS
-DEBUG = False
-TEMPLATE_DEBUG = DEBUG
-INTERNAL_IPS = ('127.0.0.1','128.200.203.33')
-
-#EMAIL AND ADMINS
-ADMINS = (
- ('Evgeny Fadeev', 'evgeny.fadeev@gmail.com'),
-)
-MANAGERS = ADMINS
-
SITE_ID = 1
ADMIN_MEDIA_PREFIX = '/forum/admin/media/'
@@ -71,8 +58,10 @@ INSTALLED_APPS = (
'django.contrib.sites',
'django.contrib.admin',
'django.contrib.humanize',
+ 'django.contrib.sitemaps',
'forum',
'django_authopenid',
+ 'djangosphinx',
#'debug_toolbar' ,
'user_messages',
)
diff --git a/settings_local.py.dist b/settings_local.py.dist
index e43522ef..5447e517 100644
--- a/settings_local.py.dist
+++ b/settings_local.py.dist
@@ -9,6 +9,15 @@ LOG_FILENAME = 'django.lanai.log'
import logging
logging.basicConfig(filename=os.path.join(SITE_SRC_ROOT, 'log', LOG_FILENAME), level=logging.DEBUG,)
+#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.
@@ -29,24 +38,24 @@ EMAIL_USE_TLS=False
#LOCALIZATIONS
TIME_ZONE = 'America/Tijuana'
-#OTHER SETTINGS
-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.'
-
###########################
#
# this will allow running your forum with url like http://site.com/forum
#
# FORUM_SCRIPT_ALIAS = 'forum/'
#
-# also make sure to set '/':'/forum' in file templates/content/js/com.cnprog.i18n.js
-# this is necessary to make client scripts work in this configuration
-#
FORUM_SCRIPT_ALIAS = '' #no leading slash, default = '' empty string
+
+#OTHER SETTINGS
+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
+
USE_I18N = True
LANGUAGE_CODE = 'en'
EMAIL_VALIDATION = 'off' #string - on|off
@@ -57,12 +66,20 @@ GOOGLE_SITEMAP_CODE = '55uGNnQVJW8p1bbXeF/Xbh9I7nZBM/wLhRz6N/I1kkA='
GOOGLE_ANALYTICS_KEY = ''
BOOKS_ON = False
WIKI_ON = True
-USE_EXTERNAL_LEGACY_LOGIN = 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
-LOGIN_URL = '/%s%s%s' % (FORUM_SCRIPT_ALIAS,'account/','signin/')
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
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/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 1f1942e3..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 `content_type_id` INT NULL
+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 9cc80974..a6af5931 100644
--- a/sql_scripts/update_2009_02_26_001.sql
+++ b/sql_scripts/update_2009_02_26_001.sql
@@ -1,19 +1,19 @@
-ALTER TABLE answer ADD COLUMN `accepted_at` datetime default null;
-
-/* Update accepted_at column with answer added datetime for existing data */
-UPDATE answer
-SET accepted_at = added_at
-WHERE accepted = 1 AND accepted_at IS NULL;
-
-/* workround for c# url problem on bluehost server */
-UPDATE tag
-SET name = 'csharp'
-WHERE name = 'c#'
-
-UPDATE question
-SET tagnames = replace(tagnames, 'c#', 'csharp')
-WHERE tagnames like '%c#%'
-
-UPDATE question_revision
-SET tagnames = replace(tagnames, 'c#', 'csharp')
-WHERE tagnames like '%c#%' \ No newline at end of file
+ALTER TABLE answer ADD COLUMN `accepted_at` datetime default null;
+
+/* Update accepted_at column with answer added datetime for existing data */
+UPDATE answer
+SET accepted_at = added_at
+WHERE accepted = 1 AND accepted_at IS NULL;
+
+/* workround for c# url problem on bluehost server */
+UPDATE tag
+SET name = 'csharp'
+WHERE name = 'c#'
+
+UPDATE question
+SET tagnames = replace(tagnames, 'c#', 'csharp')
+WHERE tagnames like '%c#%'
+
+UPDATE question_revision
+SET tagnames = replace(tagnames, 'c#', 'csharp')
+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 b0d05ac7..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_at datetime default null;
+ALTER TABLE Tag ADD COLUMN deleted_by_id INTEGER NULL;
+ALTER TABLE Tag ADD COLUMN deleted TINYINT NOT NULL;
diff --git a/templates/about.html b/templates/about.html
index 6d5f3060..db8c764e 100644
--- a/templates/about.html
+++ b/templates/about.html
@@ -12,6 +12,7 @@
</div>
<div class="content">
+<<<<<<< HEAD:templates/about.html
<!-- default text
<p>edit file templates/about.html. Below are just suggestions of what can go here</p>
<p>what is your site for?</p>
@@ -40,6 +41,31 @@
{% blocktrans %}
If you would like to find out more about this site - please see <strong><a href="{% url faq %}">frequently asked questions</a></strong>.
{% endblocktrans %}
+=======
+ <p><strong>NMR Wiki <span class="orange">Q&amp;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&amp;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&amp;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>.
+>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/about.html
</p>
</div>
{% endblock %}
diff --git a/templates/authopenid/complete.html b/templates/authopenid/complete.html
index 9a94c3c4..e3c12ae5 100644
--- a/templates/authopenid/complete.html
+++ b/templates/authopenid/complete.html
@@ -88,6 +88,10 @@ parameters:
</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>
diff --git a/templates/authopenid/external_legacy_login_info.html b/templates/authopenid/external_legacy_login_info.html
index e2f4713e..dda394c7 100644
--- a/templates/authopenid/external_legacy_login_info.html
+++ b/templates/authopenid/external_legacy_login_info.html
@@ -8,9 +8,14 @@
</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/base.html b/templates/base.html
index 2b933c4a..b4751be1 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -18,7 +18,12 @@
<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='{% href "/content/js/com.cnprog.i18n.js" %}'></script>
<script type='text/javascript' src='{% href "/content/js/jquery.i18n.js" %}'></script>
@@ -74,7 +79,6 @@
{% endblock%}
</div>
-
<div id="CARight">
{% block sidebar%}
{% endblock%}
diff --git a/templates/base_content.html b/templates/base_content.html
index 52cc6134..12297215 100644
--- a/templates/base_content.html
+++ b/templates/base_content.html
@@ -6,7 +6,13 @@
<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 }}" />
+=======
+ {% 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 %}
@@ -15,7 +21,12 @@
<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='{% href "/content/js/com.cnprog.i18n.js" %}'></script>
<script type='text/javascript' src='{% href "/content/js/jquery.i18n.js" %}'></script>
diff --git a/templates/content/images/close-small-dark.png b/templates/content/images/close-small-dark.png
new file mode 100644
index 00000000..280c1fc7
--- /dev/null
+++ b/templates/content/images/close-small-dark.png
Binary files differ
diff --git a/templates/content/js/com.cnprog.admin.js b/templates/content/js/com.cnprog.admin.js
index 73b5768f..7e91af79 100644
--- a/templates/content/js/com.cnprog.admin.js
+++ b/templates/content/js/com.cnprog.admin.js
@@ -3,7 +3,11 @@ $().ready( function(){
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(){
diff --git a/templates/content/js/com.cnprog.i18n.js b/templates/content/js/com.cnprog.i18n.js
index d2356abc..018927aa 100644
--- a/templates/content/js/com.cnprog.i18n.js
+++ b/templates/content/js/com.cnprog.i18n.js
@@ -57,7 +57,10 @@ var i18nZh = {
};
var i18nEn = {
+<<<<<<< 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',
diff --git a/templates/content/js/com.cnprog.post.js b/templates/content/js/com.cnprog.post.js
index 58db9b33..a884b571 100644
--- a/templates/content/js/com.cnprog.post.js
+++ b/templates/content/js/com.cnprog.post.js
@@ -53,11 +53,19 @@ var Vote = function(){
var acceptAnonymousMessage = $.i18n._('insufficient privilege');
var acceptOwnAnswerMessage = $.i18n._('cannot pick own answer as best');
+<<<<<<< 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 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 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;
@@ -151,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";
+<<<<<<< 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){
+<<<<<<< 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", 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
}
}
};
@@ -237,7 +258,11 @@ var Vote = function(){
type: "POST",
cache: false,
dataType: "json",
+<<<<<<< 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)}});
@@ -256,19 +281,31 @@ var Vote = function(){
showMessage(object, acceptOwnAnswerMessage);
}
else if(data.status == "1"){
+<<<<<<< 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 +']';
+<<<<<<< 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");
+<<<<<<< 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");
}
@@ -282,7 +319,11 @@ var Vote = function(){
showMessage(object, favoriteAnonymousMessage.replace("{{QuestionID}}", questionId));
}
else if(data.status == "1"){
+<<<<<<< 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)
@@ -290,7 +331,11 @@ var Vote = function(){
fav.text(data.count);
}
else if(data.success == "1"){
+<<<<<<< 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");
@@ -359,7 +404,11 @@ var Vote = function(){
}
else if (data.success == "1"){
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'){
@@ -500,7 +549,11 @@ function createComments(type) {
jDiv.append('<p id="' + divId + '" class="comment">'
+ $.i18n._('to comment, need') + ' ' +
+ 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>');
}
}
@@ -508,7 +561,11 @@ function createComments(type) {
var getComments = function(id, jDiv) {
//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); });
};
@@ -529,8 +586,13 @@ function createComments(type) {
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') + '" />';
@@ -569,7 +631,11 @@ function createComments(type) {
//todo fix url translations!!!
$.ajax({
type: "POST",
+<<<<<<< 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) {
@@ -601,7 +667,11 @@ function createComments(type) {
$(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;
@@ -615,12 +685,20 @@ function createComments(type) {
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
}
);
}
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 fff61759..5c0c4a27 100644
--- a/templates/content/js/com.cnprog.utils.js
+++ b/templates/content/js/com.cnprog.utils.js
@@ -23,7 +23,11 @@ var notify = function() {
},
close: function(doPostback) {
if (doPostback) {
+<<<<<<< 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");
@@ -36,7 +40,11 @@ var notify = function() {
function appendLoader(containerSelector) {
$(containerSelector).append('<img class="ajax-loader" '
+<<<<<<< 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...')
diff --git a/templates/content/js/compress.bat b/templates/content/js/compress.bat
index aa31271c..5b2673cf 100644
--- a/templates/content/js/compress.bat
+++ b/templates/content/js/compress.bat
@@ -1,6 +1,5 @@
-#java -jar yuicompressor-2.4.2.jar --type js --charset utf-8 wmd\wmd.js -o wmd\wmd-min.js
-#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
+#java -jar yuicompressor-2.4.2.jar --type js --charset utf-8 wmd\wmd.js -o wmd\wmd-min.js
+#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
diff --git a/templates/content/js/flot-build.bat b/templates/content/js/flot-build.bat
index 28304966..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
+java -jar yuicompressor-2.4.2.jar --type js --charset utf-8 jquery.flot.js -o jquery.flot.pack.js
+
+pause
diff --git a/templates/content/js/wmd/wmd.js b/templates/content/js/wmd/wmd.js
index 0bdc55b6..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>" +
+<<<<<<< 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/style.css b/templates/content/style/style.css
index 6c1d6a3f..0a928cd2 100644
--- a/templates/content/style/style.css
+++ b/templates/content/style/style.css
@@ -114,7 +114,10 @@ blockquote
margin-left:20px;text-decoration:underline; font-size:12px; color:#333333;}
#logo {
padding: 5px 0px 0px 0px;
+<<<<<<< HEAD:templates/content/style/style.css
margin-bottom:-3px;
+=======
+>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/content/style/style.css
}
#navBar {float:clear;position:relative;display:block;width:960px;}
#navBar .nav {margin:20px 0px 0px 16px;
@@ -163,7 +166,11 @@ blockquote
border-right:1px solid #b4b48e;
border-bottom:1px solid #b4b48e;*/
background: white;/* #f9f7ed;*/
+<<<<<<< HEAD:templates/content/style/style.css
margin:10px 0 10px 0;
+=======
+ /*margin:10px 0 10px 0;*/
+>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/content/style/style.css
/*background:url(../images/quest-bg.gif) repeat-x top;*/
}
#listA .qstA thumb {float:left; }
@@ -205,7 +212,18 @@ blockquote
/*border-bottom:1px solid #888a85;*/
}
.evenMore {font-size:14px; font-weight:800;}
+<<<<<<< HEAD:templates/content/style/style.css
.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;
+}
+>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/content/style/style.css
/*内容块*/
.boxA {background:#888a85; padding:6px; margin-bottom:8px;border 1px solid #babdb6;}
@@ -217,7 +235,11 @@ blockquote
.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 {
+<<<<<<< HEAD:templates/content/style/style.css
background:#babdb6;/*f9f7ed;*/
+=======
+ background: #cacdc6;/*f9f7ed;*/
+>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/content/style/style.css
padding:10px;
margin-bottom:8px;
border-top:1px solid #eeeeec;
@@ -225,6 +247,15 @@ blockquote
border-right:1px solid #a9aca5;
border-bottom:1px solid #babdb6;
}
+<<<<<<< HEAD:templates/content/style/style.css
+=======
+.boxC p {
+ margin-bottom:8px;
+}
+.boxC p.nomargin {
+ margin:0px;
+}
+>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/content/style/style.css
.boxC p.info-box-follow-up-links {
text-align:right;
margin:0;
@@ -258,7 +289,11 @@ blockquote
border:1px solid #fff;
background-color:#fff;
color:#777;
+<<<<<<< HEAD:templates/content/style/style.css
padding:.3em;
+=======
+ padding:2px 4px 3px 4px;
+>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/content/style/style.css
font:bold 100% sans-serif;
}
@@ -309,12 +344,21 @@ 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;}
+<<<<<<< HEAD:templates/content/style/style.css
.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;}
+>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/content/style/style.css
.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;}
+<<<<<<< HEAD:templates/content/style/style.css
+=======
+.marked-tags { margin-top: 0px;margin-bottom: 5px; }
+.deletable-tag { margin-right: 3px; white-space:nowrap; }
+>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/content/style/style.css
/*奖牌*/
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;}
@@ -1144,6 +1188,12 @@ ul.bulleta li {background:url(../images/bullet_green.gif) no-repeat 0px 2px; pad
.message p {
margin-bottom:0px;
}
+<<<<<<< HEAD:templates/content/style/style.css
+=======
+.message p.space-above {
+ margin-top:10px;
+}
+>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/content/style/style.css
.warning{color:red;}
.darkred{color:darkred;}
@@ -1420,3 +1470,15 @@ ul.form-horizontal-rows li input {
text-align:center;
font-weight:bold;
}
+<<<<<<< HEAD:templates/content/style/style.css
+=======
+#tagSelector {
+ padding-bottom: 2px;
+}
+#hideIgnoredTagsControl {
+ margin: 5px 0 0 0;
+}
+#hideIgnoredTagsCb {
+ margin: 0 2px 0 1px;
+}
+>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/content/style/style.css
diff --git a/templates/index.html b/templates/index.html
index 470612b4..4041b863 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -10,13 +10,15 @@
<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">
@@ -118,6 +120,8 @@
<div class="more"><a href="{% url faq %}">{% trans "faq" %} »</a></div>
</div>
</div>
+{% else %}
+{% include "tag_selector.html" %}
{% endif %}
<div class="boxC">
<h3>{% trans "Recent tags" %}</h3>
diff --git a/templates/question.html b/templates/question.html
index 6929b762..e88f7ef1 100644
--- a/templates/question.html
+++ b/templates/question.html
@@ -140,10 +140,13 @@
<span class="action-link"><a href="{% url edit_question question.id %}">{% trans 'edit' %}</a></span>
{% endif %}
{% separator %}
+<<<<<<< HEAD:templates/question.html
{% if request.user|can_delete_post:question %}
<span class="action-link"><a id="question-delete-link-{{question.id}}">{% trans "delete" %}</a></span>
{% endif %}
{% separator %}
+=======
+>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/question.html
{% if question.closed %}
{% if request.user|can_reopen_question:question %}
<span class="action-link"><a href="{% url reopen question.id %}">{% trans "reopen" %}</a></span>
@@ -163,6 +166,13 @@
{% endif %}
</span>
{% endif %}
+<<<<<<< HEAD:templates/question.html
+=======
+ {% separator %}
+ {% if request.user|can_delete_post:question %}
+ <span class="action-link"><a id="question-delete-link-{{question.id}}">{% trans "delete" %}</a></span>
+ {% endif %}
+>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/question.html
{% endjoinitems %}
</div>
<div class="post-update-info-container">
@@ -294,6 +304,7 @@
<span class="action-link"><a href="{% url edit_answer answer.id %}">{% trans 'edit' %}</a></span>
{% endif %}
{% separator %}
+<<<<<<< HEAD:templates/question.html
{% if request.user|can_delete_post:answer %}
{% spaceless %}
<span class="action-link">
@@ -303,6 +314,8 @@
{% endspaceless %}
{% endif %}
{% separator %}
+=======
+>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/question.html
{% 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.)" %}">
@@ -312,6 +325,18 @@
{% endif %}
</span>
{% endif %}
+<<<<<<< HEAD:templates/question.html
+=======
+ {% 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 %}
+>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/question.html
{% endjoinitems %}
</div>
<div class="post-update-info-container">
diff --git a/templates/question_edit.html b/templates/question_edit.html
index d6728caf..8ce980fe 100644
--- a/templates/question_edit.html
+++ b/templates/question_edit.html
@@ -22,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" %}]";
diff --git a/templates/questions.html b/templates/questions.html
index f298381e..7cbcbd2b 100644
--- a/templates/questions.html
+++ b/templates/questions.html
@@ -8,20 +8,45 @@
{% 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>
@@ -31,7 +56,19 @@
</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>
@@ -105,24 +142,29 @@
</div>
</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">
{% 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 %}
@@ -144,6 +186,45 @@
{% endif %}
{% endif %}
<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 %}
@@ -164,15 +245,25 @@
{% endifequal %}
</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">&#215; {{ 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">&#215; {{ tag.used_count|intcomma }}</span>
+ <br />
+ {% endfor %}
+>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/questions.html
</div>
</div>
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/user_edit.html b/templates/user_edit.html
index 051d6537..5886c071 100644
--- a/templates/user_edit.html
+++ b/templates/user_edit.html
@@ -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
index 8f27bd2a..10440529 100644
--- a/templates/user_email_subscriptions.html
+++ b/templates/user_email_subscriptions.html
@@ -13,6 +13,12 @@
{% 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" %}"/>
diff --git a/templates/user_reputation.html b/templates/user_reputation.html
index 29d642fe..16127140 100644
--- a/templates/user_reputation.html
+++ b/templates/user_reputation.html
@@ -1,6 +1,7 @@
{% extends "user.html" %}
<!-- user_reputation.html -->
{% load extra_tags %}
+{% load extra_filters %}
{% load humanize %}
{% block userjs %}
<script type='text/javascript' src='{% href "/content/js/excanvas.pack.js" %}'></script>
@@ -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 }}">{{ 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 2b7949e8..b72f5750 100644
--- a/templates/user_stats.html
+++ b/templates/user_stats.html
@@ -84,7 +84,7 @@
<a name="tags"></a>
{% spaceless %}
<h2>
- {% blocktrans count tags|length as counter %}
+ {% blocktrans count user_tags|length as counter %}
<span class="count">1</span> Tag
{% plural %}
<span class="count">{{counter}}</span> Tags
@@ -95,10 +95,16 @@
<table class="tags">
<tr>
<td width="180" valign="top">
- {% for tag in tags%}
+ {% for tag in user_tags%}
<a rel="tag"
+<<<<<<< HEAD:templates/user_stats.html
title="{% blocktrans %}see other questions tagged '{{ tag }}' {% endblocktrans %}"
href="{% url forum.views.tag tag|urlencode %}">{{tag.name}}</a><span class="tag-number"> &#215; {{ 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">&#215; {{ tag.user_tag_usage_count|intcomma }}</span><br/>
+>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/user_stats.html
{% if forloop.counter|divisibleby:"10" %}
</td>
<td width="180" valign="top">
diff --git a/templates/user_votes.html b/templates/user_votes.html
index 4abbf46d..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 %}
@@ -18,9 +19,9 @@
</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 }}">{{ 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 }}#{{ 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/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)
+