diff options
57 files changed, 3814 insertions, 653 deletions
@@ -1,5 +1,5 @@ *.pyc *.swp *.log -settings_local.py nbproject +settings_local.py @@ -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 @@ -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/>. @@ -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() @@ -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&A</span></strong> is a " +"collaboratively edited question\n" +" and answer site created for the <strong>CNPROG</strong> " +"community.\n" +" " +msgstr "" + +#: templates/about.html:25 +msgid "" +"Here you can <strong>ask</strong> and <strong>answer</strong> questions, " +"<strong>comment</strong>\n" +" and <strong>vote</strong> for the questions of others and their answers. " +"Both questions and answers\n" +" <strong>can be revised</strong> and improved. Questions can be " +"<strong>tagged</strong> with\n" +" the relevant keywords to simplify future access and organize the " +"accumulated material." +msgstr "" + +#: templates/about.html:31 +msgid "" +"This <span class=\"orange\">Q&A</span> site is moderated by its members, " +"hopefully - including yourself!\n" +" Moderation rights are gradually assigned to the site users based on the " +"accumulated <strong>\"reputation\"</strong>\n" +" points. These points are added to the users account when others vote for " +"his/her questions or answers.\n" +" These points (very) roughly reflect the level of trust of the community." +msgstr "" + #: templates/answer_edit.html:5 templates/answer_edit.html.py:48 msgid "Edit answer" msgstr "" #: templates/answer_edit.html: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&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&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&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&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&" -"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&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&A</span>. If you would like " -"to use <strong>another email</strong>, please <a href='%(change_email_url)" +"email on <span class=\"orange\">Q&A</span>. If you would like to use " +"<strong>another email</strong>, please <a href='%(change_email_url)" "s'><strong>change it again</strong></a>." #: templates/authopenid/changeemail.html: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&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&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&A</span> forum sends up to <strong>one email digest per " +"week</strong> - only when there is anything new.<br/>If you like, please " +"adjust this now or any time later from your user account." + +#: templates/authopenid/complete.html:91 +msgid "Tag filter tool will be your right panel, once you log in." +msgstr "" +"<strong>Receive forum updates by email</strong> - this will help our " "community grow and become more useful.<br/>By default " "<span class='orange'>Q&A</span> forum sends up to <strong>one " "email digest per week</strong> - only when there is anything new.<br/>If " @@ -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/>• submit your " +"user name below and check your email<br/>• <strong>follow the " +"activation link</strong> for the new password - sent to you by email and " +"login with the suggested password<br/>• at this you might want to " +"change your password to something you can remember better" + +#: templates/authopenid/sendpw.html: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&A</span></strong> is a collaboratively edited question + and answer site created for the <strong>Magnetic Resonance</strong> community, i.e. those people who + work in the fields of <strong>NMR</strong>, <strong>EPR</strong>, <strong>MRI</strong></strong>, etc. + NMR Wiki Q&A is affiliated with <strong><a href="http://nmrwiki.org">NMR Wiki</a></strong> - + the public wiki knowledge base about the Magnetic Resonance, which currently counts ~300 registered users. The most useful information collected here + will be further distilled on the wiki site. + </p> + <p>Here you can <strong>ask</strong> and <strong>answer</strong> questions, <strong>comment</strong> + and <strong>vote</strong> for the questions of others and their answers. Both questions and answers + <strong>can be revised</strong> and improved. Questions can be <strong>tagged</strong> with + the relevant keywords to simplify future access and organize the accumulated material. + </p> + + <p>This <span class="orange">Q&A</span> site is moderated by its members, hopefully - including yourself! + Moderation rights are gradually assigned to the site users based on the accumulated <strong>"reputation"</strong> + points. These points are added to the users account when others vote for his/her questions or answers. + These points (very) roughly reflect the level of trust of the community. + </p> + <p>No points are necessary to ask or answer the questions - so please - + <strong><a href="{% url user_signin %}">join us!</a></strong> + </p> + <p> + If you would like to find out more about this site - please see <strong><a href="{% url faq %}">frequently asked questions</a></strong>. +>>>>>>> 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 Binary files differnew file mode 100644 index 00000000..280c1fc7 --- /dev/null +++ b/templates/content/images/close-small-dark.png 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">× {{ tag.used_count|intcomma }}</span> <br /> {% endfor %} <br /> +======= + <a rel="tag" title="{% blocktrans with tag.name as tag_name %}see questions tagged '{{ tag_name }}'{% endblocktrans %}" href="{% url forum.views.tag tag.name|urlencode %}">{{ tag.name }}</a> + <span class="tag-number">× {{ tag.used_count|intcomma }}</span> + <br /> + {% endfor %} +>>>>>>> 82d35490db90878f013523c4d1a5ec3af2df8b23:templates/questions.html </div> </div> diff --git a/templates/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"> × {{ tag.used_count|intcomma }}</span><br/> +======= + title="{% blocktrans with tag.name as tag_name %}see other questions with {{view_user}}'s contributions tagged '{{ tag_name }}' {% endblocktrans %}" + href="{% url forum.views.tag tag|urlencode %}?user={{view_user.username}}">{{tag.name}}</a> + <span class="tag-number">× {{ tag.user_tag_usage_count|intcomma }}</span><br/> +>>>>>>> 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) + |