+
+
+ 1.6
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.project b/.project
new file mode 100644
index 0000000..8e56b00
--- /dev/null
+++ b/.project
@@ -0,0 +1,23 @@
+
+
+ osqa
+
+
+
+
+
+ org.eclipse.wst.jsdt.core.javascriptValidator
+
+
+
+
+ org.python.pydev.PyDevBuilder
+
+
+
+
+
+ org.python.pydev.pythonNature
+ org.eclipse.wst.jsdt.core.jsNature
+
+
diff --git a/.pydevproject b/.pydevproject
new file mode 100644
index 0000000..f7f3fd1
--- /dev/null
+++ b/.pydevproject
@@ -0,0 +1,10 @@
+
+
+
+
+Default
+python 2.6
+
+/osqa
+
+
diff --git a/HOW_TO_DEBUG b/HOW_TO_DEBUG
new file mode 100644
index 0000000..ba36198
--- /dev/null
+++ b/HOW_TO_DEBUG
@@ -0,0 +1,39 @@
+1) LOGGING
+Please remember that log files may contain plaintext passwords, etc.
+
+Please do not add print statements - at least do not commit them to git
+because in some environments printing to stdout causes errors
+
+Instead use python logging this way:
+--------------------------------
+#somewere on top of file
+import logging
+
+#anywhere below
+logging.debug('this maybe works')
+logging.error('have big error!')
+#or even
+logging.debug('') #this will add time, line number, function and file record
+#sometimes useful record for call tracing on its own
+#etc - take a look at http://docs.python.org/library/logging.html
+-------------------------------
+
+in OSQA logging is currently set up in settings_local.py.dist
+please update it if you need - in older revs logging strings have less info
+
+messages of interest can be grepped out of the log file by module/file/function name
+e.g. to take out all django_authopenid logs run:
+>grep 'osqa\/django_authopenid' log/django.osqa.log | sed 's/^.*MSG: //'
+in the example above 'sed' call truncates out a long prefix
+and makes output look more meaningful
+
+2) DJANGO DEBUG TOOLBAR
+osqa works with django debug toolbar
+if debugging under apache server, check
+that debug toolbar media is loaded correctly
+if toolbar is enabled but you do not see it, possibly some Alias statement
+in apache config is wrong in your VirtualHost or elsewhere
+
+3) If you discover new debugging techniques, please add here.
+Possible areas to improve - at this point there is no SQL query logging,
+as well as request data and http header.
diff --git a/INSTALL b/INSTALL
new file mode 100644
index 0000000..f70b3ec
--- /dev/null
+++ b/INSTALL
@@ -0,0 +1,314 @@
+CONTENTS
+------------------
+A. PREREQUISITES
+B. INSTALLATION
+ 1. Settings file
+ 2. Database
+ 3. Running OSQA in the development server
+ 4. Installation under Apache/WSGI
+ 5. Full text search
+ 6. Email subscriptions
+ 7. Sitemap
+ 8. Miscellaneous
+C. CONFIGURATION PARAMETERS (settings_local.py)
+D. CUSTOMIZATION
+
+
+A. PREREQUISITES
+-----------------------------------------------
+0. We recommend you to use python-setuptools to install pre-requirement libraries.
+If you haven't installed it, please try to install it first.
+e.g, sudo apt-get install python-setuptools
+
+1. Python2.5/2.6, MySQL, Django v1.0/1.1
+Note: email subscription sender job requires Django 1.1, everything else works with 1.0
+Make sure mysql for python provider has been installed.
+sudo easy_install mysql-python
+
+2. Python-openid v2.2
+http://openidenabled.com/python-openid/
+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
+
+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
+
+9. recaptcha_django
+http://code.google.com/p/recaptcha-django/
+
+10. python recaptcha module
+http://code.google.com/p/recaptcha/
+Notice that you will need to register with recaptcha.net and receive
+recaptcha public and private keys that need to be saved in your
+settings_local.py file
+
+NOTES: django_authopenid is included into OSQA code
+and is significantly modified. http://code.google.com/p/django-authopenid/
+no need to install this library
+
+B. INSTALLATION
+-----------------------------------------------
+0. Make sure you have all above python libraries installed.
+
+ make osqa installation server-readable on Linux command might be:
+ chown -R yourlogin:apache /path/to/OSQA
+
+ directories templates/upfiles and log must be server writable
+
+ on Linux type chmod
+ chmod -R g+w /path/to/OSQA/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
+
+Prepare your database by using the same database/account
+configuration from above.
+e.g,
+create database osqa DEFAULT CHARACTER SET UTF8 COLLATE utf8_general_ci;
+grant all on osqa.* to 'osqa'@'localhost';
+And then run "python manage.py syncdb" to synchronize your database.
+
+3. Running OSQA on the development server
+
+Run "python manage.py runserver" to startup django
+development environment.
+(Under Linux you can use command "python manage.py runserver `hostname -i`:8000",
+where you can use any other available number for the port)
+
+you might want to have DEBUG=True in the beginning of settings.py
+when using the test server
+
+4. Installation under Apache/WSGI
+
+4.1 Prepare wsgi script
+
+Make a file readable by your webserver with the following content:
+
+---------
+import os
+import sys
+
+sys.path.insert(0,'/one/level/above') #insert to make sure that forum will be found
+sys.path.append('/one/level/above/OSQA') #maybe this is not necessary
+os.environ['DJANGO_SETTINGS_MODULE'] = 'OSQA.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
+
+ ServerAdmin forum@example.com
+ DocumentRoot /path/to/osqa-site
+ 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 OSQA
+ WSGIProcessGroup OSQA
+
+ #force all content to be served as static files
+ #otherwise django will be crunching images through itself wasting time
+ Alias /m/ /path/to/osqa-site/forum/skins/
+ Alias /upfiles/ /path/to/osqa-site/forum/upfiles/
+
+ Order deny,allow
+ Allow from all
+
+
+ #this is your wsgi script described in the prev section
+ WSGIScriptAlias / /path/to/osqa-site/osqa.wsgi
+
+ #this will force admin interface to work only
+ #through https (optional)
+ #"nimda" is the secret spelling of "admin" ;)
+
+ RewriteEngine on
+ RewriteRule /nimda(.*)$ https://example.com/nimda$1 [L,R=301]
+
+ CustomLog /var/log/httpd/OSQA/access_log common
+ ErrorLog /var/log/httpd/OSQA/error_log
+
+#(optional) run admin interface under https
+
+ ServerAdmin forum@example.com
+ DocumentRoot /path/to/osqa-site
+ ServerName example.com
+ SSLEngine on
+ SSLCertificateFile /path/to/ssl-certificate/server.crt
+ SSLCertificateKeyFile /path/to/ssl-certificate/server.key
+ WSGIScriptAlias / /path/to/osqa-site/osqa.wsgi
+ CustomLog /var/log/httpd/OSQA/access_log common
+ ErrorLog /var/log/httpd/OSQA/error_log
+ DirectoryIndex index.html
+
+-------------
+
+5. Full text search (using sphinx search)
+
+ Currently full text search works only with sphinx search engine
+ And builtin PostgreSQL (postgres only >= 8.3???)
+
+ 5.1 Instructions for Sphinx search setup
+ 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 osqa index first time manually
+
+ % indexer --config /path/to/sphinx.conf --index osqa
+
+ 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!
+
+ in settings.py look for INSTALLED_APPS
+ and uncomment #'djangosphinx',
+
+
+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 /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 = 'osqa.log' #where logging messages should go
+DATABASE_NAME = 'osqa' # 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 = '[OSQA] '
+EMAIL_HOST='osqa.com'
+EMAIL_PORT='25'
+EMAIL_USE_TLS=False
+TIME_ZONE = 'America/Tijuana'
+APP_TITLE = u'OSQA Q&A Forum' #title of your forum
+APP_KEYWORDS = u'OSQA,forum,community' #keywords for search engines
+APP_DESCRIPTION = u'Ask and answer questions.' #site description for searche engines
+APP_INTRO = u'
'
+ if num_moot > 0:
+ text += ''
+ 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 += '
'
+
+ link = url_prefix + user.get_profile_url() + '?sort=email_subscriptions'
+ text += _('go to %(link)s to change frequency of email updates or %(email)s administrator') \
+ % {'link':link, 'email':settings.ADMINS[0][1]}
+ msg = EmailMessage(subject, text, settings.DEFAULT_FROM_EMAIL, [user.email])
+ msg.content_subtype = 'html'
+ msg.send()
diff --git a/forum/management/commands/subscribe_everyone.py b/forum/management/commands/subscribe_everyone.py
new file mode 100644
index 0000000..c79528f
--- /dev/null
+++ b/forum/management/commands/subscribe_everyone.py
@@ -0,0 +1,32 @@
+from django.core.management.base import NoArgsCommand
+from django.db import connection
+from django.db.models import Q, F
+from forum.models import *
+from django.core.mail import EmailMessage
+from django.utils.translation import ugettext as _
+from django.utils.translation import ungettext
+import datetime
+from django.conf import settings
+
+class Command(NoArgsCommand):
+ def handle_noargs(self,**options):
+ try:
+ try:
+ self.subscribe_everyone()
+ except Exception, e:
+ print e
+ finally:
+ connection.close()
+
+ def subscribe_everyone(self):
+
+ feed_type_info = EmailFeedSetting.FEED_TYPES
+ for user in User.objects.all():
+ for feed_type in feed_type_info:
+ try:
+ feed_setting = EmailFeedSetting.objects.get(subscriber=user,feed_type = feed_type[0])
+ except EmailFeedSetting.DoesNotExist:
+ feed_setting = EmailFeedSetting(subscriber=user,feed_type=feed_type[0])
+ feed_setting.frequency = 'w'
+ feed_setting.reported_at = None
+ feed_setting.save()
diff --git a/forum/middleware/__init__.py b/forum/middleware/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/forum/middleware/anon_user.py b/forum/middleware/anon_user.py
new file mode 100644
index 0000000..866734d
--- /dev/null
+++ b/forum/middleware/anon_user.py
@@ -0,0 +1,35 @@
+from django.http import HttpResponseRedirect
+from forum.utils.forms import get_next_url
+from django.utils.translation import ugettext as _
+from forum.user_messages import create_message, get_and_delete_messages
+from django.conf import settings
+from django.core.urlresolvers import reverse
+import logging
+
+class AnonymousMessageManager(object):
+ def __init__(self,request):
+ self.request = request
+ def create(self,message=''):
+ create_message(self.request,message)
+ def get_and_delete(self):
+ messages = get_and_delete_messages(self.request)
+ return messages
+
+def dummy_deepcopy(*arg):
+ """this is necessary to prevent deepcopy() on anonymous user object
+ that now contains reference to request, which cannot be deepcopied
+ """
+ return None
+
+class ConnectToSessionMessagesMiddleware(object):
+ def process_request(self, request):
+ if not request.user.is_authenticated():
+ request.user.__deepcopy__ = dummy_deepcopy #plug on deepcopy which may be called by django db "driver"
+ request.user.message_set = AnonymousMessageManager(request) #here request is linked to anon user
+ request.user.get_and_delete_messages = request.user.message_set.get_and_delete
+
+ #also set the first greeting one time per session only
+ if 'greeting_set' not in request.session:
+ request.session['greeting_set'] = True
+ msg = _('First time here? Check out the FAQ!') % reverse('faq')
+ request.user.message_set.create(message=msg)
diff --git a/forum/middleware/cancel.py b/forum/middleware/cancel.py
new file mode 100644
index 0000000..15a4371
--- /dev/null
+++ b/forum/middleware/cancel.py
@@ -0,0 +1,15 @@
+from django.http import HttpResponseRedirect
+from forum.utils.forms import get_next_url
+import logging
+class CancelActionMiddleware(object):
+ def process_view(self, request, view_func, view_args, view_kwargs):
+ if 'cancel' in request.REQUEST:
+ #todo use session messages for the anonymous users
+ try:
+ msg = getattr(view_func,'CANCEL_MESSAGE')
+ except AttributeError:
+ msg = 'action canceled'
+ request.user.message_set.create(message=msg)
+ return HttpResponseRedirect(get_next_url(request))
+ else:
+ return None
diff --git a/forum/middleware/pagesize.py b/forum/middleware/pagesize.py
new file mode 100644
index 0000000..f6e6fcf
--- /dev/null
+++ b/forum/middleware/pagesize.py
@@ -0,0 +1,33 @@
+# used in questions
+QUESTIONS_PAGE_SIZE = 10
+class QuestionsPageSizeMiddleware(object):
+ def process_request(self, request):
+ # Set flag to False by default. If it is equal to True, then need to be saved.
+ pagesize_changed = False
+ # get pagesize from session, if failed then get default value
+ user_page_size = request.session.get("pagesize", QUESTIONS_PAGE_SIZE)
+ # set pagesize equal to logon user specified value in database
+ if request.user.is_authenticated() and request.user.questions_per_page > 0:
+ user_page_size = request.user.questions_per_page
+
+ try:
+ # get new pagesize from UI selection
+ pagesize = int(request.GET.get('pagesize', user_page_size))
+ if pagesize <> user_page_size:
+ pagesize_changed = True
+
+ except ValueError:
+ pagesize = user_page_size
+
+ # save this pagesize to user database
+ if pagesize_changed:
+ if request.user.is_authenticated():
+ user = request.user
+ user.questions_per_page = pagesize
+ user.save()
+ # put pagesize into session
+ request.session["pagesize"] = pagesize
+
+ def process_exception(self,request,exception):
+ import logging
+ logging.debug('have exception %s' % str(exception))
diff --git a/forum/models/__init__.py b/forum/models/__init__.py
new file mode 100755
index 0000000..12a0239
--- /dev/null
+++ b/forum/models/__init__.py
@@ -0,0 +1,343 @@
+from question import Question ,QuestionRevision, QuestionView, AnonymousQuestion, FavoriteQuestion
+from answer import Answer, AnonymousAnswer, AnswerRevision
+from tag import Tag, MarkedTag
+from meta import Vote, Comment, FlaggedItem
+from user import Activity, AnonymousEmail, EmailFeedSetting, AuthKeyUserAssociation
+from repute import Badge, Award, Repute
+
+from base import *
+
+# User extend properties
+QUESTIONS_PER_PAGE_CHOICES = (
+ (10, u'10'),
+ (30, u'30'),
+ (50, u'50'),
+)
+
+def user_is_username_taken(cls,username):
+ try:
+ cls.objects.get(username=username)
+ return True
+ except cls.MultipleObjectsReturned:
+ return True
+ except cls.DoesNotExist:
+ return False
+
+def user_get_q_sel_email_feed_frequency(self):
+ #print 'looking for frequency for user %s' % self
+ try:
+ feed_setting = EmailFeedSetting.objects.get(subscriber=self,feed_type='q_sel')
+ except Exception, e:
+ #print 'have error %s' % e.message
+ raise e
+ #print 'have freq=%s' % feed_setting.frequency
+ return feed_setting.frequency
+
+User.add_to_class('is_approved', models.BooleanField(default=False))
+User.add_to_class('email_isvalid', models.BooleanField(default=False))
+User.add_to_class('email_key', models.CharField(max_length=32, null=True))
+User.add_to_class('reputation', models.PositiveIntegerField(default=1))
+User.add_to_class('gravatar', models.CharField(max_length=32))
+
+#User.add_to_class('favorite_questions',
+# models.ManyToManyField(Question, through=FavoriteQuestion,
+# related_name='favorited_by'))
+
+#User.add_to_class('badges', models.ManyToManyField(Badge, through=Award,
+# related_name='awarded_to'))
+User.add_to_class('gold', models.SmallIntegerField(default=0))
+User.add_to_class('silver', models.SmallIntegerField(default=0))
+User.add_to_class('bronze', models.SmallIntegerField(default=0))
+User.add_to_class('questions_per_page',
+ models.SmallIntegerField(choices=QUESTIONS_PER_PAGE_CHOICES, default=10))
+User.add_to_class('last_seen',
+ models.DateTimeField(default=datetime.datetime.now))
+User.add_to_class('real_name', models.CharField(max_length=100, blank=True))
+User.add_to_class('website', models.URLField(max_length=200, blank=True))
+User.add_to_class('location', models.CharField(max_length=100, blank=True))
+User.add_to_class('date_of_birth', models.DateField(null=True, blank=True))
+User.add_to_class('about', models.TextField(blank=True))
+User.add_to_class('is_username_taken',classmethod(user_is_username_taken))
+User.add_to_class('get_q_sel_email_feed_frequency',user_get_q_sel_email_feed_frequency)
+User.add_to_class('hide_ignored_questions', models.BooleanField(default=False))
+User.add_to_class('tag_filter_setting',
+ models.CharField(
+ max_length=16,
+ choices=TAG_EMAIL_FILTER_CHOICES,
+ default='ignored'
+ )
+ )
+
+# custom signal
+tags_updated = django.dispatch.Signal(providing_args=["question"])
+edit_question_or_answer = django.dispatch.Signal(providing_args=["instance", "modified_by"])
+delete_post_or_answer = django.dispatch.Signal(providing_args=["instance", "deleted_by"])
+mark_offensive = django.dispatch.Signal(providing_args=["instance", "mark_by"])
+user_updated = django.dispatch.Signal(providing_args=["instance", "updated_by"])
+user_logged_in = django.dispatch.Signal(providing_args=["session"])
+
+
+def get_messages(self):
+ messages = []
+ for m in self.message_set.all():
+ messages.append(m.message)
+ return messages
+
+def delete_messages(self):
+ self.message_set.all().delete()
+
+def get_profile_url(self):
+ """Returns the URL for this User's profile."""
+ return '%s%s/' % (reverse('user', args=[self.id]), slugify(self.username))
+
+def get_profile_link(self):
+ profile_link = u'%s' % (self.get_profile_url(),self.username)
+ logging.debug('in get profile link %s' % profile_link)
+ return mark_safe(profile_link)
+
+User.add_to_class('get_profile_url', get_profile_url)
+User.add_to_class('get_profile_link', get_profile_link)
+User.add_to_class('get_messages', get_messages)
+User.add_to_class('delete_messages', delete_messages)
+
+def calculate_gravatar_hash(instance, **kwargs):
+ """Calculates a User's gravatar hash from their email address."""
+ if kwargs.get('raw', False):
+ return
+ instance.gravatar = hashlib.md5(instance.email).hexdigest()
+
+def record_ask_event(instance, created, **kwargs):
+ if created:
+ activity = Activity(user=instance.author, active_at=instance.added_at, content_object=instance, activity_type=TYPE_ACTIVITY_ASK_QUESTION)
+ activity.save()
+
+def record_answer_event(instance, created, **kwargs):
+ if created:
+ activity = Activity(user=instance.author, active_at=instance.added_at, content_object=instance, activity_type=TYPE_ACTIVITY_ANSWER)
+ activity.save()
+
+def record_comment_event(instance, created, **kwargs):
+ if created:
+ from django.contrib.contenttypes.models import ContentType
+ question_type = ContentType.objects.get_for_model(Question)
+ question_type_id = question_type.id
+ if (instance.content_type_id == question_type_id):
+ type = TYPE_ACTIVITY_COMMENT_QUESTION
+ else:
+ type = TYPE_ACTIVITY_COMMENT_ANSWER
+ activity = Activity(user=instance.user, active_at=instance.added_at, content_object=instance, activity_type=type)
+ activity.save()
+
+def record_revision_question_event(instance, created, **kwargs):
+ if created and instance.revision <> 1:
+ activity = Activity(user=instance.author, active_at=instance.revised_at, content_object=instance, activity_type=TYPE_ACTIVITY_UPDATE_QUESTION)
+ activity.save()
+
+def record_revision_answer_event(instance, created, **kwargs):
+ if created and instance.revision <> 1:
+ activity = Activity(user=instance.author, active_at=instance.revised_at, content_object=instance, activity_type=TYPE_ACTIVITY_UPDATE_ANSWER)
+ activity.save()
+
+def record_award_event(instance, created, **kwargs):
+ """
+ After we awarded a badge to user, we need to record this activity and notify user.
+ We also recaculate awarded_count of this badge and user information.
+ """
+ if created:
+ activity = Activity(user=instance.user, active_at=instance.awarded_at, content_object=instance,
+ activity_type=TYPE_ACTIVITY_PRIZE)
+ activity.save()
+
+ instance.badge.awarded_count += 1
+ instance.badge.save()
+
+ if instance.badge.type == Badge.GOLD:
+ instance.user.gold += 1
+ if instance.badge.type == Badge.SILVER:
+ instance.user.silver += 1
+ if instance.badge.type == Badge.BRONZE:
+ instance.user.bronze += 1
+ instance.user.save()
+
+def notify_award_message(instance, created, **kwargs):
+ """
+ Notify users when they have been awarded badges by using Django message.
+ """
+ if created:
+ user = instance.user
+ user.message_set.create(message=u"Congratulations, you have received a badge '%s'" % instance.badge.name)
+
+def record_answer_accepted(instance, created, **kwargs):
+ """
+ when answer is accepted, we record this for question author - who accepted it.
+ """
+ if not created and instance.accepted:
+ activity = Activity(user=instance.question.author, active_at=datetime.datetime.now(), \
+ content_object=instance, activity_type=TYPE_ACTIVITY_MARK_ANSWER)
+ activity.save()
+
+def update_last_seen(instance, created, **kwargs):
+ """
+ when user has activities, we update 'last_seen' time stamp for him
+ """
+ user = instance.user
+ user.last_seen = datetime.datetime.now()
+ user.save()
+
+def record_vote(instance, created, **kwargs):
+ """
+ when user have voted
+ """
+ if created:
+ if instance.vote == 1:
+ vote_type = TYPE_ACTIVITY_VOTE_UP
+ else:
+ vote_type = TYPE_ACTIVITY_VOTE_DOWN
+
+ activity = Activity(user=instance.user, active_at=instance.voted_at, content_object=instance, activity_type=vote_type)
+ activity.save()
+
+def record_cancel_vote(instance, **kwargs):
+ """
+ when user canceled vote, the vote will be deleted.
+ """
+ activity = Activity(user=instance.user, active_at=datetime.datetime.now(), content_object=instance, activity_type=TYPE_ACTIVITY_CANCEL_VOTE)
+ activity.save()
+
+def record_delete_question(instance, delete_by, **kwargs):
+ """
+ when user deleted the question
+ """
+ if instance.__class__ == "Question":
+ activity_type = TYPE_ACTIVITY_DELETE_QUESTION
+ else:
+ activity_type = TYPE_ACTIVITY_DELETE_ANSWER
+
+ activity = Activity(user=delete_by, active_at=datetime.datetime.now(), content_object=instance, activity_type=activity_type)
+ activity.save()
+
+def record_mark_offensive(instance, mark_by, **kwargs):
+ activity = Activity(user=mark_by, active_at=datetime.datetime.now(), content_object=instance, activity_type=TYPE_ACTIVITY_MARK_OFFENSIVE)
+ activity.save()
+
+def record_update_tags(question, **kwargs):
+ """
+ when user updated tags of the question
+ """
+ activity = Activity(user=question.author, active_at=datetime.datetime.now(), content_object=question, activity_type=TYPE_ACTIVITY_UPDATE_TAGS)
+ activity.save()
+
+def record_favorite_question(instance, created, **kwargs):
+ """
+ when user add the question in him favorite questions list.
+ """
+ if created:
+ activity = Activity(user=instance.user, active_at=datetime.datetime.now(), content_object=instance, activity_type=TYPE_ACTIVITY_FAVORITE)
+ activity.save()
+
+def record_user_full_updated(instance, **kwargs):
+ activity = Activity(user=instance, active_at=datetime.datetime.now(), content_object=instance, activity_type=TYPE_ACTIVITY_USER_FULL_UPDATED)
+ activity.save()
+
+def post_stored_anonymous_content(sender,user,session_key,signal,*args,**kwargs):
+ aq_list = AnonymousQuestion.objects.filter(session_key = session_key)
+ aa_list = AnonymousAnswer.objects.filter(session_key = session_key)
+ import settings
+ if settings.EMAIL_VALIDATION == 'on':#add user to the record
+ for aq in aq_list:
+ aq.author = user
+ aq.save()
+ for aa in aa_list:
+ aa.author = user
+ aa.save()
+ #maybe add pending posts message?
+ else: #just publish the questions
+ for aq in aq_list:
+ aq.publish(user)
+ for aa in aa_list:
+ aa.publish(user)
+
+#signal for User modle save changes
+
+pre_save.connect(calculate_gravatar_hash, sender=User)
+post_save.connect(record_ask_event, sender=Question)
+post_save.connect(record_answer_event, sender=Answer)
+post_save.connect(record_comment_event, sender=Comment)
+post_save.connect(record_revision_question_event, sender=QuestionRevision)
+post_save.connect(record_revision_answer_event, sender=AnswerRevision)
+post_save.connect(record_award_event, sender=Award)
+post_save.connect(notify_award_message, sender=Award)
+post_save.connect(record_answer_accepted, sender=Answer)
+post_save.connect(update_last_seen, sender=Activity)
+post_save.connect(record_vote, sender=Vote)
+post_delete.connect(record_cancel_vote, sender=Vote)
+delete_post_or_answer.connect(record_delete_question, sender=Question)
+delete_post_or_answer.connect(record_delete_question, sender=Answer)
+mark_offensive.connect(record_mark_offensive, sender=Question)
+mark_offensive.connect(record_mark_offensive, sender=Answer)
+tags_updated.connect(record_update_tags, sender=Question)
+post_save.connect(record_favorite_question, sender=FavoriteQuestion)
+user_updated.connect(record_user_full_updated, sender=User)
+user_logged_in.connect(post_stored_anonymous_content)
+
+Question = Question
+QuestionRevision = QuestionRevision
+QuestionView = QuestionView
+FavoriteQuestion = FavoriteQuestion
+AnonymousQuestion = AnonymousQuestion
+
+Answer = Answer
+AnswerRevision = AnswerRevision
+AnonymousAnswer = AnonymousAnswer
+
+Tag = Tag
+Comment = Comment
+Vote = Vote
+FlaggedItem = FlaggedItem
+MarkedTag = MarkedTag
+
+Badge = Badge
+Award = Award
+Repute = Repute
+
+Activity = Activity
+EmailFeedSetting = EmailFeedSetting
+AnonymousEmail = AnonymousEmail
+AuthKeyUserAssociation = AuthKeyUserAssociation
+
+__all__ = [
+ 'Question',
+ 'QuestionRevision',
+ 'QuestionView',
+ 'FavoriteQuestion',
+ 'AnonymousQuestion',
+
+ 'Answer',
+ 'AnswerRevision',
+ 'AnonymousAnswer',
+
+ 'Tag',
+ 'Comment',
+ 'Vote',
+ 'FlaggedItem',
+ 'MarkedTag',
+
+ 'Badge',
+ 'Award',
+ 'Repute',
+
+ 'Activity',
+ 'EmailFeedSetting',
+ 'AnonymousEmail',
+ 'AuthKeyUserAssociation',
+
+ 'User'
+ ]
+
+
+from forum.modules import get_modules_script_classes
+
+for k, v in get_modules_script_classes('models', models.Model).items():
+ if not k in __all__:
+ __all__.append(k)
+ exec "%s = v" % k
\ No newline at end of file
diff --git a/forum/models/answer.py b/forum/models/answer.py
new file mode 100755
index 0000000..14199de
--- /dev/null
+++ b/forum/models/answer.py
@@ -0,0 +1,134 @@
+from base import *
+
+from question import Question
+
+class AnswerManager(models.Manager):
+ @staticmethod
+ def create_new(cls, question=None, author=None, added_at=None, wiki=False, text='', email_notify=False):
+ answer = Answer(
+ question = question,
+ author = author,
+ added_at = added_at,
+ wiki = wiki,
+ html = text
+ )
+ if answer.wiki:
+ answer.last_edited_by = answer.author
+ answer.last_edited_at = added_at
+ answer.wikified_at = added_at
+
+ answer.save()
+
+ #update question data
+ question.last_activity_at = added_at
+ question.last_activity_by = author
+ question.save()
+ Question.objects.update_answer_count(question)
+
+ AnswerRevision.objects.create(
+ answer = answer,
+ revision = 1,
+ author = author,
+ revised_at = added_at,
+ summary = CONST['default_version'],
+ text = text
+ )
+
+ #set notification/delete
+ if email_notify:
+ if author not in question.followed_by.all():
+ question.followed_by.add(author)
+ else:
+ #not sure if this is necessary. ajax should take care of this...
+ try:
+ question.followed_by.remove(author)
+ except:
+ pass
+
+ #GET_ANSWERS_FROM_USER_QUESTIONS = u'SELECT answer.* FROM answer INNER JOIN question ON answer.question_id = question.id WHERE question.author_id =%s AND answer.author_id <> %s'
+ def get_answers_from_question(self, question, user=None):
+ """
+ Retrieves visibile answers for the given question. Delete answers
+ are only visibile to the person who deleted them.
+ """
+
+ if user is None or not user.is_authenticated():
+ return self.filter(question=question, deleted=False)
+ else:
+ return self.filter(models.Q(question=question),
+ models.Q(deleted=False) | models.Q(deleted_by=user))
+
+ #todo: I think this method is not being used anymore, I'll just comment it for now
+ #def get_answers_from_questions(self, user_id):
+ # """
+ # Retrieves visibile answers for the given question. Which are not included own answers
+ # """
+ # cursor = connection.cursor()
+ # cursor.execute(self.GET_ANSWERS_FROM_USER_QUESTIONS, [user_id, user_id])
+ # return cursor.fetchall()
+
+class Answer(Content, DeletableContent):
+ question = models.ForeignKey('Question', related_name='answers')
+ accepted = models.BooleanField(default=False)
+ accepted_at = models.DateTimeField(null=True, blank=True)
+
+ objects = AnswerManager()
+
+ class Meta(Content.Meta):
+ db_table = u'answer'
+
+ def get_user_vote(self, user):
+ if user.__class__.__name__ == "AnonymousUser":
+ return None
+
+ votes = self.votes.filter(user=user)
+ if votes and votes.count() > 0:
+ return votes[0]
+ else:
+ return None
+
+ def get_latest_revision(self):
+ return self.revisions.all()[0]
+
+ def get_question_title(self):
+ return self.question.title
+
+ def get_absolute_url(self):
+ return '%s%s#%s' % (reverse('question', args=[self.question.id]), django_urlquote(slugify(self.question.title)), self.id)
+
+ def __unicode__(self):
+ return self.html
+
+
+class AnswerRevision(ContentRevision):
+ """A revision of an Answer."""
+ answer = models.ForeignKey('Answer', related_name='revisions')
+
+ def get_absolute_url(self):
+ return reverse('answer_revisions', kwargs={'id':self.answer.id})
+
+ def get_question_title(self):
+ return self.answer.question.title
+
+ class Meta(ContentRevision.Meta):
+ db_table = u'answer_revision'
+ ordering = ('-revision',)
+
+ def save(self, **kwargs):
+ """Looks up the next available revision number if not set."""
+ if not self.revision:
+ self.revision = AnswerRevision.objects.filter(
+ answer=self.answer).values_list('revision',
+ flat=True)[0] + 1
+ super(AnswerRevision, self).save(**kwargs)
+
+class AnonymousAnswer(AnonymousContent):
+ question = models.ForeignKey('Question', related_name='anonymous_answers')
+
+ def publish(self,user):
+ added_at = datetime.datetime.now()
+ #print user.id
+ AnswerManager.create_new(question=self.question,wiki=self.wiki,
+ added_at=added_at,text=self.text,
+ author=user)
+ self.delete()
diff --git a/forum/models/base.py b/forum/models/base.py
new file mode 100755
index 0000000..2c28a47
--- /dev/null
+++ b/forum/models/base.py
@@ -0,0 +1,139 @@
+import datetime
+import hashlib
+from urllib import quote_plus, urlencode
+from django.db import models, IntegrityError, connection, transaction
+from django.utils.http import urlquote as django_urlquote
+from django.utils.html import strip_tags
+from django.core.urlresolvers import reverse
+from django.contrib.auth.models import User
+from django.contrib.contenttypes import generic
+from django.contrib.contenttypes.models import ContentType
+from django.template.defaultfilters import slugify
+from django.db.models.signals import post_delete, post_save, pre_save
+from django.utils.translation import ugettext as _
+from django.utils.safestring import mark_safe
+from django.contrib.sitemaps import ping_google
+import django.dispatch
+from django.conf import settings
+import logging
+
+if settings.USE_SPHINX_SEARCH == True:
+ from djangosphinx.models import SphinxSearch
+
+from forum.const import *
+
+class MetaContent(models.Model):
+ """
+ Base class for Vote, Comment and FlaggedItem
+ """
+ content_type = models.ForeignKey(ContentType)
+ object_id = models.PositiveIntegerField()
+ content_object = generic.GenericForeignKey('content_type', 'object_id')
+ user = models.ForeignKey(User, related_name='%(class)ss')
+
+ class Meta:
+ abstract = True
+ app_label = 'forum'
+
+
+class DeletableContent(models.Model):
+ deleted = models.BooleanField(default=False)
+ deleted_at = models.DateTimeField(null=True, blank=True)
+ deleted_by = models.ForeignKey(User, null=True, blank=True, related_name='deleted_%(class)ss')
+
+ class Meta:
+ abstract = True
+ app_label = 'forum'
+
+
+class ContentRevision(models.Model):
+ """
+ Base class for QuestionRevision and AnswerRevision
+ """
+ revision = models.PositiveIntegerField()
+ author = models.ForeignKey(User, related_name='%(class)ss')
+ revised_at = models.DateTimeField()
+ summary = models.CharField(max_length=300, blank=True)
+ text = models.TextField()
+
+ class Meta:
+ abstract = True
+ app_label = 'forum'
+
+
+class AnonymousContent(models.Model):
+ """
+ Base class for AnonymousQuestion and AnonymousAnswer
+ """
+ session_key = models.CharField(max_length=40) #session id for anonymous questions
+ wiki = models.BooleanField(default=False)
+ added_at = models.DateTimeField(default=datetime.datetime.now)
+ ip_addr = models.IPAddressField(max_length=21) #allow high port numbers
+ author = models.ForeignKey(User,null=True)
+ text = models.TextField()
+ summary = models.CharField(max_length=180)
+
+ class Meta:
+ abstract = True
+ app_label = 'forum'
+
+
+from meta import Comment, Vote, FlaggedItem
+
+class Content(models.Model):
+ """
+ Base class for Question and Answer
+ """
+ author = models.ForeignKey(User, related_name='%(class)ss')
+ added_at = models.DateTimeField(default=datetime.datetime.now)
+
+ wiki = models.BooleanField(default=False)
+ wikified_at = models.DateTimeField(null=True, blank=True)
+
+ locked = models.BooleanField(default=False)
+ locked_by = models.ForeignKey(User, null=True, blank=True, related_name='locked_%(class)ss')
+ locked_at = models.DateTimeField(null=True, blank=True)
+
+ score = models.IntegerField(default=0)
+ vote_up_count = models.IntegerField(default=0)
+ vote_down_count = models.IntegerField(default=0)
+
+ comment_count = models.PositiveIntegerField(default=0)
+ offensive_flag_count = models.SmallIntegerField(default=0)
+
+ last_edited_at = models.DateTimeField(null=True, blank=True)
+ last_edited_by = models.ForeignKey(User, null=True, blank=True, related_name='last_edited_%(class)ss')
+
+ html = models.TextField()
+ comments = generic.GenericRelation(Comment)
+ votes = generic.GenericRelation(Vote)
+ flagged_items = generic.GenericRelation(FlaggedItem)
+
+ class Meta:
+ abstract = True
+ app_label = 'forum'
+
+ def save(self,**kwargs):
+ super(Content,self).save(**kwargs)
+ try:
+ ping_google()
+ except Exception:
+ logging.debug('problem pinging google did you register you sitemap with google?')
+
+ def get_object_comments(self):
+ comments = self.comments.all().order_by('id')
+ return comments
+
+ def post_get_last_update_info(self):
+ when = self.added_at
+ who = self.author
+ if self.last_edited_at and self.last_edited_at > when:
+ when = self.last_edited_at
+ who = self.last_edited_by
+ comments = self.comments.all()
+ if len(comments) > 0:
+ for c in comments:
+ if c.added_at > when:
+ when = c.added_at
+ who = c.user
+ return when, who
\ No newline at end of file
diff --git a/forum/models/meta.py b/forum/models/meta.py
new file mode 100755
index 0000000..3dfd3e8
--- /dev/null
+++ b/forum/models/meta.py
@@ -0,0 +1,89 @@
+from base import *
+
+class VoteManager(models.Manager):
+ def get_up_vote_count_from_user(self, user):
+ if user is not None:
+ return self.filter(user=user, vote=1).count()
+ else:
+ return 0
+
+ def get_down_vote_count_from_user(self, user):
+ if user is not None:
+ return self.filter(user=user, vote=-1).count()
+ else:
+ return 0
+
+ def get_votes_count_today_from_user(self, user):
+ if user is not None:
+ today = datetime.date.today()
+ return self.filter(user=user, voted_at__range=(today, today + datetime.timedelta(1))).count()
+
+ else:
+ return 0
+
+
+class Vote(MetaContent):
+ VOTE_UP = +1
+ VOTE_DOWN = -1
+ VOTE_CHOICES = (
+ (VOTE_UP, u'Up'),
+ (VOTE_DOWN, u'Down'),
+ )
+
+ vote = models.SmallIntegerField(choices=VOTE_CHOICES)
+ voted_at = models.DateTimeField(default=datetime.datetime.now)
+
+ objects = VoteManager()
+
+ class Meta(MetaContent.Meta):
+ unique_together = ('content_type', 'object_id', 'user')
+ db_table = u'vote'
+
+ def __unicode__(self):
+ return '[%s] voted at %s: %s' %(self.user, self.voted_at, self.vote)
+
+ def is_upvote(self):
+ return self.vote == self.VOTE_UP
+
+ def is_downvote(self):
+ return self.vote == self.VOTE_DOWN
+
+
+class FlaggedItemManager(models.Manager):
+ def get_flagged_items_count_today(self, user):
+ if user is not None:
+ today = datetime.date.today()
+ return self.filter(user=user, flagged_at__range=(today, today + datetime.timedelta(1))).count()
+ else:
+ return 0
+
+class FlaggedItem(MetaContent):
+ """A flag on a Question or Answer indicating offensive content."""
+ flagged_at = models.DateTimeField(default=datetime.datetime.now)
+
+ objects = FlaggedItemManager()
+
+ class Meta(MetaContent.Meta):
+ unique_together = ('content_type', 'object_id', 'user')
+ db_table = u'flagged_item'
+
+ def __unicode__(self):
+ return '[%s] flagged at %s' %(self.user, self.flagged_at)
+
+class Comment(MetaContent):
+ comment = models.CharField(max_length=300)
+ added_at = models.DateTimeField(default=datetime.datetime.now)
+
+ class Meta(MetaContent.Meta):
+ ordering = ('-added_at',)
+ db_table = u'comment'
+
+ def save(self,**kwargs):
+ super(Comment,self).save(**kwargs)
+ try:
+ ping_google()
+ except Exception:
+ logging.debug('problem pinging google did you register you sitemap with google?')
+
+ def __unicode__(self):
+ return self.comment
\ No newline at end of file
diff --git a/forum/models/question.py b/forum/models/question.py
new file mode 100755
index 0000000..f916e65
--- /dev/null
+++ b/forum/models/question.py
@@ -0,0 +1,336 @@
+from base import *
+from tag import Tag
+
+class QuestionManager(models.Manager):
+ @staticmethod
+ def create_new(cls, title=None,author=None,added_at=None, wiki=False,tagnames=None,summary=None, text=None):
+ question = Question(
+ title = title,
+ author = author,
+ added_at = added_at,
+ last_activity_at = added_at,
+ last_activity_by = author,
+ wiki = wiki,
+ tagnames = tagnames,
+ html = text,
+ summary = summary
+ )
+ if question.wiki:
+ question.last_edited_by = question.author
+ question.last_edited_at = added_at
+ question.wikified_at = added_at
+
+ question.save()
+
+ # create the first revision
+ QuestionRevision.objects.create(
+ question = question,
+ revision = 1,
+ title = question.title,
+ author = author,
+ revised_at = added_at,
+ tagnames = question.tagnames,
+ summary = CONST['default_version'],
+ text = text
+ )
+ return question
+
+ def update_tags(self, question, tagnames, user):
+ """
+ Updates Tag associations for a question to match the given
+ tagname string.
+
+ Returns ``True`` if tag usage counts were updated as a result,
+ ``False`` otherwise.
+ """
+
+ current_tags = list(question.tags.all())
+ current_tagnames = set(t.name for t in current_tags)
+ updated_tagnames = set(t for t in tagnames.split(' ') if t)
+ modified_tags = []
+
+ removed_tags = [t for t in current_tags
+ if t.name not in updated_tagnames]
+ if removed_tags:
+ modified_tags.extend(removed_tags)
+ question.tags.remove(*removed_tags)
+
+ added_tagnames = updated_tagnames - current_tagnames
+ if added_tagnames:
+ added_tags = Tag.objects.get_or_create_multiple(added_tagnames,
+ user)
+ modified_tags.extend(added_tags)
+ question.tags.add(*added_tags)
+
+ if modified_tags:
+ Tag.objects.update_use_counts(modified_tags)
+ return True
+
+ return False
+
+ def update_answer_count(self, question):
+ """
+ Executes an UPDATE query to update denormalised data with the
+ number of answers the given question has.
+ """
+
+ # for some reasons, this Answer class failed to be imported,
+ # although we have imported all classes from models on top.
+ from answer import Answer
+ self.filter(id=question.id).update(
+ answer_count=Answer.objects.get_answers_from_question(question).filter(deleted=False).count())
+
+ def update_view_count(self, question):
+ """
+ update counter+1 when user browse question page
+ """
+ self.filter(id=question.id).update(view_count = question.view_count + 1)
+
+ def update_favorite_count(self, question):
+ """
+ update favourite_count for given question
+ """
+ self.filter(id=question.id).update(favourite_count = FavoriteQuestion.objects.filter(question=question).count())
+
+ def get_similar_questions(self, question):
+ """
+ Get 10 similar questions for given one.
+ This will search the same tag list for give question(by exactly same string) first.
+ Questions with the individual tags will be added to list if above questions are not full.
+ """
+ #print datetime.datetime.now()
+ 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]
+ for item in extend_questions:
+ if item not in questions and len(questions) < 10:
+ questions.append(item)
+
+ #print datetime.datetime.now()
+ return questions
+
+class Question(Content, DeletableContent):
+ title = models.CharField(max_length=300)
+ tags = models.ManyToManyField('Tag', related_name='questions')
+ answer_accepted = models.BooleanField(default=False)
+ closed = models.BooleanField(default=False)
+ closed_by = models.ForeignKey(User, null=True, blank=True, related_name='closed_questions')
+ closed_at = models.DateTimeField(null=True, blank=True)
+ close_reason = models.SmallIntegerField(choices=CLOSE_REASONS, null=True, blank=True)
+ followed_by = models.ManyToManyField(User, related_name='followed_questions')
+
+ # Denormalised data
+ answer_count = models.PositiveIntegerField(default=0)
+ view_count = models.PositiveIntegerField(default=0)
+ favourite_count = models.PositiveIntegerField(default=0)
+ last_activity_at = models.DateTimeField(default=datetime.datetime.now)
+ last_activity_by = models.ForeignKey(User, related_name='last_active_in_questions')
+ tagnames = models.CharField(max_length=125)
+ summary = models.CharField(max_length=180)
+
+ favorited_by = models.ManyToManyField(User, through='FavoriteQuestion', related_name='favorite_questions')
+
+ objects = QuestionManager()
+
+ class Meta(Content.Meta):
+ db_table = u'question'
+
+ def delete(self):
+ super(Question, self).delete()
+ try:
+ ping_google()
+ except Exception:
+ logging.debug('problem pinging google did you register you sitemap with google?')
+
+ def save(self, **kwargs):
+ """
+ Overridden to manually manage addition of tags when the object
+ is first saved.
+
+ This is required as we're using ``tagnames`` as the sole means of
+ adding and editing tags.
+ """
+ initial_addition = (self.id is None)
+
+ super(Question, self).save(**kwargs)
+
+ if initial_addition:
+ tags = Tag.objects.get_or_create_multiple(self.tagname_list(),
+ self.author)
+ self.tags.add(*tags)
+ Tag.objects.update_use_counts(tags)
+
+ def tagname_list(self):
+ """Creates a list of Tag names from the ``tagnames`` attribute."""
+ return [name for name in self.tagnames.split(u' ')]
+
+ def tagname_meta_generator(self):
+ return u','.join([unicode(tag) for tag in self.tagname_list()])
+
+ def get_absolute_url(self):
+ return '%s%s' % (reverse('question', args=[self.id]), django_urlquote(slugify(self.title)))
+
+ def has_favorite_by_user(self, user):
+ if not user.is_authenticated():
+ return False
+
+ return FavoriteQuestion.objects.filter(question=self, user=user).count() > 0
+
+ def get_answer_count_by_user(self, user_id):
+ from answer import Answer
+ query_set = Answer.objects.filter(author__id=user_id)
+ return query_set.filter(question=self).count()
+
+ def get_question_title(self):
+ if self.closed:
+ attr = CONST['closed']
+ elif self.deleted:
+ attr = CONST['deleted']
+ else:
+ attr = None
+ if attr is not None:
+ return u'%s %s' % (self.title, attr)
+ else:
+ return self.title
+
+ def get_revision_url(self):
+ return reverse('question_revisions', args=[self.id])
+
+ def get_latest_revision(self):
+ return self.revisions.all()[0]
+
+ def get_last_update_info(self):
+ when, who = self.post_get_last_update_info()
+
+ answers = self.answers.all()
+ if len(answers) > 0:
+ for a in answers:
+ a_when, a_who = a.post_get_last_update_info()
+ if a_when > when:
+ when = a_when
+ who = a_who
+
+ return when, who
+
+ def get_update_summary(self,last_reported_at=None,recipient_email=''):
+ edited = False
+ if self.last_edited_at and self.last_edited_at > last_reported_at:
+ if self.last_edited_by.email != recipient_email:
+ edited = True
+ comments = []
+ for comment in self.comments.all():
+ if comment.added_at > last_reported_at and comment.user.email != recipient_email:
+ comments.append(comment)
+ new_answers = []
+ answer_comments = []
+ modified_answers = []
+ commented_answers = []
+ import sets
+ commented_answers = sets.Set([])
+ for answer in self.answers.all():
+ if (answer.added_at > last_reported_at and answer.author.email != recipient_email):
+ new_answers.append(answer)
+ if (answer.last_edited_at
+ and answer.last_edited_at > last_reported_at
+ and answer.last_edited_by.email != recipient_email):
+ modified_answers.append(answer)
+ for comment in answer.comments.all():
+ if comment.added_at > last_reported_at and comment.user.email != recipient_email:
+ commented_answers.add(answer)
+ answer_comments.append(comment)
+
+ #create the report
+ if edited or new_answers or modified_answers or answer_comments:
+ out = []
+ if edited:
+ out.append(_('%(author)s modified the question') % {'author':self.last_edited_by.username})
+ if new_answers:
+ names = sets.Set(map(lambda x: x.author.username,new_answers))
+ people = ', '.join(names)
+ out.append(_('%(people)s posted %(new_answer_count)s new answers') \
+ % {'new_answer_count':len(new_answers),'people':people})
+ if comments:
+ names = sets.Set(map(lambda x: x.user.username,comments))
+ people = ', '.join(names)
+ out.append(_('%(people)s commented the question') % {'people':people})
+ if answer_comments:
+ names = sets.Set(map(lambda x: x.user.username,answer_comments))
+ people = ', '.join(names)
+ if len(commented_answers) > 1:
+ out.append(_('%(people)s commented answers') % {'people':people})
+ else:
+ out.append(_('%(people)s commented an answer') % {'people':people})
+ url = settings.APP_URL + self.get_absolute_url()
+ retval = '%s: \n' % (url,self.title)
+ out = map(lambda x: '
' + x + '
',out)
+ retval += '
' + '\n'.join(out) + '
\n'
+ return retval
+ else:
+ return None
+
+ def __unicode__(self):
+ return self.title
+
+
+class QuestionView(models.Model):
+ question = models.ForeignKey(Question, related_name='viewed')
+ who = models.ForeignKey(User, related_name='question_views')
+ when = models.DateTimeField()
+
+ class Meta:
+ app_label = 'forum'
+
+class FavoriteQuestion(models.Model):
+ """A favorite Question of a User."""
+ question = models.ForeignKey(Question)
+ user = models.ForeignKey(User, related_name='user_favorite_questions')
+ added_at = models.DateTimeField(default=datetime.datetime.now)
+
+ class Meta:
+ app_label = 'forum'
+ db_table = u'favorite_question'
+ def __unicode__(self):
+ return '[%s] favorited at %s' %(self.user, self.added_at)
+
+class QuestionRevision(ContentRevision):
+ """A revision of a Question."""
+ question = models.ForeignKey(Question, related_name='revisions')
+ title = models.CharField(max_length=300)
+ tagnames = models.CharField(max_length=125)
+
+ class Meta(ContentRevision.Meta):
+ db_table = u'question_revision'
+ ordering = ('-revision',)
+
+ def get_question_title(self):
+ return self.question.title
+
+ def get_absolute_url(self):
+ #print 'in QuestionRevision.get_absolute_url()'
+ return reverse('question_revisions', args=[self.question.id])
+
+ def save(self, **kwargs):
+ """Looks up the next available revision number."""
+ if not self.revision:
+ self.revision = QuestionRevision.objects.filter(
+ question=self.question).values_list('revision',
+ flat=True)[0] + 1
+ super(QuestionRevision, self).save(**kwargs)
+
+ def __unicode__(self):
+ return u'revision %s of %s' % (self.revision, self.title)
+
+class AnonymousQuestion(AnonymousContent):
+ title = models.CharField(max_length=300)
+ tagnames = models.CharField(max_length=125)
+
+ def publish(self,user):
+ added_at = datetime.datetime.now()
+ QuestionManager.create_new(title=self.title, author=user, added_at=added_at,
+ wiki=self.wiki, tagnames=self.tagnames,
+ summary=self.summary, text=self.text)
+ self.delete()
+
+from answer import Answer, AnswerManager
diff --git a/forum/models/repute.py b/forum/models/repute.py
new file mode 100755
index 0000000..a47ce47
--- /dev/null
+++ b/forum/models/repute.py
@@ -0,0 +1,109 @@
+from base import *
+
+from django.utils.translation import ugettext as _
+
+class Badge(models.Model):
+ """Awarded for notable actions performed on the site by Users."""
+ GOLD = 1
+ SILVER = 2
+ BRONZE = 3
+ TYPE_CHOICES = (
+ (GOLD, _('gold')),
+ (SILVER, _('silver')),
+ (BRONZE, _('bronze')),
+ )
+
+ name = models.CharField(max_length=50)
+ type = models.SmallIntegerField(choices=TYPE_CHOICES)
+ slug = models.SlugField(max_length=50, blank=True)
+ description = models.CharField(max_length=300)
+ multiple = models.BooleanField(default=False)
+ # Denormalised data
+ awarded_count = models.PositiveIntegerField(default=0)
+
+ awarded_to = models.ManyToManyField(User, through='Award', related_name='badges')
+
+ class Meta:
+ app_label = 'forum'
+ db_table = u'badge'
+ ordering = ('name',)
+ unique_together = ('name', 'type')
+
+ def __unicode__(self):
+ return u'%s: %s' % (self.get_type_display(), self.name)
+
+ def save(self, **kwargs):
+ if not self.slug:
+ self.slug = self.name#slugify(self.name)
+ super(Badge, self).save(**kwargs)
+
+ def get_absolute_url(self):
+ return '%s%s/' % (reverse('badge', args=[self.id]), self.slug)
+
+class AwardManager(models.Manager):
+ def get_recent_awards(self):
+ awards = super(AwardManager, self).extra(
+ select={'badge_id': 'badge.id', 'badge_name':'badge.name',
+ 'badge_description': 'badge.description', 'badge_type': 'badge.type',
+ 'user_id': 'auth_user.id', 'user_name': 'auth_user.username'
+ },
+ tables=['award', 'badge', 'auth_user'],
+ order_by=['-awarded_at'],
+ where=['auth_user.id=award.user_id AND badge_id=badge.id'],
+ ).values('badge_id', 'badge_name', 'badge_description', 'badge_type', 'user_id', 'user_name')
+ return awards
+
+class Award(models.Model):
+ """The awarding of a Badge to a User."""
+ user = models.ForeignKey(User, related_name='award_user')
+ badge = models.ForeignKey('Badge', related_name='award_badge')
+ content_type = models.ForeignKey(ContentType)
+ object_id = models.PositiveIntegerField()
+ content_object = generic.GenericForeignKey('content_type', 'object_id')
+ awarded_at = models.DateTimeField(default=datetime.datetime.now)
+ notified = models.BooleanField(default=False)
+
+ objects = AwardManager()
+
+ def __unicode__(self):
+ return u'[%s] is awarded a badge [%s] at %s' % (self.user.username, self.badge.name, self.awarded_at)
+
+ class Meta:
+ app_label = 'forum'
+ db_table = u'award'
+
+class ReputeManager(models.Manager):
+ def get_reputation_by_upvoted_today(self, user):
+ """
+ For one user in one day, he can only earn rep till certain score (ep. +200)
+ by upvoted(also substracted from upvoted canceled). This is because we need
+ to prohibit gaming system by upvoting/cancel again and again.
+ """
+ if user is not None:
+ today = datetime.date.today()
+ sums = self.filter(models.Q(reputation_type=1) | models.Q(reputation_type=-8),
+ user=user, reputed_at__range=(today, today + datetime.timedelta(1))). \
+ agregate(models.Sum('positive'), models.Sum('negative'))
+
+ return sums['positive__sum'] + sums['negative__sum']
+ else:
+ return 0
+
+class Repute(models.Model):
+ """The reputation histories for user"""
+ user = models.ForeignKey(User)
+ positive = models.SmallIntegerField(default=0)
+ negative = models.SmallIntegerField(default=0)
+ question = models.ForeignKey('Question')
+ reputed_at = models.DateTimeField(default=datetime.datetime.now)
+ reputation_type = models.SmallIntegerField(choices=TYPE_REPUTATION)
+ reputation = models.IntegerField(default=1)
+
+ objects = ReputeManager()
+
+ def __unicode__(self):
+ return u'[%s]\' reputation changed at %s' % (self.user.username, self.reputed_at)
+
+ class Meta:
+ app_label = 'forum'
+ db_table = u'repute'
diff --git a/forum/models/tag.py b/forum/models/tag.py
new file mode 100755
index 0000000..28b9e57
--- /dev/null
+++ b/forum/models/tag.py
@@ -0,0 +1,85 @@
+from base import *
+
+from django.utils.translation import ugettext as _
+
+class TagManager(models.Manager):
+ UPDATE_USED_COUNTS_QUERY = (
+ 'UPDATE tag '
+ 'SET used_count = ('
+ 'SELECT COUNT(*) FROM question_tags '
+ 'INNER JOIN question ON question_id=question.id '
+ 'WHERE tag_id = tag.id AND question.deleted=False'
+ ') '
+ 'WHERE id IN (%s)')
+
+ def get_valid_tags(self, page_size):
+ tags = self.all().filter(deleted=False).exclude(used_count=0).order_by("-id")[:page_size]
+ return tags
+
+ def get_or_create_multiple(self, names, user):
+ """
+ Fetches a list of Tags with the given names, creating any Tags
+ which don't exist when necesssary.
+ """
+ tags = list(self.filter(name__in=names))
+ #Set all these tag visible
+ for tag in tags:
+ if tag.deleted:
+ tag.deleted = False
+ tag.deleted_by = None
+ tag.deleted_at = None
+ tag.save()
+
+ if len(tags) < len(names):
+ existing_names = set(tag.name for tag in tags)
+ new_names = [name for name in names if name not in existing_names]
+ tags.extend([self.create(name=name, created_by=user)
+ for name in new_names if self.filter(name=name).count() == 0 and len(name.strip()) > 0])
+
+ return tags
+
+ def update_use_counts(self, tags):
+ """Updates the given Tags with their current use counts."""
+ if not tags:
+ return
+ cursor = connection.cursor()
+ query = self.UPDATE_USED_COUNTS_QUERY % ','.join(['%s'] * len(tags))
+ cursor.execute(query, [tag.id for tag in tags])
+ transaction.commit_unless_managed()
+
+ def get_tags_by_questions(self, questions):
+ question_ids = []
+ for question in questions:
+ question_ids.append(question.id)
+
+ question_ids_str = ','.join([str(id) for id in question_ids])
+ related_tags = self.extra(
+ tables=['tag', 'question_tags'],
+ where=["tag.id = question_tags.tag_id AND question_tags.question_id IN (" + question_ids_str + ")"]
+ ).distinct()
+
+ return related_tags
+
+class Tag(DeletableContent):
+ name = models.CharField(max_length=255, unique=True)
+ created_by = models.ForeignKey(User, related_name='created_tags')
+ # Denormalised data
+ used_count = models.PositiveIntegerField(default=0)
+
+ objects = TagManager()
+
+ class Meta(DeletableContent.Meta):
+ db_table = u'tag'
+ ordering = ('-used_count', 'name')
+
+ def __unicode__(self):
+ return self.name
+
+class MarkedTag(models.Model):
+ TAG_MARK_REASONS = (('good',_('interesting')),('bad',_('ignored')))
+ tag = models.ForeignKey('Tag', related_name='user_selections')
+ user = models.ForeignKey(User, related_name='tag_selections')
+ reason = models.CharField(max_length=16, choices=TAG_MARK_REASONS)
+
+ class Meta:
+ app_label = 'forum'
\ No newline at end of file
diff --git a/forum/models/user.py b/forum/models/user.py
new file mode 100755
index 0000000..9502416
--- /dev/null
+++ b/forum/models/user.py
@@ -0,0 +1,77 @@
+from base import *
+
+from django.utils.translation import ugettext as _
+
+class Activity(models.Model):
+ """
+ We keep some history data for user activities
+ """
+ user = models.ForeignKey(User)
+ activity_type = models.SmallIntegerField(choices=TYPE_ACTIVITY)
+ active_at = models.DateTimeField(default=datetime.datetime.now)
+ content_type = models.ForeignKey(ContentType)
+ object_id = models.PositiveIntegerField()
+ content_object = generic.GenericForeignKey('content_type', 'object_id')
+ is_auditted = models.BooleanField(default=False)
+
+ def __unicode__(self):
+ return u'[%s] was active at %s' % (self.user.username, self.active_at)
+
+ class Meta:
+ app_label = 'forum'
+ db_table = u'activity'
+
+class EmailFeedSetting(models.Model):
+ DELTA_TABLE = {
+ 'w':datetime.timedelta(7),
+ 'd':datetime.timedelta(1),
+ 'n':datetime.timedelta(-1),
+ }
+ FEED_TYPES = (
+ ('q_all',_('Entire forum')),
+ ('q_ask',_('Questions that I asked')),
+ ('q_ans',_('Questions that I answered')),
+ ('q_sel',_('Individually selected questions')),
+ )
+ UPDATE_FREQUENCY = (
+ ('w',_('Weekly')),
+ ('d',_('Daily')),
+ ('n',_('No email')),
+ )
+ subscriber = models.ForeignKey(User)
+ feed_type = models.CharField(max_length=16,choices=FEED_TYPES)
+ frequency = models.CharField(max_length=8,choices=UPDATE_FREQUENCY,default='n')
+ added_at = models.DateTimeField(auto_now_add=True)
+ reported_at = models.DateTimeField(null=True)
+
+ def save(self,*args,**kwargs):
+ type = self.feed_type
+ subscriber = self.subscriber
+ similar = self.__class__.objects.filter(feed_type=type,subscriber=subscriber).exclude(pk=self.id)
+ if len(similar) > 0:
+ raise IntegrityError('email feed setting already exists')
+ super(EmailFeedSetting,self).save(*args,**kwargs)
+
+ class Meta:
+ app_label = 'forum'
+
+class AnonymousEmail(models.Model):
+ #validation key, if used
+ key = models.CharField(max_length=32)
+ email = models.EmailField(null=False,unique=True)
+ isvalid = models.BooleanField(default=False)
+
+ class Meta:
+ app_label = 'forum'
+
+
+class AuthKeyUserAssociation(models.Model):
+ key = models.CharField(max_length=256,null=False,unique=True)
+ provider = models.CharField(max_length=64)
+ user = models.ForeignKey(User)
+ added_at = models.DateTimeField(default=datetime.datetime.now)
+
+ class Meta:
+ app_label = 'forum'
+
+
\ No newline at end of file
diff --git a/forum/modules.py b/forum/modules.py
new file mode 100755
index 0000000..9c07233
--- /dev/null
+++ b/forum/modules.py
@@ -0,0 +1,79 @@
+import os
+import types
+import re
+
+from django.template import Template, TemplateDoesNotExist
+
+MODULES_PACKAGE = 'forum_modules'
+
+MODULES_FOLDER = os.path.join(os.path.dirname(__file__), '../' + MODULES_PACKAGE)
+
+MODULE_LIST = [
+ __import__('forum_modules.%s' % f, globals(), locals(), ['forum_modules'])
+ for f in os.listdir(MODULES_FOLDER)
+ if os.path.isdir(os.path.join(MODULES_FOLDER, f)) and
+ os.path.exists(os.path.join(MODULES_FOLDER, "%s/__init__.py" % f)) and
+ not os.path.exists(os.path.join(MODULES_FOLDER, "%s/DISABLED" % f))
+]
+
+def get_modules_script(script_name):
+ all = []
+
+ for m in MODULE_LIST:
+ try:
+ all.append(__import__('%s.%s' % (m.__name__, script_name), globals(), locals(), [m.__name__]))
+ except Exception, e:
+ #print script_name + ":" + str(e)
+ pass
+
+ return all
+
+def get_modules_script_classes(script_name, base_class):
+ scripts = get_modules_script(script_name)
+ all_classes = {}
+
+ for script in scripts:
+ all_classes.update(dict([
+ (n, c) for (n, c) in [(n, getattr(script, n)) for n in dir(script)]
+ if isinstance(c, (type, types.ClassType)) and issubclass(c, base_class)
+ ]))
+
+ return all_classes
+
+def get_all_handlers(name):
+ handler_files = get_modules_script('handlers')
+
+ return [
+ h for h in [
+ getattr(f, name) for f in handler_files
+ if hasattr(f, name)
+ ]
+
+ if callable(h)
+ ]
+
+def get_handler(name, default):
+ all = get_all_handlers(name)
+ print(len(all))
+ return len(all) and all[0] or default
+
+module_template_re = re.compile('^modules\/(\w+)\/(.*)$')
+
+def module_templates_loader(name, dirs=None):
+ result = module_template_re.search(name)
+
+ if result is not None:
+ file_name = os.path.join(MODULES_FOLDER, result.group(1), 'templates', result.group(2))
+
+ if os.path.exists(file_name):
+ try:
+ f = open(file_name, 'r')
+ source = f.read()
+ f.close()
+ return (source, file_name)
+ except:
+ pass
+
+ raise TemplateDoesNotExist, name
+
+module_templates_loader.is_usable = True
\ No newline at end of file
diff --git a/forum/sitemap.py b/forum/sitemap.py
new file mode 100644
index 0000000..c0c60b5
--- /dev/null
+++ b/forum/sitemap.py
@@ -0,0 +1,14 @@
+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
+
+ def location(self, obj):
+ return obj.get_absolute_url()
diff --git a/forum/skins/README b/forum/skins/README
new file mode 100644
index 0000000..5565fa8
--- /dev/null
+++ b/forum/skins/README
@@ -0,0 +1,22 @@
+this directory contains available skins
+
+1) default - default skin with templates
+2) common - this directory is to media directory common to all or many templates
+
+to create a new skin just create another directory under skins/
+and start populating it with the directory structure as in
+default/templates - templates must be named the same way
+
+NO NEED TO CREATE ALL TEMPLATES/MEDIA FILES AT ONCE
+
+templates are resolved in the following way:
+* check in skin named as in settings.OSQA_DEFAULT_SKIN
+* then skin named 'default'
+
+media is resolved with one extra option
+* settings.OSQA_DEFAULT_SKIN
+* 'default'
+* 'common'
+
+media does not have to be composed of files named the same way as in default skin
+whatever media you link to from your templates - will be in operation
diff --git a/forum/skins/__init__.py b/forum/skins/__init__.py
new file mode 100644
index 0000000..be6bd4f
--- /dev/null
+++ b/forum/skins/__init__.py
@@ -0,0 +1,57 @@
+from django.conf import settings
+from django.template import loader
+from django.template.loaders import filesystem
+from django.http import HttpResponse
+import os.path
+import logging
+
+#module for skinning osqa
+#at this point skin can be changed only in settings file
+#via OSQA_DEFAULT_SKIN variable
+
+#note - Django template loaders use method django.utils._os.safe_join
+#to work on unicode file paths
+#here it is ignored because it is assumed that we won't use unicode paths
+
+def load_template_source(name, dirs=None):
+ try:
+ tname = os.path.join(settings.OSQA_DEFAULT_SKIN,'templates',name)
+ return filesystem.load_template_source(tname,dirs)
+ except:
+ tname = os.path.join('default','templates',name)
+ return filesystem.load_template_source(tname,dirs)
+load_template_source.is_usable = True
+
+def find_media_source(url):
+ """returns url prefixed with the skin name
+ of the first skin that contains the file
+ directories are searched in this order:
+ settings.OSQA_DEFAULT_SKIN, then 'default', then 'commmon'
+ if file is not found - returns None
+ and logs an error message
+ """
+ while url[0] == '/': url = url[1:]
+ d = os.path.dirname
+ n = os.path.normpath
+ j = os.path.join
+ f = os.path.isfile
+ skins = n(j(d(d(__file__)),'skins'))
+ try:
+ media = os.path.join(skins, settings.OSQA_DEFAULT_SKIN, url)
+ assert(f(media))
+ use_skin = settings.OSQA_DEFAULT_SKIN
+ except:
+ try:
+ media = j(skins, 'default', url)
+ assert(f(media))
+ use_skin = 'default'
+ except:
+ media = j(skins, 'common', url)
+ try:
+ assert(f(media))
+ use_skin = 'common'
+ except:
+ logging.error('could not find media for %s' % url)
+ use_skin = ''
+ return None
+ return use_skin + '/' + url
diff --git a/forum/skins/common/media/README b/forum/skins/common/media/README
new file mode 100644
index 0000000..3376e75
--- /dev/null
+++ b/forum/skins/common/media/README
@@ -0,0 +1 @@
+directory for media common to all or many templates
diff --git a/forum/skins/default/media/images/blue-up-arrow-h18px.png b/forum/skins/default/media/images/blue-up-arrow-h18px.png
new file mode 100644
index 0000000..e1f29e8
Binary files /dev/null and b/forum/skins/default/media/images/blue-up-arrow-h18px.png differ
diff --git a/forum/skins/default/media/images/box-arrow.gif b/forum/skins/default/media/images/box-arrow.gif
new file mode 100644
index 0000000..89dcf5b
Binary files /dev/null and b/forum/skins/default/media/images/box-arrow.gif differ
diff --git a/forum/skins/default/media/images/bullet_green.gif b/forum/skins/default/media/images/bullet_green.gif
new file mode 100644
index 0000000..fa53091
Binary files /dev/null and b/forum/skins/default/media/images/bullet_green.gif differ
diff --git a/forum/skins/default/media/images/cc-88x31.png b/forum/skins/default/media/images/cc-88x31.png
new file mode 100644
index 0000000..0f2a0f1
Binary files /dev/null and b/forum/skins/default/media/images/cc-88x31.png differ
diff --git a/forum/skins/default/media/images/cc-wiki.png b/forum/skins/default/media/images/cc-wiki.png
new file mode 100644
index 0000000..3e68053
Binary files /dev/null and b/forum/skins/default/media/images/cc-wiki.png differ
diff --git a/forum/skins/default/media/images/close-small-dark.png b/forum/skins/default/media/images/close-small-dark.png
new file mode 100644
index 0000000..280c1fc
Binary files /dev/null and b/forum/skins/default/media/images/close-small-dark.png differ
diff --git a/forum/skins/default/media/images/close-small-hover.png b/forum/skins/default/media/images/close-small-hover.png
new file mode 100644
index 0000000..7899aec
Binary files /dev/null and b/forum/skins/default/media/images/close-small-hover.png differ
diff --git a/forum/skins/default/media/images/close-small.png b/forum/skins/default/media/images/close-small.png
new file mode 100644
index 0000000..5a99d31
Binary files /dev/null and b/forum/skins/default/media/images/close-small.png differ
diff --git a/forum/skins/default/media/images/dash.gif b/forum/skins/default/media/images/dash.gif
new file mode 100644
index 0000000..d1ddc50
Binary files /dev/null and b/forum/skins/default/media/images/dash.gif differ
diff --git a/forum/skins/default/media/images/djangomade124x25_grey.gif b/forum/skins/default/media/images/djangomade124x25_grey.gif
new file mode 100644
index 0000000..d34bb31
Binary files /dev/null and b/forum/skins/default/media/images/djangomade124x25_grey.gif differ
diff --git a/forum/skins/default/media/images/dot-g.gif b/forum/skins/default/media/images/dot-g.gif
new file mode 100644
index 0000000..5d6bb28
Binary files /dev/null and b/forum/skins/default/media/images/dot-g.gif differ
diff --git a/forum/skins/default/media/images/dot-list.gif b/forum/skins/default/media/images/dot-list.gif
new file mode 100644
index 0000000..f6a6b86
Binary files /dev/null and b/forum/skins/default/media/images/dot-list.gif differ
diff --git a/forum/skins/default/media/images/edit.png b/forum/skins/default/media/images/edit.png
new file mode 100644
index 0000000..dcb09be
Binary files /dev/null and b/forum/skins/default/media/images/edit.png differ
diff --git a/forum/skins/default/media/images/expander-arrow-hide.gif b/forum/skins/default/media/images/expander-arrow-hide.gif
new file mode 100644
index 0000000..feb6a61
Binary files /dev/null and b/forum/skins/default/media/images/expander-arrow-hide.gif differ
diff --git a/forum/skins/default/media/images/expander-arrow-show.gif b/forum/skins/default/media/images/expander-arrow-show.gif
new file mode 100644
index 0000000..6825c56
Binary files /dev/null and b/forum/skins/default/media/images/expander-arrow-show.gif differ
diff --git a/forum/skins/default/media/images/favicon.gif b/forum/skins/default/media/images/favicon.gif
new file mode 100644
index 0000000..910c266
Binary files /dev/null and b/forum/skins/default/media/images/favicon.gif differ
diff --git a/forum/skins/default/media/images/feed-icon-small.png b/forum/skins/default/media/images/feed-icon-small.png
new file mode 100644
index 0000000..b3c949d
Binary files /dev/null and b/forum/skins/default/media/images/feed-icon-small.png differ
diff --git a/forum/skins/default/media/images/gray-up-arrow-h18px.png b/forum/skins/default/media/images/gray-up-arrow-h18px.png
new file mode 100644
index 0000000..7876744
Binary files /dev/null and b/forum/skins/default/media/images/gray-up-arrow-h18px.png differ
diff --git a/forum/skins/default/media/images/grippie.png b/forum/skins/default/media/images/grippie.png
new file mode 100644
index 0000000..6524d41
Binary files /dev/null and b/forum/skins/default/media/images/grippie.png differ
diff --git a/forum/skins/default/media/images/indicator.gif b/forum/skins/default/media/images/indicator.gif
new file mode 100644
index 0000000..1c72ebb
Binary files /dev/null and b/forum/skins/default/media/images/indicator.gif differ
diff --git a/forum/skins/default/media/images/logo.gif b/forum/skins/default/media/images/logo.gif
new file mode 100644
index 0000000..ab690de
Binary files /dev/null and b/forum/skins/default/media/images/logo.gif differ
diff --git a/forum/skins/default/media/images/logo.png b/forum/skins/default/media/images/logo.png
new file mode 100644
index 0000000..6a250e3
Binary files /dev/null and b/forum/skins/default/media/images/logo.png differ
diff --git a/forum/skins/default/media/images/logo1.png b/forum/skins/default/media/images/logo1.png
new file mode 100644
index 0000000..d79a627
Binary files /dev/null and b/forum/skins/default/media/images/logo1.png differ
diff --git a/forum/skins/default/media/images/logo2.png b/forum/skins/default/media/images/logo2.png
new file mode 100644
index 0000000..bd3cccd
Binary files /dev/null and b/forum/skins/default/media/images/logo2.png differ
diff --git a/forum/skins/default/media/images/medala.gif b/forum/skins/default/media/images/medala.gif
new file mode 100644
index 0000000..93dd1a3
Binary files /dev/null and b/forum/skins/default/media/images/medala.gif differ
diff --git a/forum/skins/default/media/images/medala_on.gif b/forum/skins/default/media/images/medala_on.gif
new file mode 100644
index 0000000..a18f9e8
Binary files /dev/null and b/forum/skins/default/media/images/medala_on.gif differ
diff --git a/forum/skins/default/media/images/new.gif b/forum/skins/default/media/images/new.gif
new file mode 100644
index 0000000..8a220b5
Binary files /dev/null and b/forum/skins/default/media/images/new.gif differ
diff --git a/forum/skins/default/media/images/nophoto.png b/forum/skins/default/media/images/nophoto.png
new file mode 100644
index 0000000..2daf0ff
Binary files /dev/null and b/forum/skins/default/media/images/nophoto.png differ
diff --git a/forum/skins/default/media/images/openid.gif b/forum/skins/default/media/images/openid.gif
new file mode 100644
index 0000000..8540e12
Binary files /dev/null and b/forum/skins/default/media/images/openid.gif differ
diff --git a/forum/skins/default/media/images/openid/aol.gif b/forum/skins/default/media/images/openid/aol.gif
new file mode 100644
index 0000000..decc4f1
Binary files /dev/null and b/forum/skins/default/media/images/openid/aol.gif differ
diff --git a/forum/skins/default/media/images/openid/blogger.ico b/forum/skins/default/media/images/openid/blogger.ico
new file mode 100644
index 0000000..1b9730b
Binary files /dev/null and b/forum/skins/default/media/images/openid/blogger.ico differ
diff --git a/forum/skins/default/media/images/openid/claimid.ico b/forum/skins/default/media/images/openid/claimid.ico
new file mode 100644
index 0000000..2b80f49
Binary files /dev/null and b/forum/skins/default/media/images/openid/claimid.ico differ
diff --git a/forum/skins/default/media/images/openid/facebook.gif b/forum/skins/default/media/images/openid/facebook.gif
new file mode 100644
index 0000000..b997b35
Binary files /dev/null and b/forum/skins/default/media/images/openid/facebook.gif differ
diff --git a/forum/skins/default/media/images/openid/flickr.ico b/forum/skins/default/media/images/openid/flickr.ico
new file mode 100644
index 0000000..11f6e07
Binary files /dev/null and b/forum/skins/default/media/images/openid/flickr.ico differ
diff --git a/forum/skins/default/media/images/openid/google.gif b/forum/skins/default/media/images/openid/google.gif
new file mode 100644
index 0000000..1b6cd07
Binary files /dev/null and b/forum/skins/default/media/images/openid/google.gif differ
diff --git a/forum/skins/default/media/images/openid/livejournal.ico b/forum/skins/default/media/images/openid/livejournal.ico
new file mode 100644
index 0000000..f3d21ec
Binary files /dev/null and b/forum/skins/default/media/images/openid/livejournal.ico differ
diff --git a/forum/skins/default/media/images/openid/myopenid.ico b/forum/skins/default/media/images/openid/myopenid.ico
new file mode 100644
index 0000000..ceb06e6
Binary files /dev/null and b/forum/skins/default/media/images/openid/myopenid.ico differ
diff --git a/forum/skins/default/media/images/openid/openid-inputicon.gif b/forum/skins/default/media/images/openid/openid-inputicon.gif
new file mode 100644
index 0000000..cde836c
Binary files /dev/null and b/forum/skins/default/media/images/openid/openid-inputicon.gif differ
diff --git a/forum/skins/default/media/images/openid/openid.gif b/forum/skins/default/media/images/openid/openid.gif
new file mode 100644
index 0000000..c718b0e
Binary files /dev/null and b/forum/skins/default/media/images/openid/openid.gif differ
diff --git a/forum/skins/default/media/images/openid/technorati.ico b/forum/skins/default/media/images/openid/technorati.ico
new file mode 100644
index 0000000..fa1083c
Binary files /dev/null and b/forum/skins/default/media/images/openid/technorati.ico differ
diff --git a/forum/skins/default/media/images/openid/twitter.png b/forum/skins/default/media/images/openid/twitter.png
new file mode 100755
index 0000000..9a6552d
Binary files /dev/null and b/forum/skins/default/media/images/openid/twitter.png differ
diff --git a/forum/skins/default/media/images/openid/verisign.ico b/forum/skins/default/media/images/openid/verisign.ico
new file mode 100644
index 0000000..3953af9
Binary files /dev/null and b/forum/skins/default/media/images/openid/verisign.ico differ
diff --git a/forum/skins/default/media/images/openid/vidoop.ico b/forum/skins/default/media/images/openid/vidoop.ico
new file mode 100644
index 0000000..bbd9a0d
Binary files /dev/null and b/forum/skins/default/media/images/openid/vidoop.ico differ
diff --git a/forum/skins/default/media/images/openid/wordpress.ico b/forum/skins/default/media/images/openid/wordpress.ico
new file mode 100644
index 0000000..31b7d2c
Binary files /dev/null and b/forum/skins/default/media/images/openid/wordpress.ico differ
diff --git a/forum/skins/default/media/images/openid/yahoo.gif b/forum/skins/default/media/images/openid/yahoo.gif
new file mode 100644
index 0000000..0f0eb8e
Binary files /dev/null and b/forum/skins/default/media/images/openid/yahoo.gif differ
diff --git a/forum/skins/default/media/images/quest-bg.gif b/forum/skins/default/media/images/quest-bg.gif
new file mode 100644
index 0000000..b754023
Binary files /dev/null and b/forum/skins/default/media/images/quest-bg.gif differ
diff --git a/forum/skins/default/media/images/vote-accepted-on.png b/forum/skins/default/media/images/vote-accepted-on.png
new file mode 100644
index 0000000..2026f3b
Binary files /dev/null and b/forum/skins/default/media/images/vote-accepted-on.png differ
diff --git a/forum/skins/default/media/images/vote-accepted.png b/forum/skins/default/media/images/vote-accepted.png
new file mode 100644
index 0000000..ecd1855
Binary files /dev/null and b/forum/skins/default/media/images/vote-accepted.png differ
diff --git a/forum/skins/default/media/images/vote-arrow-down-on.png b/forum/skins/default/media/images/vote-arrow-down-on.png
new file mode 100644
index 0000000..048dbb4
Binary files /dev/null and b/forum/skins/default/media/images/vote-arrow-down-on.png differ
diff --git a/forum/skins/default/media/images/vote-arrow-down.png b/forum/skins/default/media/images/vote-arrow-down.png
new file mode 100644
index 0000000..e4fdec0
Binary files /dev/null and b/forum/skins/default/media/images/vote-arrow-down.png differ
diff --git a/forum/skins/default/media/images/vote-arrow-up-on.png b/forum/skins/default/media/images/vote-arrow-up-on.png
new file mode 100644
index 0000000..56ad0c2
Binary files /dev/null and b/forum/skins/default/media/images/vote-arrow-up-on.png differ
diff --git a/forum/skins/default/media/images/vote-arrow-up.png b/forum/skins/default/media/images/vote-arrow-up.png
new file mode 100644
index 0000000..6e9a51c
Binary files /dev/null and b/forum/skins/default/media/images/vote-arrow-up.png differ
diff --git a/forum/skins/default/media/images/vote-favorite-off.png b/forum/skins/default/media/images/vote-favorite-off.png
new file mode 100644
index 0000000..c1bef07
Binary files /dev/null and b/forum/skins/default/media/images/vote-favorite-off.png differ
diff --git a/forum/skins/default/media/images/vote-favorite-on.png b/forum/skins/default/media/images/vote-favorite-on.png
new file mode 100644
index 0000000..1f9c14a
Binary files /dev/null and b/forum/skins/default/media/images/vote-favorite-on.png differ
diff --git a/forum/skins/default/media/jquery-openid/images/aol.gif b/forum/skins/default/media/jquery-openid/images/aol.gif
new file mode 100644
index 0000000..decc4f1
Binary files /dev/null and b/forum/skins/default/media/jquery-openid/images/aol.gif differ
diff --git a/forum/skins/default/media/jquery-openid/images/blogger-1.png b/forum/skins/default/media/jquery-openid/images/blogger-1.png
new file mode 100644
index 0000000..8b360ea
Binary files /dev/null and b/forum/skins/default/media/jquery-openid/images/blogger-1.png differ
diff --git a/forum/skins/default/media/jquery-openid/images/blogger.ico b/forum/skins/default/media/jquery-openid/images/blogger.ico
new file mode 100644
index 0000000..1b9730b
Binary files /dev/null and b/forum/skins/default/media/jquery-openid/images/blogger.ico differ
diff --git a/forum/skins/default/media/jquery-openid/images/claimid-0.png b/forum/skins/default/media/jquery-openid/images/claimid-0.png
new file mode 100644
index 0000000..4a0ea1b
Binary files /dev/null and b/forum/skins/default/media/jquery-openid/images/claimid-0.png differ
diff --git a/forum/skins/default/media/jquery-openid/images/claimid.ico b/forum/skins/default/media/jquery-openid/images/claimid.ico
new file mode 100644
index 0000000..2b80f49
Binary files /dev/null and b/forum/skins/default/media/jquery-openid/images/claimid.ico differ
diff --git a/forum/skins/default/media/jquery-openid/images/facebook.gif b/forum/skins/default/media/jquery-openid/images/facebook.gif
new file mode 100644
index 0000000..b997b35
Binary files /dev/null and b/forum/skins/default/media/jquery-openid/images/facebook.gif differ
diff --git a/forum/skins/default/media/jquery-openid/images/flickr.ico b/forum/skins/default/media/jquery-openid/images/flickr.ico
new file mode 100644
index 0000000..11f6e07
Binary files /dev/null and b/forum/skins/default/media/jquery-openid/images/flickr.ico differ
diff --git a/forum/skins/default/media/jquery-openid/images/flickr.png b/forum/skins/default/media/jquery-openid/images/flickr.png
new file mode 100644
index 0000000..142405a
Binary files /dev/null and b/forum/skins/default/media/jquery-openid/images/flickr.png differ
diff --git a/forum/skins/default/media/jquery-openid/images/google.gif b/forum/skins/default/media/jquery-openid/images/google.gif
new file mode 100644
index 0000000..1b6cd07
Binary files /dev/null and b/forum/skins/default/media/jquery-openid/images/google.gif differ
diff --git a/forum/skins/default/media/jquery-openid/images/livejournal-1.png b/forum/skins/default/media/jquery-openid/images/livejournal-1.png
new file mode 100644
index 0000000..e643608
Binary files /dev/null and b/forum/skins/default/media/jquery-openid/images/livejournal-1.png differ
diff --git a/forum/skins/default/media/jquery-openid/images/livejournal.ico b/forum/skins/default/media/jquery-openid/images/livejournal.ico
new file mode 100644
index 0000000..f3d21ec
Binary files /dev/null and b/forum/skins/default/media/jquery-openid/images/livejournal.ico differ
diff --git a/forum/skins/default/media/jquery-openid/images/myopenid-2.png b/forum/skins/default/media/jquery-openid/images/myopenid-2.png
new file mode 100644
index 0000000..f64fb8e
Binary files /dev/null and b/forum/skins/default/media/jquery-openid/images/myopenid-2.png differ
diff --git a/forum/skins/default/media/jquery-openid/images/myopenid.ico b/forum/skins/default/media/jquery-openid/images/myopenid.ico
new file mode 100644
index 0000000..ceb06e6
Binary files /dev/null and b/forum/skins/default/media/jquery-openid/images/myopenid.ico differ
diff --git a/forum/skins/default/media/jquery-openid/images/openid-inputicon.gif b/forum/skins/default/media/jquery-openid/images/openid-inputicon.gif
new file mode 100644
index 0000000..cde836c
Binary files /dev/null and b/forum/skins/default/media/jquery-openid/images/openid-inputicon.gif differ
diff --git a/forum/skins/default/media/jquery-openid/images/openid.gif b/forum/skins/default/media/jquery-openid/images/openid.gif
new file mode 100644
index 0000000..c718b0e
Binary files /dev/null and b/forum/skins/default/media/jquery-openid/images/openid.gif differ
diff --git a/forum/skins/default/media/jquery-openid/images/openidico.png b/forum/skins/default/media/jquery-openid/images/openidico.png
new file mode 100644
index 0000000..ab62266
Binary files /dev/null and b/forum/skins/default/media/jquery-openid/images/openidico.png differ
diff --git a/forum/skins/default/media/jquery-openid/images/openidico16.png b/forum/skins/default/media/jquery-openid/images/openidico16.png
new file mode 100644
index 0000000..ad718ac
Binary files /dev/null and b/forum/skins/default/media/jquery-openid/images/openidico16.png differ
diff --git a/forum/skins/default/media/jquery-openid/images/technorati-1.png b/forum/skins/default/media/jquery-openid/images/technorati-1.png
new file mode 100644
index 0000000..f719524
Binary files /dev/null and b/forum/skins/default/media/jquery-openid/images/technorati-1.png differ
diff --git a/forum/skins/default/media/jquery-openid/images/technorati.ico b/forum/skins/default/media/jquery-openid/images/technorati.ico
new file mode 100644
index 0000000..fa1083c
Binary files /dev/null and b/forum/skins/default/media/jquery-openid/images/technorati.ico differ
diff --git a/forum/skins/default/media/jquery-openid/images/verisign-2.png b/forum/skins/default/media/jquery-openid/images/verisign-2.png
new file mode 100644
index 0000000..c146700
Binary files /dev/null and b/forum/skins/default/media/jquery-openid/images/verisign-2.png differ
diff --git a/forum/skins/default/media/jquery-openid/images/verisign.ico b/forum/skins/default/media/jquery-openid/images/verisign.ico
new file mode 100644
index 0000000..3953af9
Binary files /dev/null and b/forum/skins/default/media/jquery-openid/images/verisign.ico differ
diff --git a/forum/skins/default/media/jquery-openid/images/vidoop.ico b/forum/skins/default/media/jquery-openid/images/vidoop.ico
new file mode 100644
index 0000000..bbd9a0d
Binary files /dev/null and b/forum/skins/default/media/jquery-openid/images/vidoop.ico differ
diff --git a/forum/skins/default/media/jquery-openid/images/vidoop.png b/forum/skins/default/media/jquery-openid/images/vidoop.png
new file mode 100644
index 0000000..032c9e9
Binary files /dev/null and b/forum/skins/default/media/jquery-openid/images/vidoop.png differ
diff --git a/forum/skins/default/media/jquery-openid/images/wordpress.ico b/forum/skins/default/media/jquery-openid/images/wordpress.ico
new file mode 100644
index 0000000..31b7d2c
Binary files /dev/null and b/forum/skins/default/media/jquery-openid/images/wordpress.ico differ
diff --git a/forum/skins/default/media/jquery-openid/images/wordpress.png b/forum/skins/default/media/jquery-openid/images/wordpress.png
new file mode 100644
index 0000000..ee29f0c
Binary files /dev/null and b/forum/skins/default/media/jquery-openid/images/wordpress.png differ
diff --git a/forum/skins/default/media/jquery-openid/images/yahoo.gif b/forum/skins/default/media/jquery-openid/images/yahoo.gif
new file mode 100644
index 0000000..42adbfa
Binary files /dev/null and b/forum/skins/default/media/jquery-openid/images/yahoo.gif differ
diff --git a/forum/skins/default/media/jquery-openid/jquery.openid.js b/forum/skins/default/media/jquery-openid/jquery.openid.js
new file mode 100644
index 0000000..8d1cd20
--- /dev/null
+++ b/forum/skins/default/media/jquery-openid/jquery.openid.js
@@ -0,0 +1,111 @@
+//jQuery OpenID Plugin 1.1 Copyright 2009 Jarrett Vance http://jvance.com/pages/jQueryOpenIdPlugin.xhtml
+$.fn.openid = function() {
+ var $this = $(this);
+
+ //name input value - needed for name based OpenID
+ var $usr = $this.find('input[name=openid_username]');
+
+ //final url input value
+ var $id = $this.find('input[name=openid_url]');
+
+ //beginning and end of name OpenID url (name being the middle)
+ var $front = $this.find('p:has(input[name=openid_username])>span:eq(0)');
+ var $end = $this.find('p:has(input[name=openid_username])>span:eq(1)');
+
+ //needed for special effects only
+ var $localfs = $this.find('fieldset:has(input[name=username])');
+ var $usrfs = $this.find('fieldset:has(input[name=openid_username])');
+ var $idfs = $this.find('fieldset:has(input[name=openid_url])');
+
+ var submitusr = function() {
+ if ($usr.val().length < 1) {
+ $usr.focus();
+ return false;
+ }
+ $id.val($front.text() + $usr.val() + $end.text());
+ return true;
+ };
+
+ var submitid = function() {
+ if ($id.val().length < 1) {
+ $id.focus();
+ return false;
+ }
+ return true;
+
+ };
+ var local = function() {
+ var $li = $(this);
+ $('#openid_form .providers li').removeClass('highlight');
+ $li.addClass('highlight');
+ $usrfs.hide();
+ $idfs.hide();
+ $localfs.show();
+ $this.unbind('submit').submit(submitid);
+ return false;
+ };
+
+ var direct = function() {
+ var $li = $(this);
+ $('#openid_form .providers li').removeClass('highlight');
+ $li.addClass('highlight');
+ $usrfs.fadeOut('slow');
+ $localfs.fadeOut('slow');
+ $idfs.fadeOut('slow');
+ $id.val($this.find("li.highlight span").text());
+ setTimeout(function(){$('#bsignin').click();},1000);
+ return false;
+ };
+
+ var openid = function() {
+ var $li = $(this);
+ $('#openid_form .providers li').removeClass('highlight');
+ $li.addClass('highlight');
+ $usrfs.hide();
+ $localfs.hide();
+ $idfs.show();
+ $id.focus();
+ $this.unbind('submit').submit(submitid);
+ return false;
+ };
+
+ var username = function() {
+ var $li = $(this);
+ $('#openid_form .providers li').removeClass('highlight');
+ $li.addClass('highlight');
+ $idfs.hide();
+ $localfs.hide();
+ $usrfs.show();
+ $this.find('#enter_your_what').text($li.attr("title"));
+ $front.text($li.find("span").text().split("username")[0]);
+ $end.text("").text($li.find("span").text().split("username")[1]);
+ $id.focus();
+ $this.unbind('submit').submit(submitusr);
+ return false;
+ };
+
+ $this.find('li.local').click(local);
+ $this.find('li.direct').click(direct);
+ $this.find('li.openid').click(openid);
+ $this.find('li.username').click(username);
+ $id.keypress(function(e) {
+ if ((e.which && e.which == 13) || (e.keyCode && e.keyCode == 13)) {
+ return submitid();
+ }
+ });
+ $usr.keypress(function(e) {
+ if ((e.which && e.which == 13) || (e.keyCode && e.keyCode == 13)) {
+ return submitusr();
+ }
+ });
+ $this.find('li span').hide();
+ $this.find('li').css('line-height', 0).css('cursor', 'pointer');
+ $usrfs.hide();
+ $idfs.hide();
+ $localfs.hide();
+ $this.find('li:eq(0)').click();
+
+ return this;
+};
+// submitting next=%2F&openid_username=&openid_url=http%3A%2F%2Fyahoo.com%2F
+// submitting next=%2F&openid_username=&openid_url=http%3A%2F%2Fyahoo.com%2F
diff --git a/forum/skins/default/media/jquery-openid/openid.css b/forum/skins/default/media/jquery-openid/openid.css
new file mode 100644
index 0000000..1b7aaf8
--- /dev/null
+++ b/forum/skins/default/media/jquery-openid/openid.css
@@ -0,0 +1,75 @@
+fieldset { border-style:none; }
+img {border-style:none;}
+
+.logo_box {display:inline-block;float:left;width:90px;height:40px;background:white;border:1px solid #dddddd;}
+.openid_box img {margin-top:6px;}
+.aol_box img {margin-top:6px;}
+.yahoo_box img {margin-top:13px;}
+.google_box img {margin-top:6px;}
+.local_login_box img {margin-top:2px;margin-left:-3px;}
+
+form.openid ul{ margin:0;padding:0;text-align:center; list-style-type:none; display:block;}
+form.openid ul li {float:left; padding:4px;display:inline-block;}
+form.openid ul li div {display:inline-block;}
+form.openid ul li span {padding:0 1em 0 3px}
+form.openid ul li.first_tiny_li {clear:left;}
+form.openid fieldset {clear:both;padding:10px 0px 0px 0px;}
+form.openid div+fieldset {display:none}
+form.openid label {display:block; font-weight:bold;}
+input[name=openid_username] {width:8em}
+input[name=openid_identifier] {width:18em}
+form.openid ul li.highlight { -moz-border-radius:4px; -webkit-border-radius:4px; background-color: #FD6}
+form.openid fieldset div {
+ -moz-border-radius:4px;
+ -webkit-border-radius:4px;
+ background: #DCDCDC;
+ padding:10px;
+ display:inline-block;
+ float:left;
+}
+form.openid p {margin-bottom:4px;}
+form.openid fieldset div p {padding:0px;margin:0px;}
+form.openid fieldset div p.login {padding:0px;margin:0 0 10px 0;}
+form.openid label {
+ display:inline-block;
+ font-weight:normal;
+ width:6em;
+ text-align:right;
+}
+#local_login_fs div {
+ padding-bottom:4px;
+}
+#local_login_buttons {
+ text-align:center;
+ line-height:1.8em;
+ margin-top:3px;
+}
+/*form.openid input[type='submit'] {margin-left:1em;}*/
+#openid_username {background:#ffffa0;}
+#openid_url {background:#ffffa0;}
+
+.openid_logo{color:#F7931E;padding:6px 0px 8px 28px;
+background: url(images/openidico.png) no-repeat;
+}
+
+#openid_login {float:left; width:30%; margin:2em 1em; text-align:center}
+#openid_login div{margin-top:0.5em}
+
+form.openid ul.errorlist {
+ border: none;
+ list-style-position:inside;
+ list-style-type: disc;
+ margin-bottom:5px;
+}
+form.openid ul.errorlist li {
+ text-align: left;
+ margin: 5px;
+ float: none;
+ color:blue;
+}
+#openid_small_providers li {
+ margin-top:4px;
+}
+#openid_small_providers li.facebook {
+ margin-top:0px;
+}
diff --git a/forum/skins/default/media/js/com.cnprog.admin.js b/forum/skins/default/media/js/com.cnprog.admin.js
new file mode 100644
index 0000000..39dff48
--- /dev/null
+++ b/forum/skins/default/media/js/com.cnprog.admin.js
@@ -0,0 +1,13 @@
+$(document).ready( function(){
+ var options = {
+ success: function(a,b){$('.admin #action_status').html($.i18n._('changes saved'));},
+ dataType:'json',
+ timeout:5000,
+ url: scriptUrl + $.i18n._('moderate-user/') + viewUserID + '/'
+ };
+ var form = $('.admin #moderate_user_form').ajaxForm(options);
+ var box = $('.admin input#id_is_approved').click(function(){
+ $('.admin #action_status').html($.i18n._('sending data...'));
+ form.ajaxSubmit(options);
+ });
+});
diff --git a/forum/skins/default/media/js/com.cnprog.editor.js b/forum/skins/default/media/js/com.cnprog.editor.js
new file mode 100644
index 0000000..18cc516
--- /dev/null
+++ b/forum/skins/default/media/js/com.cnprog.editor.js
@@ -0,0 +1,68 @@
+/*
+ jQuery TextAreaResizer plugin
+ Created on 17th January 2008 by Ryan O'Dell
+ Version 1.0.4
+*/(function($){var textarea,staticOffset;var iLastMousePos=0;var iMin=32;var grip;$.fn.TextAreaResizer=function(){return this.each(function(){textarea=$(this).addClass('processed'),staticOffset=null;$(this).wrap('
"',
+ 'upload image':'æè ä¸ä¼ æ¬å°å¾çï¼'
+};
+
+var i18nEn = {
+ 'need >15 points to report spam':'need >15 points to report spam ',
+ '>15 points requried to upvote':'>15 points required to upvote ',
+ 'tags cannot be empty':'please enter at least one tag',
+ 'anonymous users cannot vote':'sorry, anonymous users cannot vote ',
+ 'anonymous users cannot select favorite questions':'sorry, anonymous users cannot select favorite questions ',
+ 'to comment, need': '(to comment other people\'s posts, karma ',
+ 'please see':'please see ',
+ 'community karma points':' or more is necessary) - ',
+ 'upload image':'Upload image:',
+ 'enter image url':'enter URL of the image, e.g. http://www.example.com/image.jpg \"image title\"',
+ 'enter url':'enter Web address, e.g. http://www.example.com \"page title\"',
+ 'daily vote cap exhausted':'sorry, you\'ve used up todays vote cap',
+ 'cannot pick own answer as best':'sorry, you cannot accept your own answer',
+ 'cannot revoke old vote':'sorry, older votes cannot be revoked',
+ 'please confirm offensive':'are you sure this post is offensive, contains spam, advertising, malicious remarks, etc.?',
+ 'flag offensive cap exhausted':'sorry, you\'ve used up todays cap of flagging offensive messages ',
+ 'confirm delete':'are you sure you want to delete this?',
+ 'anonymous users cannot delete/undelete':'sorry, anonymous users cannot delete or undelete posts',
+ 'post recovered':'your post is now restored!',
+ 'post deleted':'your post has been deleted',
+ 'confirm delete comment':'do you really want to delete this comment?',
+ 'can write':'have ',
+ 'tablimits info':'up to 5 tags, no more than 20 characters each',
+ 'content minchars': 'please enter more than {0} characters',
+ 'title minchars':"please enter at least {0} characters",
+ 'characters':'characters left',
+ 'cannot vote for own posts':'sorry, you cannot vote for your own posts',
+ 'cannot flag message as offensive twice':'cannot flag message as offensive twice ',
+ '>100 points required to downvote':'>100 points required to downvote '
+};
+
+var i18nEs = {
+ 'insufficient privilege':'privilegio insuficiente',
+ 'cannot pick own answer as best':'no puede escoger su propia respuesta como la mejor',
+ 'anonymous users cannot select favorite questions':'usuarios anonimos no pueden seleccionar',
+ 'please login':'por favor inicie sesión',
+ 'anonymous users cannot vote':'usuarios anónimos no pueden votar',
+ '>15 points requried to upvote': '>15 puntos requeridos para votar positivamente',
+ '>100 points required to downvote':'>100 puntos requeridos para votar negativamente',
+ 'please see': 'por favor vea',
+ 'cannot vote for own posts':'no se puede votar por sus propias publicaciones',
+ 'daily vote cap exhausted':'cuota de votos diarios excedida',
+ 'cannot revoke old vote':'no puede revocar un voto viejo',
+ 'please confirm offensive':"por favor confirme ofensiva",
+ 'anonymous users cannot flag offensive posts':'usuarios anónimos no pueden marcar publicaciones como ofensivas',
+ 'cannot flag message as offensive twice':'no puede marcar mensaje como ofensivo dos veces',
+ 'flag offensive cap exhausted':'cuota para marcar ofensivas ha sido excedida',
+ 'need >15 points to report spam':"necesita >15 puntos para reportar spam",
+ 'confirm delete':"¿Está seguro que desea borrar esto?",
+ 'anonymous users cannot delete/undelete':"usuarios anónimos no pueden borrar o recuperar publicaciones",
+ 'post recovered':"publicación recuperada",
+ 'post deleted':"publicación borradaã",
+ 'add comment':'agregar comentario',
+ 'community karma points':'reputación comunitaria',
+ 'to comment, need':'para comentar, necesita reputación',
+ 'delete this comment':'borrar este comentario',
+ 'hide comments':"ocultar comentarios",
+ 'add a comment':"agregar comentarios",
+ 'comments':"comentarios",
+ 'confirm delete comment':"¿Realmente desea borrar este comentario?",
+ 'characters':'caracteres faltantes',
+ 'can write':'tiene ',
+ 'click to close':'haga click para cerrar',
+ 'loading...':'cargando...',
+ 'tags cannot be empty':'las etiquetas no pueden estar vacÃas',
+ 'tablimits info':"hasta 5 etiquetas de no mas de 20 caracteres cada una",
+ 'content cannot be empty':'el contenido no puede estar vacÃo',
+ 'content minchars': 'por favor introduzca mas de {0} caracteres',
+ 'please enter title':'por favor ingrese un tÃtulo',
+ 'title minchars':"por favor introduzca al menos {0} caracteres",
+ 'delete':'borrar',
+ 'undelete': 'recuperar',
+ 'bold': 'negrita',
+ 'italic':'cursiva',
+ 'link':'enlace',
+ 'quote':'citar',
+ 'preformatted text':'texto preformateado',
+ 'image':'imagen',
+ 'numbered list':'lista numerada',
+ 'bulleted list':'lista no numerada',
+ 'heading':'æ é¢',
+ 'horizontal bar':'barra horizontal',
+ 'undo':'deshacer',
+ 'redo':'rehacer',
+ 'enter image url':'introduzca la URL de la imagen, por ejemploï¼ http://www.example.com/image.jpg \"titulo de imagen\"',
+ 'enter url':'introduzca direcciones web, ejemploï¼ http://www.cnprog.com/ \"titulo del enlace\""',
+ 'upload image':'cargar imagenï¼',
+ 'questions/' : 'preguntas/',
+ 'vote/' : 'votar/'
+};
+
+var i18n = {
+ 'en':i18nEn,
+ 'zh_CN':i18nZh,
+ 'es':i18nEs
+};
+
+var i18n_dict = i18n[i18nLang];
diff --git a/forum/skins/default/media/js/com.cnprog.post.js b/forum/skins/default/media/js/com.cnprog.post.js
new file mode 100644
index 0000000..4325e66
--- /dev/null
+++ b/forum/skins/default/media/js/com.cnprog.post.js
@@ -0,0 +1,691 @@
+/*
+Scripts for cnprog.com
+Project Name: Lanai
+All Rights Resevred 2008. CNPROG.COM
+*/
+var lanai =
+{
+ /**
+ * Finds any
");});
+
+ // atx-style headers:
+ // # Header 1
+ // ## Header 2
+ // ## Header 2 with closing hashes ##
+ // ...
+ // ###### Header 6
+ //
+
+ /*
+ text = text.replace(/
+ ^(\#{1,6}) // $1 = string of #'s
+ [ \t]*
+ (.+?) // $2 = Header text
+ [ \t]*
+ \#* // optional closing #'s (not counted)
+ \n+
+ /gm, function() {...});
+ */
+
+ text = text.replace(/^(\#{1,6})[ \t]*(.+?)[ \t]*\#*\n+/gm,
+ function(wholeMatch,m1,m2) {
+ var h_level = m1.length;
+ return hashBlock("" + _RunSpanGamut(m2) + "");
+ });
+
+ return text;
+}
+
+// This declaration keeps Dojo compressor from outputting garbage:
+var _ProcessListItems;
+
+var _DoLists = function(text) {
+//
+// Form HTML ordered (numbered) and unordered (bulleted) lists.
+//
+
+ // attacklab: add sentinel to hack around khtml/safari bug:
+ // http://bugs.webkit.org/show_bug.cgi?id=11231
+ text += "~0";
+
+ // Re-usable pattern to match any entirel ul or ol list:
+
+ /*
+ var whole_list = /
+ ( // $1 = whole list
+ ( // $2
+ [ ]{0,3} // attacklab: g_tab_width - 1
+ ([*+-]|\d+[.]) // $3 = first list item marker
+ [ \t]+
+ )
+ [^\r]+?
+ ( // $4
+ ~0 // sentinel for workaround; should be $
+ |
+ \n{2,}
+ (?=\S)
+ (?! // Negative lookahead for another list item marker
+ [ \t]*
+ (?:[*+-]|\d+[.])[ \t]+
+ )
+ )
+ )/g
+ */
+ var whole_list = /^(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/gm;
+
+ if (g_list_level) {
+ text = text.replace(whole_list,function(wholeMatch,m1,m2) {
+ var list = m1;
+ var list_type = (m2.search(/[*+-]/g)>-1) ? "ul" : "ol";
+
+ // Turn double returns into triple returns, so that we can make a
+ // paragraph for the last item in a list, if necessary:
+ list = list.replace(/\n{2,}/g,"\n\n\n");;
+ var result = _ProcessListItems(list);
+
+ // Trim any trailing whitespace, to put the closing `$list_type>`
+ // up on the preceding line, to get it past the current stupid
+ // HTML block parser. This is a hack to work around the terrible
+ // hack that is the HTML block parser.
+ result = result.replace(/\s+$/,"");
+ result = "<"+list_type+">" + result + ""+list_type+">\n";
+ return result;
+ });
+ } else {
+ whole_list = /(\n\n|^\n?)(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/g;
+ text = text.replace(whole_list,function(wholeMatch,m1,m2,m3) {
+ var runup = m1;
+ var list = m2;
+
+ var list_type = (m3.search(/[*+-]/g)>-1) ? "ul" : "ol";
+ // Turn double returns into triple returns, so that we can make a
+ // paragraph for the last item in a list, if necessary:
+ var list = list.replace(/\n{2,}/g,"\n\n\n");;
+ var result = _ProcessListItems(list);
+ result = runup + "<"+list_type+">\n" + result + ""+list_type+">\n";
+ return result;
+ });
+ }
+
+ // attacklab: strip sentinel
+ text = text.replace(/~0/,"");
+
+ return text;
+}
+
+_ProcessListItems = function(list_str) {
+//
+// Process the contents of a single ordered or unordered list, splitting it
+// into individual list items.
+//
+ // The $g_list_level global keeps track of when we're inside a list.
+ // Each time we enter a list, we increment it; when we leave a list,
+ // we decrement. If it's zero, we're not in a list anymore.
+ //
+ // We do this because when we're not inside a list, we want to treat
+ // something like this:
+ //
+ // I recommend upgrading to version
+ // 8. Oops, now this line is treated
+ // as a sub-list.
+ //
+ // As a single paragraph, despite the fact that the second line starts
+ // with a digit-period-space sequence.
+ //
+ // Whereas when we're inside a list (or sub-list), that line will be
+ // treated as the start of a sub-list. What a kludge, huh? This is
+ // an aspect of Markdown's syntax that's hard to parse perfectly
+ // without resorting to mind-reading. Perhaps the solution is to
+ // change the syntax rules such that sub-lists must start with a
+ // starting cardinal number; e.g. "1." or "a.".
+
+ g_list_level++;
+
+ // trim trailing blank lines:
+ list_str = list_str.replace(/\n{2,}$/,"\n");
+
+ // attacklab: add sentinel to emulate \z
+ list_str += "~0";
+
+ /*
+ list_str = list_str.replace(/
+ (\n)? // leading line = $1
+ (^[ \t]*) // leading whitespace = $2
+ ([*+-]|\d+[.]) [ \t]+ // list marker = $3
+ ([^\r]+? // list item text = $4
+ (\n{1,2}))
+ (?= \n* (~0 | \2 ([*+-]|\d+[.]) [ \t]+))
+ /gm, function(){...});
+ */
+ list_str = list_str.replace(/(\n)?(^[ \t]*)([*+-]|\d+[.])[ \t]+([^\r]+?(\n{1,2}))(?=\n*(~0|\2([*+-]|\d+[.])[ \t]+))/gm,
+ function(wholeMatch,m1,m2,m3,m4){
+ var item = m4;
+ var leading_line = m1;
+ var leading_space = m2;
+
+ if (leading_line || (item.search(/\n{2,}/)>-1)) {
+ item = _RunBlockGamut(_Outdent(item));
+ }
+ else {
+ // Recursion for sub-lists:
+ item = _DoLists(_Outdent(item));
+ item = item.replace(/\n$/,""); // chomp(item)
+ item = _RunSpanGamut(item);
+ }
+
+ return "
` blocks.
+//
+
+ /*
+ text = text.replace(text,
+ /(?:\n\n|^)
+ ( // $1 = the code block -- one or more lines, starting with a space/tab
+ (?:
+ (?:[ ]{4}|\t) // Lines must start with a tab or a tab-width of spaces - attacklab: g_tab_width
+ .*\n+
+ )+
+ )
+ (\n*[ ]{0,3}[^ \t\n]|(?=~0)) // attacklab: g_tab_width
+ /g,function(){...});
+ */
+
+ // attacklab: sentinel workarounds for lack of \A and \Z, safari\khtml bug
+ text += "~0";
+
+ text = text.replace(/(?:\n\n|^)((?:(?:[ ]{4}|\t).*\n+)+)(\n*[ ]{0,3}[^ \t\n]|(?=~0))/g,
+ function(wholeMatch,m1,m2) {
+ var codeblock = m1;
+ var nextChar = m2;
+
+ codeblock = _EncodeCode( _Outdent(codeblock));
+ codeblock = _Detab(codeblock);
+ codeblock = codeblock.replace(/^\n+/g,""); // trim leading newlines
+ codeblock = codeblock.replace(/\n+$/g,""); // trim trailing whitespace
+
+ codeblock = "
" + codeblock + "\n
";
+
+ return hashBlock(codeblock) + nextChar;
+ }
+ );
+
+ // attacklab: strip sentinel
+ text = text.replace(/~0/,"");
+
+ return text;
+}
+
+var hashBlock = function(text) {
+ text = text.replace(/(^\n+|\n+$)/g,"");
+ return "\n\n~K" + (g_html_blocks.push(text)-1) + "K\n\n";
+}
+
+
+var _DoCodeSpans = function(text) {
+//
+// * Backtick quotes are used for spans.
+//
+// * You can use multiple backticks as the delimiters if you want to
+// include literal backticks in the code span. So, this input:
+//
+// Just type ``foo `bar` baz`` at the prompt.
+//
+// Will translate to:
+//
+//
Just type foo `bar` baz at the prompt.
+//
+// There's no arbitrary limit to the number of backticks you
+// can use as delimters. If you need three consecutive backticks
+// in your code, use four for delimiters, etc.
+//
+// * You can use spaces to get literal backticks at the edges:
+//
+// ... type `` `bar` `` ...
+//
+// Turns to:
+//
+// ... type `bar` ...
+//
+
+ /*
+ text = text.replace(/
+ (^|[^\\]) // Character before opening ` can't be a backslash
+ (`+) // $2 = Opening run of `
+ ( // $3 = The code block
+ [^\r]*?
+ [^`] // attacklab: work around lack of lookbehind
+ )
+ \2 // Matching closer
+ (?!`)
+ /gm, function(){...});
+ */
+
+ text = text.replace(/(^|[^\\])(`+)([^\r]*?[^`])\2(?!`)/gm,
+ function(wholeMatch,m1,m2,m3,m4) {
+ var c = m3;
+ c = c.replace(/^([ \t]*)/g,""); // leading whitespace
+ c = c.replace(/[ \t]*$/g,""); // trailing whitespace
+ c = _EncodeCode(c);
+ return m1+""+c+"";
+ });
+
+ return text;
+}
+
+
+var _EncodeCode = function(text) {
+//
+// Encode/escape certain characters inside Markdown code runs.
+// The point is that in code, these characters are literals,
+// and lose their special Markdown meanings.
+//
+ // Encode all ampersands; HTML entities are not
+ // entities within a Markdown code span.
+ text = text.replace(/&/g,"&");
+
+ // Do the angle bracket song and dance:
+ text = text.replace(//g,">");
+
+ // Now, escape characters that are magic in Markdown:
+ text = escapeCharacters(text,"\*_{}[]\\",false);
+
+// jj the line above breaks this:
+//---
+
+//* Item
+
+// 1. Subitem
+
+// special char: *
+//---
+
+ return text;
+}
+
+
+var _DoItalicsAndBold = function(text) {
+
+ // must go first:
+ text = text.replace(/(\*\*|__)(?=\S)([^\r]*?\S[\*_]*)\1/g,
+ "$2");
+
+ text = text.replace(/(\*|_)(?=\S)([^\r]*?\S)\1/g,
+ "$2");
+
+ return text;
+}
+
+
+var _DoBlockQuotes = function(text) {
+
+ /*
+ text = text.replace(/
+ ( // Wrap whole match in $1
+ (
+ ^[ \t]*>[ \t]? // '>' at the start of a line
+ .+\n // rest of the first line
+ (.+\n)* // subsequent consecutive lines
+ \n* // blanks
+ )+
+ )
+ /gm, function(){...});
+ */
+
+ text = text.replace(/((^[ \t]*>[ \t]?.+\n(.+\n)*\n*)+)/gm,
+ function(wholeMatch,m1) {
+ var bq = m1;
+
+ // attacklab: hack around Konqueror 3.5.4 bug:
+ // "----------bug".replace(/^-/g,"") == "bug"
+
+ bq = bq.replace(/^[ \t]*>[ \t]?/gm,"~0"); // trim one level of quoting
+
+ // attacklab: clean up hack
+ bq = bq.replace(/~0/g,"");
+
+ bq = bq.replace(/^[ \t]+$/gm,""); // trim whitespace-only lines
+ bq = _RunBlockGamut(bq); // recurse
+
+ bq = bq.replace(/(^|\n)/g,"$1 ");
+ // These leading spaces screw with
content, so we need to fix that:
+ bq = bq.replace(
+ /(\s*
[^\r]+?<\/pre>)/gm,
+ function(wholeMatch,m1) {
+ var pre = m1;
+ // attacklab: hack around Konqueror 3.5.4 bug:
+ pre = pre.replace(/^ /mg,"~0");
+ pre = pre.replace(/~0/g,"");
+ return pre;
+ });
+
+ return hashBlock("
\n" + bq + "\n
");
+ });
+ return text;
+}
+
+
+var _FormParagraphs = function(text) {
+//
+// Params:
+// $text - string to process with html
tags
+//
+
+ // Strip leading and trailing lines:
+ text = text.replace(/^\n+/g,"");
+ text = text.replace(/\n+$/g,"");
+
+ var grafs = text.split(/\n{2,}/g);
+ var grafsOut = new Array();
+
+ //
+ // Wrap
tags.
+ //
+ var end = grafs.length;
+ for (var i=0; i= 0) {
+ grafsOut.push(str);
+ }
+ else if (str.search(/\S/) >= 0) {
+ str = _RunSpanGamut(str);
+ str = str.replace(/^([ \t]*)/g,"
");
+ str += "
"
+ grafsOut.push(str);
+ }
+
+ }
+
+ //
+ // Unhashify HTML blocks
+ //
+ end = grafsOut.length;
+ for (var i=0; i= 0) {
+ var blockText = g_html_blocks[RegExp.$1];
+ blockText = blockText.replace(/\$/g,"$$$$"); // Escape any dollar signs
+ grafsOut[i] = grafsOut[i].replace(/~K\d+K/,blockText);
+ }
+ }
+
+ return grafsOut.join("\n\n");
+}
+
+
+var _EncodeAmpsAndAngles = function(text) {
+// Smart processing for ampersands and angle brackets that need to be encoded.
+
+ // Ampersand-encoding based entirely on Nat Irons's Amputator MT plugin:
+ // http://bumppo.net/projects/amputator/
+ text = text.replace(/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/g,"&");
+
+ // Encode naked <'s
+ text = text.replace(/<(?![a-z\/?\$!])/gi,"<");
+
+ return text;
+}
+
+
+var _EncodeBackslashEscapes = function(text) {
+//
+// Parameter: String.
+// Returns: The string, with after processing the following backslash
+// escape sequences.
+//
+
+ // attacklab: The polite way to do this is with the new
+ // escapeCharacters() function:
+ //
+ // text = escapeCharacters(text,"\\",true);
+ // text = escapeCharacters(text,"`*_{}[]()>#+-.!",true);
+ //
+ // ...but we're sidestepping its use of the (slow) RegExp constructor
+ // as an optimization for Firefox. This function gets called a LOT.
+
+ text = text.replace(/\\(\\)/g,escapeCharacters_callback);
+ text = text.replace(/\\([`*_{}\[\]()>#+-.!])/g,escapeCharacters_callback);
+ return text;
+}
+
+
+var _DoAutoLinks = function(text) {
+
+ text = text.replace(/<((https?|ftp|dict):[^'">\s]+)>/gi,"$1");
+
+ // Email addresses:
+
+ /*
+ text = text.replace(/
+ <
+ (?:mailto:)?
+ (
+ [-.\w]+
+ \@
+ [-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+
+ )
+ >
+ /gi, _DoAutoLinks_callback());
+ */
+ text = text.replace(/<(?:mailto:)?([-.\w]+\@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+)>/gi,
+ function(wholeMatch,m1) {
+ return _EncodeEmailAddress( _UnescapeSpecialChars(m1) );
+ }
+ );
+
+ return text;
+}
+
+
+var _EncodeEmailAddress = function(addr) {
+//
+// Input: an email address, e.g. "foo@example.com"
+//
+// Output: the email address as a mailto link, with each character
+// of the address encoded as either a decimal or hex entity, in
+// the hopes of foiling most address harvesting spam bots. E.g.:
+//
+// foo
+// @example.com
+//
+// Based on a filter by Matthew Wickline, posted to the BBEdit-Talk
+// mailing list:
+//
+
+ // attacklab: why can't javascript speak hex?
+ function char2hex(ch) {
+ var hexDigits = '0123456789ABCDEF';
+ var dec = ch.charCodeAt(0);
+ return(hexDigits.charAt(dec>>4) + hexDigits.charAt(dec&15));
+ }
+
+ var encode = [
+ function(ch){return ""+ch.charCodeAt(0)+";";},
+ function(ch){return ""+char2hex(ch)+";";},
+ function(ch){return ch;}
+ ];
+
+ addr = "mailto:" + addr;
+
+ addr = addr.replace(/./g, function(ch) {
+ if (ch == "@") {
+ // this *must* be encoded. I insist.
+ ch = encode[Math.floor(Math.random()*2)](ch);
+ } else if (ch !=":") {
+ // leave ':' alone (to spot mailto: later)
+ var r = Math.random();
+ // roughly 10% raw, 45% hex, 45% dec
+ ch = (
+ r > .9 ? encode[2](ch) :
+ r > .45 ? encode[1](ch) :
+ encode[0](ch)
+ );
+ }
+ return ch;
+ });
+
+ addr = "" + addr + "";
+ addr = addr.replace(/">.+:/g,"\">"); // strip the mailto: from the visible part
+
+ return addr;
+}
+
+
+var _UnescapeSpecialChars = function(text) {
+//
+// Swap back in all the special characters we've hidden.
+//
+ text = text.replace(/~E(\d+)E/g,
+ function(wholeMatch,m1) {
+ var charCodeToReplace = parseInt(m1);
+ return String.fromCharCode(charCodeToReplace);
+ }
+ );
+ return text;
+}
+
+
+var _Outdent = function(text) {
+//
+// Remove one level of line-leading tabs or spaces
+//
+
+ // attacklab: hack around Konqueror 3.5.4 bug:
+ // "----------bug".replace(/^-/g,"") == "bug"
+
+ text = text.replace(/^(\t|[ ]{1,4})/gm,"~0"); // attacklab: g_tab_width
+
+ // attacklab: clean up hack
+ text = text.replace(/~0/g,"")
+
+ return text;
+}
+
+var _Detab = function(text) {
+// attacklab: Detab's completely rewritten for speed.
+// In perl we could fix it by anchoring the regexp with \G.
+// In javascript we're less fortunate.
+
+ // expand first n-1 tabs
+ text = text.replace(/\t(?=\t)/g," "); // attacklab: g_tab_width
+
+ // replace the nth with two sentinels
+ text = text.replace(/\t/g,"~A~B");
+
+ // use the sentinel to anchor our regex so it doesn't explode
+ text = text.replace(/~B(.+?)~A/g,
+ function(wholeMatch,m1,m2) {
+ var leadingText = m1;
+ var numSpaces = 4 - leadingText.length % 4; // attacklab: g_tab_width
+
+ // there *must* be a better way to do this:
+ for (var i=0; i";var D="
+
+ {% trans "system error log is recorded, error will be fixed as soon as possible" %}
+ {% trans "please report the error to the site administrators if you wish" %}
+
Here you can ask and answer questions, comment
+ and vote for the questions of others and their answers. Both questions and answers
+ can be revised and improved. Questions can be tagged with
+ the relevant keywords to simplify future access and organize the accumulated material.
+
+
+
This Q&A site is moderated by its members, hopefully - including yourself!
+ Moderation rights are gradually assigned to the site users based on the accumulated "reputation"
+ points. These points are added to the users account when others vote for his/her questions or answers.
+ These points (very) roughly reflect the level of trust of the community.
+
+
No points are necessary to ask or answer the questions - so please -
+ join us!
+
+{% endblock %}
+
+{% block sidebar %}
+{% include "question_edit_tips.html" %}
+{% endblock %}
+
+{% block endjs %}
+{% endblock %}
+
diff --git a/forum/skins/default/templates/auth/complete.html b/forum/skins/default/templates/auth/complete.html
new file mode 100755
index 0000000..cb2dc5a
--- /dev/null
+++ b/forum/skins/default/templates/auth/complete.html
@@ -0,0 +1,95 @@
+{% extends "base_content.html" %}
+
+{% load i18n %}
+{% block head %}{% endblock %}
+{% block title %}{% spaceless %}{% trans "Connect your OpenID with this site" %}{% endspaceless %}{% endblock %}
+{% block content %}
+
+ {% trans "Connect your OpenID with your account on this site" %}
+
+
+
+ {% trans "You are here for the first time with " %}{{ provider }}
+ {% trans "Please create your screen name and save your email address. Saved email address will let you subscribe for the updates on the most interesting questions and will be used to create and retrieve your unique avatar image. " %}
+
+
{% trans "This account already exists, please use another." %}
+
+
+ {% if form1.errors %}
+
+ {% if form1.non_field_errors %}
+ {% for error in form1.non_field_errors %}
+
+ {% trans "Community gives you awards for your questions, answers and votes." %}
+ {% blocktrans %}Below is the list of available badges and number
+ of times each type of badge has been awarded. Give us feedback at {{feedback_faq_url}}.
+ {% endblocktrans %}
+
+
+ {% for badge in badges %}
+
+
+ {% for a in mybadges %}
+ {% ifequal a.badge_id badge.id %}
+ ✔
+ {% endifequal %}
+ {% endfor %}
+
+ {% trans "Frequently Asked Questions " %}(FAQ)
+
+
+
+
+
{% trans "What kinds of questions can I ask here?" %}
+
{% trans "Most importanly - questions should be relevant to this community." %}
+ {% trans "Before asking the question - please make sure to use search to see whether your question has alredy been answered."%}
+
+
+
{% trans "What questions should I avoid asking?" %}
+
{% trans "Please avoid asking questions that are not relevant to this community, too subjective and argumentative." %}
+
+
+
+
+
{% trans "What should I avoid in my answers?" %}
+
{{ settings.APP_TITLE }} {% trans "is a Q&A site, not a discussion group. Therefore - please avoid having discussions in your answers, comment facility allows some space for brief discussions." %}
+
+
+
+
{% trans "Who moderates this community?" %}
+
{% trans "The short answer is: you." %}
+ {% trans "This website is moderated by the users." %}
+ {% trans "The reputation system allows users earn the authorization to perform a variety of moderation tasks." %}
+
+
+
+
+
{% trans "How does reputation system work?" %}
+
{% trans "Rep system summary" %}
+
{% blocktrans %}For example, if you ask an interesting question or give a helpful answer, your input will be upvoted. On the other hand if the answer is misleading - it will be downvoted. Each vote in favor will generate 10 points, each vote against will subtract 2 points. There is a limit of 200 points that can be accumulated per question or answer. The table below explains reputation point requirements for each type of moderation task.{% endblocktrans %}
+
+
+
+
+
+
+
+
+
+
50
+
{% trans "add comments" %}
+
+
+
100
+
{% trans "downvote" %}
+
+
250
+
{% trans "open and close own questions" %}
+
+
+
500
+
{% trans "retag questions" %}
+
+ {% if settings.WIKI_ON %}
+
+
750
+
{% trans "edit community wiki questions" %}
+
+ {% endif %}
+
+
2000
+
{% trans "edit any answer" %}
+
+
+
3000
+
{% trans "open any closed question" %}
+
+
+
5000
+
{% trans "delete any comment" %}
+
+
+
10000
+
{% trans "delete any questions and answers and perform other moderation tasks" %}
+
+ {% blocktrans %}how to validate email info with {{send_email_key_url}} {{gravatar_faq_url}}{% endblocktrans %}
+
+ {% endifequal %}
+ {% endcomment %}
+
+
{% trans "what is gravatar" %}
+
{% trans "gravatar faq info" %}
+
+
+
{% trans "To register, do I need to create new password?" %}
+
{% trans "No, you don't have to. You can login through any service that supports OpenID, e.g. Google, Yahoo, AOL, etc." %}
+ {% trans "Login now!" %} »
+
+
+
+
+
{% trans "Why other people can edit my questions/answers?" %}
+
{% trans "Goal of this site is..." %} {% trans "So questions and answers can be edited like wiki pages by experienced users of this site and this improves the overall quality of the knowledge base content." %}
+ {% trans "If this approach is not for you, we respect your choice." %}
+
+
+
+
{% trans "Still have questions?" %}
+
{% blocktrans %}Please ask your question at {{ask_question_url}}, help make our community better!{% endblocktrans %}
+
+
{% blocktrans with question.get_close_reason_display as close_reason %}The question has been closed for the following reason "{{ close_reason }}" by{% endblocktrans %}
+ {{ question.closed_by.username }}
+ {% blocktrans with question.closed_at as closed_at %}close date {{closed_at}}{% endblocktrans %}
+ {% if searchtag %}
+ {% 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 %}
+
+ {% ifequal tab_id "latest" %}
+ {% trans "latest questions info" %}
+ {% endifequal %}
+
+ {% ifequal tab_id "active" %}
+ {% trans "Questions are sorted by the time of last update." %}
+ {% trans "Most recently answered ones are shown first." %}
+ {% endifequal %}
+
+ {% ifequal tab_id "hottest" %}
+ {% trans "Questions sorted by number of responses." %}
+ {% trans "Most answered questions are shown first." %}
+ {% endifequal %}
+
+ {% ifequal tab_id "mostvoted" %}
+ {% trans "Questions are sorted by the number of votes." %}
+ {% trans "Most voted questions are shown first." %}
+ {% endifequal %}
+
+
+{% if request.user.is_authenticated %}
+{% include "tag_selector.html" %}
+{% endif %}
+
+
{% trans "Related tags" %}
+
+ {% for tag in tags %}
+ {{ tag.name }}
+ × {{ tag.used_count|intcomma }}
+
+ {% endfor %}
+
{% trans "The question was closed for the following reason " %}"{{ question.get_close_reason_display }}"{% trans "reason - leave blank in english" %} {{ question.closed_by.username }} {% trans "on "%} {% diff_date question.closed_at %}{% trans "date closed" %}
+
+
+{% if stag %}
+ {% trans "All tags matching query" %} '{{ stag }}' {% trans "all tags - make this empty in english" %}:
+{% endif %}
+{% if not tags.object_list %}
+ {% trans "Nothing found" %}
+{% endif %}
+
+
diff --git a/forum/templatetags/__init__.py b/forum/templatetags/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/forum/templatetags/extra_filters.py b/forum/templatetags/extra_filters.py
new file mode 100644
index 0000000..22ec010
--- /dev/null
+++ b/forum/templatetags/extra_filters.py
@@ -0,0 +1,98 @@
+from django import template
+from django.core import serializers
+from forum import auth
+import logging
+
+register = template.Library()
+
+@template.defaultfilters.stringfilter
+@register.filter
+def collapse(input):
+ return ' '.join(input.split())
+
+@register.filter
+def can_moderate_users(user):
+ return auth.can_moderate_users(user)
+
+@register.filter
+def can_vote_up(user):
+ return auth.can_vote_up(user)
+
+@register.filter
+def can_flag_offensive(user):
+ return auth.can_flag_offensive(user)
+
+@register.filter
+def can_add_comments(user,subject):
+ return auth.can_add_comments(user,subject)
+
+@register.filter
+def can_vote_down(user):
+ return auth.can_vote_down(user)
+
+@register.filter
+def can_retag_questions(user):
+ return auth.can_retag_questions(user)
+
+@register.filter
+def can_edit_post(user, post):
+ return auth.can_edit_post(user, post)
+
+@register.filter
+def can_delete_comment(user, comment):
+ return auth.can_delete_comment(user, comment)
+
+@register.filter
+def can_view_offensive_flags(user):
+ return auth.can_view_offensive_flags(user)
+
+@register.filter
+def can_close_question(user, question):
+ return auth.can_close_question(user, question)
+
+@register.filter
+def can_lock_posts(user):
+ return auth.can_lock_posts(user)
+
+@register.filter
+def can_accept_answer(user, question, answer):
+ return auth.can_accept_answer(user, question, answer)
+
+@register.filter
+def can_reopen_question(user, question):
+ return auth.can_reopen_question(user, question)
+
+@register.filter
+def can_delete_post(user, post):
+ return auth.can_delete_post(user, post)
+
+@register.filter
+def can_view_user_edit(request_user, target_user):
+ return auth.can_view_user_edit(request_user, target_user)
+
+@register.filter
+def can_view_user_votes(request_user, target_user):
+ return auth.can_view_user_votes(request_user, target_user)
+
+@register.filter
+def can_view_user_preferences(request_user, target_user):
+ return auth.can_view_user_preferences(request_user, target_user)
+
+@register.filter
+def is_user_self(request_user, target_user):
+ return auth.is_user_self(request_user, target_user)
+
+@register.filter
+def cnprog_intword(number):
+ try:
+ if 1000 <= number < 10000:
+ string = str(number)[0:1]
+ return "%sk" % string
+ else:
+ return number
+ except:
+ return number
+
+@register.filter
+def json_serialize(object):
+ return serializers.serialize('json',object)
diff --git a/forum/templatetags/extra_tags.py b/forum/templatetags/extra_tags.py
new file mode 100644
index 0000000..26c52b8
--- /dev/null
+++ b/forum/templatetags/extra_tags.py
@@ -0,0 +1,357 @@
+import time
+import os
+import posixpath
+import datetime
+import math
+import re
+import logging
+from django import template
+from django.utils.encoding import smart_unicode
+from django.utils.safestring import mark_safe
+from forum.const import *
+from forum.models import Question, Answer, QuestionRevision, AnswerRevision
+from django.utils.translation import ugettext as _
+from django.utils.translation import ungettext
+from django.conf import settings
+from forum import skins
+
+register = template.Library()
+
+GRAVATAR_TEMPLATE = ('')
+
+@register.simple_tag
+def gravatar(user, size):
+ """
+ Creates an ```` for a user's Gravatar with a given size.
+
+ This tag can accept a User object, or a dict containing the
+ appropriate values.
+ """
+ try:
+ gravatar = user['gravatar']
+ username = user['username']
+ except (TypeError, AttributeError, KeyError):
+ gravatar = user.gravatar
+ username = user.username
+ return mark_safe(GRAVATAR_TEMPLATE % {
+ 'size': size,
+ 'gravatar_hash': gravatar,
+ 'username': template.defaultfilters.urlencode(username),
+ })
+
+MAX_FONTSIZE = 18
+MIN_FONTSIZE = 12
+@register.simple_tag
+def tag_font_size(max_size, min_size, current_size):
+ """
+ do a logarithmic mapping calcuation for a proper size for tagging cloud
+ Algorithm from http://blogs.dekoh.com/dev/2007/10/29/choosing-a-good-font-size-variation-algorithm-for-your-tag-cloud/
+ """
+ #avoid invalid calculation
+ if current_size == 0:
+ current_size = 1
+ try:
+ weight = (math.log10(current_size) - math.log10(min_size)) / (math.log10(max_size) - math.log10(min_size))
+ except:
+ weight = 0
+ return MIN_FONTSIZE + round((MAX_FONTSIZE - MIN_FONTSIZE) * weight)
+
+
+LEADING_PAGE_RANGE_DISPLAYED = TRAILING_PAGE_RANGE_DISPLAYED = 5
+LEADING_PAGE_RANGE = TRAILING_PAGE_RANGE = 4
+NUM_PAGES_OUTSIDE_RANGE = 1
+ADJACENT_PAGES = 2
+@register.inclusion_tag("paginator.html")
+def cnprog_paginator(context):
+ """
+ custom paginator tag
+ Inspired from http://blog.localkinegrinds.com/2007/09/06/digg-style-pagination-in-django/
+ """
+ if (context["is_paginated"]):
+ " Initialize variables "
+ in_leading_range = in_trailing_range = False
+ pages_outside_leading_range = pages_outside_trailing_range = range(0)
+
+ if (context["pages"] <= LEADING_PAGE_RANGE_DISPLAYED):
+ in_leading_range = in_trailing_range = True
+ page_numbers = [n for n in range(1, context["pages"] + 1) if n > 0 and n <= context["pages"]]
+ elif (context["page"] <= LEADING_PAGE_RANGE):
+ in_leading_range = True
+ page_numbers = [n for n in range(1, LEADING_PAGE_RANGE_DISPLAYED + 1) if n > 0 and n <= context["pages"]]
+ pages_outside_leading_range = [n + context["pages"] for n in range(0, -NUM_PAGES_OUTSIDE_RANGE, -1)]
+ elif (context["page"] > context["pages"] - TRAILING_PAGE_RANGE):
+ in_trailing_range = True
+ page_numbers = [n for n in range(context["pages"] - TRAILING_PAGE_RANGE_DISPLAYED + 1, context["pages"] + 1) if n > 0 and n <= context["pages"]]
+ pages_outside_trailing_range = [n + 1 for n in range(0, NUM_PAGES_OUTSIDE_RANGE)]
+ else:
+ page_numbers = [n for n in range(context["page"] - ADJACENT_PAGES, context["page"] + ADJACENT_PAGES + 1) if n > 0 and n <= context["pages"]]
+ pages_outside_leading_range = [n + context["pages"] for n in range(0, -NUM_PAGES_OUTSIDE_RANGE, -1)]
+ pages_outside_trailing_range = [n + 1 for n in range(0, NUM_PAGES_OUTSIDE_RANGE)]
+
+ extend_url = context.get('extend_url', '')
+ return {
+ "base_url": context["base_url"],
+ "is_paginated": context["is_paginated"],
+ "previous": context["previous"],
+ "has_previous": context["has_previous"],
+ "next": context["next"],
+ "has_next": context["has_next"],
+ "page": context["page"],
+ "pages": context["pages"],
+ "page_numbers": page_numbers,
+ "in_leading_range" : in_leading_range,
+ "in_trailing_range" : in_trailing_range,
+ "pages_outside_leading_range": pages_outside_leading_range,
+ "pages_outside_trailing_range": pages_outside_trailing_range,
+ "extend_url" : extend_url
+ }
+
+@register.inclusion_tag("pagesize.html")
+def cnprog_pagesize(context):
+ """
+ display the pagesize selection boxes for paginator
+ """
+ if (context["is_paginated"]):
+ return {
+ "base_url": context["base_url"],
+ "pagesize" : context["pagesize"],
+ "is_paginated": context["is_paginated"]
+ }
+
+@register.inclusion_tag("post_contributor_info.html")
+def post_contributor_info(post,contributor_type='original_author'):
+ """contributor_type: original_author|last_updater
+ """
+ if isinstance(post,Question):
+ post_type = 'question'
+ elif isinstance(post,Answer):
+ post_type = 'answer'
+ elif isinstance(post,AnswerRevision) or isinstance(post,QuestionRevision):
+ post_type = 'revision'
+ return {
+ 'post':post,
+ 'post_type':post_type,
+ 'wiki_on':settings.WIKI_ON,
+ 'contributor_type':contributor_type
+ }
+
+@register.simple_tag
+def get_score_badge(user):
+ BADGE_TEMPLATE = '%(reputation)s'
+ if user.gold > 0 :
+ BADGE_TEMPLATE = '%s%s' % (BADGE_TEMPLATE, ''
+ '●'
+ '%(gold)s'
+ '')
+ if user.silver > 0:
+ BADGE_TEMPLATE = '%s%s' % (BADGE_TEMPLATE, ''
+ '●'
+ '%(silver)s'
+ '')
+ if user.bronze > 0:
+ BADGE_TEMPLATE = '%s%s' % (BADGE_TEMPLATE, ''
+ '●'
+ '%(bronze)s'
+ '')
+ BADGE_TEMPLATE = smart_unicode(BADGE_TEMPLATE, encoding='utf-8', strings_only=False, errors='strict')
+ return mark_safe(BADGE_TEMPLATE % {
+ 'reputation' : user.reputation,
+ 'gold' : user.gold,
+ 'silver' : user.silver,
+ 'bronze' : user.bronze,
+ 'badgesword' : _('badges'),
+ 'reputationword' : _('reputation points'),
+ })
+
+@register.simple_tag
+def get_score_badge_by_details(rep, gold, silver, bronze):
+ BADGE_TEMPLATE = '%(reputation)s'
+ if gold > 0 :
+ BADGE_TEMPLATE = '%s%s' % (BADGE_TEMPLATE, ''
+ '●'
+ '%(gold)s'
+ '')
+ if silver > 0:
+ BADGE_TEMPLATE = '%s%s' % (BADGE_TEMPLATE, ''
+ '●'
+ '%(silver)s'
+ '')
+ if bronze > 0:
+ BADGE_TEMPLATE = '%s%s' % (BADGE_TEMPLATE, ''
+ '●'
+ '%(bronze)s'
+ '')
+ BADGE_TEMPLATE = smart_unicode(BADGE_TEMPLATE, encoding='utf-8', strings_only=False, errors='strict')
+ return mark_safe(BADGE_TEMPLATE % {
+ 'reputation' : rep,
+ 'gold' : gold,
+ 'silver' : silver,
+ 'bronze' : bronze,
+ 'repword' : _('reputation points'),
+ 'badgeword' : _('badges'),
+ })
+
+@register.simple_tag
+def get_user_vote_image(dic, key, arrow):
+ if dic.has_key(key):
+ if int(dic[key]) == int(arrow):
+ return '-on'
+ return ''
+
+@register.simple_tag
+def get_age(birthday):
+ current_time = datetime.datetime(*time.localtime()[0:6])
+ year = birthday.year
+ month = birthday.month
+ day = birthday.day
+ diff = current_time - datetime.datetime(year,month,day,0,0,0)
+ return diff.days / 365
+
+@register.simple_tag
+def get_total_count(up_count, down_count):
+ return up_count + down_count
+
+@register.simple_tag
+def format_number(value):
+ strValue = str(value)
+ if len(strValue) <= 3:
+ return strValue
+ result = ''
+ first = ''
+ pattern = re.compile('(-?\d+)(\d{3})')
+ m = re.match(pattern, strValue)
+ while m != None:
+ first = m.group(1)
+ second = m.group(2)
+ result = ',' + second + result
+ strValue = first + ',' + second
+ m = re.match(pattern, strValue)
+ return first + result
+
+@register.simple_tag
+def convert2tagname_list(question):
+ question['tagnames'] = [name for name in question['tagnames'].split(u' ')]
+ return ''
+
+@register.simple_tag
+def diff_date(date, limen=2):
+ now = datetime.datetime.now()#datetime(*time.localtime()[0:6])#???
+ diff = now - date
+ days = diff.days
+ hours = int(diff.seconds/3600)
+ minutes = int(diff.seconds/60)
+
+ if days > 2:
+ if date.year == now.year:
+ return date.strftime("%b %d at %H:%M")
+ else:
+ return date.strftime("%b %d '%y at %H:%M")
+ elif days == 2:
+ return _('2 days ago')
+ elif days == 1:
+ return _('yesterday')
+ elif minutes >= 60:
+ return ungettext('%(hr)d hour ago','%(hr)d hours ago',hours) % {'hr':hours}
+ else:
+ return ungettext('%(min)d min ago','%(min)d mins ago',minutes) % {'min':minutes}
+
+@register.simple_tag
+def get_latest_changed_timestamp():
+ try:
+ from time import localtime, strftime
+ from os import path
+ root = settings.SITE_SRC_ROOT
+ dir = (
+ root,
+ '%s/forum' % root,
+ '%s/templates' % root,
+ )
+ stamp = (path.getmtime(d) for d in dir)
+ latest = max(stamp)
+ timestr = strftime("%H:%M %b-%d-%Y %Z", localtime(latest))
+ except:
+ timestr = ''
+ return timestr
+
+@register.simple_tag
+def media(url):
+ url = skins.find_media_source(url)
+ if url:
+ url = '///' + settings.FORUM_SCRIPT_ALIAS + '/m/' + url
+ return posixpath.normpath(url) + '?v=%d' % settings.RESOURCE_REVISION
+
+class ItemSeparatorNode(template.Node):
+ def __init__(self,separator):
+ sep = separator.strip()
+ if sep[0] == sep[-1] and sep[0] in ('\'','"'):
+ sep = sep[1:-1]
+ else:
+ raise template.TemplateSyntaxError('separator in joinitems tag must be quoted')
+ self.content = sep
+ def render(self,context):
+ return self.content
+
+class JoinItemListNode(template.Node):
+ def __init__(self,separator=ItemSeparatorNode("''"), items=()):
+ self.separator = separator
+ self.items = items
+ def render(self,context):
+ out = []
+ empty_re = re.compile(r'^\s*$')
+ for item in self.items:
+ bit = item.render(context)
+ if not empty_re.search(bit):
+ out.append(bit)
+ return self.separator.render(context).join(out)
+
+@register.tag(name="joinitems")
+def joinitems(parser,token):
+ try:
+ tagname,junk,sep_token = token.split_contents()
+ except ValueError:
+ raise template.TemplateSyntaxError("joinitems tag requires 'using \"separator html\"' parameters")
+ if junk == 'using':
+ sep_node = ItemSeparatorNode(sep_token)
+ else:
+ raise template.TemplateSyntaxError("joinitems tag requires 'using \"separator html\"' parameters")
+ nodelist = []
+ while True:
+ nodelist.append(parser.parse(('separator','endjoinitems')))
+ next = parser.next_token()
+ if next.contents == 'endjoinitems':
+ break
+
+ return JoinItemListNode(separator=sep_node,items=nodelist)
+
+class BlockMediaUrlNode(template.Node):
+ def __init__(self,nodelist):
+ self.items = nodelist
+ def render(self,context):
+ prefix = '///' + settings.FORUM_SCRIPT_ALIAS + 'm/'
+ url = ''
+ if self.items:
+ url += '/'
+ for item in self.items:
+ url += item.render(context)
+
+ url = skins.find_media_source(url)
+ url = prefix + url
+ out = posixpath.normpath(url) + '?v=%d' % settings.RESOURCE_REVISION
+ return out.replace(' ','')
+
+@register.tag(name='blockmedia')
+def blockmedia(parser,token):
+ try:
+ tagname = token.split_contents()
+ except ValueError:
+ raise template.TemplateSyntaxError("blockmedia tag does not use arguments")
+ nodelist = []
+ while True:
+ nodelist.append(parser.parse(('endblockmedia')))
+ next = parser.next_token()
+ if next.contents == 'endblockmedia':
+ break
+ return BlockMediaUrlNode(nodelist)
diff --git a/forum/templatetags/smart_if.py b/forum/templatetags/smart_if.py
new file mode 100644
index 0000000..ca3b43f
--- /dev/null
+++ b/forum/templatetags/smart_if.py
@@ -0,0 +1,401 @@
+"""
+A smarter {% if %} tag for django templates.
+
+While retaining current Django functionality, it also handles equality,
+greater than and less than operators. Some common case examples::
+
+ {% if articles|length >= 5 %}...{% endif %}
+ {% if "ifnotequal tag" != "beautiful" %}...{% endif %}
+"""
+import unittest
+from django import template
+
+
+register = template.Library()
+
+
+#==============================================================================
+# Calculation objects
+#==============================================================================
+
+class BaseCalc(object):
+ def __init__(self, var1, var2=None, negate=False):
+ self.var1 = var1
+ self.var2 = var2
+ self.negate = negate
+
+ def resolve(self, context):
+ try:
+ var1, var2 = self.resolve_vars(context)
+ outcome = self.calculate(var1, var2)
+ except:
+ outcome = False
+ if self.negate:
+ return not outcome
+ return outcome
+
+ def resolve_vars(self, context):
+ var2 = self.var2 and self.var2.resolve(context)
+ return self.var1.resolve(context), var2
+
+ def calculate(self, var1, var2):
+ raise NotImplementedError()
+
+
+class Or(BaseCalc):
+ def calculate(self, var1, var2):
+ return var1 or var2
+
+
+class And(BaseCalc):
+ def calculate(self, var1, var2):
+ return var1 and var2
+
+
+class Equals(BaseCalc):
+ def calculate(self, var1, var2):
+ return var1 == var2
+
+
+class Greater(BaseCalc):
+ def calculate(self, var1, var2):
+ return var1 > var2
+
+
+class GreaterOrEqual(BaseCalc):
+ def calculate(self, var1, var2):
+ return var1 >= var2
+
+
+class In(BaseCalc):
+ def calculate(self, var1, var2):
+ return var1 in var2
+
+
+#==============================================================================
+# Tests
+#==============================================================================
+
+class TestVar(object):
+ """
+ A basic self-resolvable object similar to a Django template variable. Used
+ to assist with tests.
+ """
+ def __init__(self, value):
+ self.value = value
+
+ def resolve(self, context):
+ return self.value
+
+
+class SmartIfTests(unittest.TestCase):
+ def setUp(self):
+ self.true = TestVar(True)
+ self.false = TestVar(False)
+ self.high = TestVar(9000)
+ self.low = TestVar(1)
+
+ def assertCalc(self, calc, context=None):
+ """
+ Test a calculation is True, also checking the inverse "negate" case.
+ """
+ context = context or {}
+ self.assert_(calc.resolve(context))
+ calc.negate = not calc.negate
+ self.assertFalse(calc.resolve(context))
+
+ def assertCalcFalse(self, calc, context=None):
+ """
+ Test a calculation is False, also checking the inverse "negate" case.
+ """
+ context = context or {}
+ self.assertFalse(calc.resolve(context))
+ calc.negate = not calc.negate
+ self.assert_(calc.resolve(context))
+
+ def test_or(self):
+ self.assertCalc(Or(self.true))
+ self.assertCalcFalse(Or(self.false))
+ self.assertCalc(Or(self.true, self.true))
+ self.assertCalc(Or(self.true, self.false))
+ self.assertCalc(Or(self.false, self.true))
+ self.assertCalcFalse(Or(self.false, self.false))
+
+ def test_and(self):
+ self.assertCalc(And(self.true, self.true))
+ self.assertCalcFalse(And(self.true, self.false))
+ self.assertCalcFalse(And(self.false, self.true))
+ self.assertCalcFalse(And(self.false, self.false))
+
+ def test_equals(self):
+ self.assertCalc(Equals(self.low, self.low))
+ self.assertCalcFalse(Equals(self.low, self.high))
+
+ def test_greater(self):
+ self.assertCalc(Greater(self.high, self.low))
+ self.assertCalcFalse(Greater(self.low, self.low))
+ self.assertCalcFalse(Greater(self.low, self.high))
+
+ def test_greater_or_equal(self):
+ self.assertCalc(GreaterOrEqual(self.high, self.low))
+ self.assertCalc(GreaterOrEqual(self.low, self.low))
+ self.assertCalcFalse(GreaterOrEqual(self.low, self.high))
+
+ def test_in(self):
+ list_ = TestVar([1,2,3])
+ invalid_list = TestVar(None)
+ self.assertCalc(In(self.low, list_))
+ self.assertCalcFalse(In(self.low, invalid_list))
+
+ def test_parse_bits(self):
+ var = IfParser([True]).parse()
+ self.assert_(var.resolve({}))
+ var = IfParser([False]).parse()
+ self.assertFalse(var.resolve({}))
+
+ var = IfParser([False, 'or', True]).parse()
+ self.assert_(var.resolve({}))
+
+ var = IfParser([False, 'and', True]).parse()
+ self.assertFalse(var.resolve({}))
+
+ var = IfParser(['not', False, 'and', 'not', False]).parse()
+ self.assert_(var.resolve({}))
+
+ var = IfParser(['not', 'not', True]).parse()
+ self.assert_(var.resolve({}))
+
+ var = IfParser([1, '=', 1]).parse()
+ self.assert_(var.resolve({}))
+
+ var = IfParser([1, 'not', '=', 1]).parse()
+ self.assertFalse(var.resolve({}))
+
+ var = IfParser([1, 'not', 'not', '=', 1]).parse()
+ self.assert_(var.resolve({}))
+
+ var = IfParser([1, '!=', 1]).parse()
+ self.assertFalse(var.resolve({}))
+
+ var = IfParser([3, '>', 2]).parse()
+ self.assert_(var.resolve({}))
+
+ var = IfParser([1, '<', 2]).parse()
+ self.assert_(var.resolve({}))
+
+ var = IfParser([2, 'not', 'in', [2, 3]]).parse()
+ self.assertFalse(var.resolve({}))
+
+ var = IfParser([1, 'or', 1, '=', 2]).parse()
+ self.assert_(var.resolve({}))
+
+ def test_boolean(self):
+ var = IfParser([True, 'and', True, 'and', True]).parse()
+ self.assert_(var.resolve({}))
+ var = IfParser([False, 'or', False, 'or', True]).parse()
+ self.assert_(var.resolve({}))
+ var = IfParser([True, 'and', False, 'or', True]).parse()
+ self.assert_(var.resolve({}))
+ var = IfParser([False, 'or', True, 'and', True]).parse()
+ self.assert_(var.resolve({}))
+
+ var = IfParser([True, 'and', True, 'and', False]).parse()
+ self.assertFalse(var.resolve({}))
+ var = IfParser([False, 'or', False, 'or', False]).parse()
+ self.assertFalse(var.resolve({}))
+ var = IfParser([False, 'or', True, 'and', False]).parse()
+ self.assertFalse(var.resolve({}))
+ var = IfParser([False, 'and', True, 'or', False]).parse()
+ self.assertFalse(var.resolve({}))
+
+ def test_invalid(self):
+ self.assertRaises(ValueError, IfParser(['not']).parse)
+ self.assertRaises(ValueError, IfParser(['==']).parse)
+ self.assertRaises(ValueError, IfParser([1, 'in']).parse)
+ self.assertRaises(ValueError, IfParser([1, '>', 'in']).parse)
+ self.assertRaises(ValueError, IfParser([1, '==', 'not', 'not']).parse)
+ self.assertRaises(ValueError, IfParser([1, 2]).parse)
+
+
+OPERATORS = {
+ '=': (Equals, True),
+ '==': (Equals, True),
+ '!=': (Equals, False),
+ '>': (Greater, True),
+ '>=': (GreaterOrEqual, True),
+ '<=': (Greater, False),
+ '<': (GreaterOrEqual, False),
+ 'or': (Or, True),
+ 'and': (And, True),
+ 'in': (In, True),
+}
+BOOL_OPERATORS = ('or', 'and')
+
+
+class IfParser(object):
+ error_class = ValueError
+
+ def __init__(self, tokens):
+ self.tokens = tokens
+
+ def _get_tokens(self):
+ return self._tokens
+
+ def _set_tokens(self, tokens):
+ self._tokens = tokens
+ self.len = len(tokens)
+ self.pos = 0
+
+ tokens = property(_get_tokens, _set_tokens)
+
+ def parse(self):
+ if self.at_end():
+ raise self.error_class('No variables provided.')
+ var1 = self.get_bool_var()
+ while not self.at_end():
+ op, negate = self.get_operator()
+ var2 = self.get_bool_var()
+ var1 = op(var1, var2, negate=negate)
+ return var1
+
+ def get_token(self, eof_message=None, lookahead=False):
+ negate = True
+ token = None
+ pos = self.pos
+ while token is None or token == 'not':
+ if pos >= self.len:
+ if eof_message is None:
+ raise self.error_class()
+ raise self.error_class(eof_message)
+ token = self.tokens[pos]
+ negate = not negate
+ pos += 1
+ if not lookahead:
+ self.pos = pos
+ return token, negate
+
+ def at_end(self):
+ return self.pos >= self.len
+
+ def create_var(self, value):
+ return TestVar(value)
+
+ def get_bool_var(self):
+ """
+ Returns either a variable by itself or a non-boolean operation (such as
+ ``x == 0`` or ``x < 0``).
+
+ This is needed to keep correct precedence for boolean operations (i.e.
+ ``x or x == 0`` should be ``x or (x == 0)``, not ``(x or x) == 0``).
+ """
+ var = self.get_var()
+ if not self.at_end():
+ op_token = self.get_token(lookahead=True)[0]
+ if isinstance(op_token, basestring) and (op_token not in
+ BOOL_OPERATORS):
+ op, negate = self.get_operator()
+ return op(var, self.get_var(), negate=negate)
+ return var
+
+ def get_var(self):
+ token, negate = self.get_token('Reached end of statement, still '
+ 'expecting a variable.')
+ if isinstance(token, basestring) and token in OPERATORS:
+ raise self.error_class('Expected variable, got operator (%s).' %
+ token)
+ var = self.create_var(token)
+ if negate:
+ return Or(var, negate=True)
+ return var
+
+ def get_operator(self):
+ token, negate = self.get_token('Reached end of statement, still '
+ 'expecting an operator.')
+ if not isinstance(token, basestring) or token not in OPERATORS:
+ raise self.error_class('%s is not a valid operator.' % token)
+ if self.at_end():
+ raise self.error_class('No variable provided after "%s".' % token)
+ op, true = OPERATORS[token]
+ if not true:
+ negate = not negate
+ return op, negate
+
+
+#==============================================================================
+# Actual templatetag code.
+#==============================================================================
+
+class TemplateIfParser(IfParser):
+ error_class = template.TemplateSyntaxError
+
+ def __init__(self, parser, *args, **kwargs):
+ self.template_parser = parser
+ return super(TemplateIfParser, self).__init__(*args, **kwargs)
+
+ def create_var(self, value):
+ return self.template_parser.compile_filter(value)
+
+
+class SmartIfNode(template.Node):
+ def __init__(self, var, nodelist_true, nodelist_false=None):
+ self.nodelist_true, self.nodelist_false = nodelist_true, nodelist_false
+ self.var = var
+
+ def render(self, context):
+ if self.var.resolve(context):
+ return self.nodelist_true.render(context)
+ if self.nodelist_false:
+ return self.nodelist_false.render(context)
+ return ''
+
+ def __repr__(self):
+ return ""
+
+ def __iter__(self):
+ for node in self.nodelist_true:
+ yield node
+ if self.nodelist_false:
+ for node in self.nodelist_false:
+ yield node
+
+ def get_nodes_by_type(self, nodetype):
+ nodes = []
+ if isinstance(self, nodetype):
+ nodes.append(self)
+ nodes.extend(self.nodelist_true.get_nodes_by_type(nodetype))
+ if self.nodelist_false:
+ nodes.extend(self.nodelist_false.get_nodes_by_type(nodetype))
+ return nodes
+
+
+@register.tag('if')
+def smart_if(parser, token):
+ """
+ A smarter {% if %} tag for django templates.
+
+ While retaining current Django functionality, it also handles equality,
+ greater than and less than operators. Some common case examples::
+
+ {% if articles|length >= 5 %}...{% endif %}
+ {% if "ifnotequal tag" != "beautiful" %}...{% endif %}
+
+ Arguments and operators _must_ have a space between them, so
+ ``{% if 1>2 %}`` is not a valid smart if tag.
+
+ All supported operators are: ``or``, ``and``, ``in``, ``=`` (or ``==``),
+ ``!=``, ``>``, ``>=``, ``<`` and ``<=``.
+ """
+ bits = token.split_contents()[1:]
+ var = TemplateIfParser(parser, bits).parse()
+ nodelist_true = parser.parse(('else', 'endif'))
+ token = parser.next_token()
+ if token.contents == 'else':
+ nodelist_false = parser.parse(('endif',))
+ parser.delete_first_token()
+ else:
+ nodelist_false = None
+ return SmartIfNode(var, nodelist_true, nodelist_false)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/forum/upfiles/README b/forum/upfiles/README
new file mode 100644
index 0000000..17bf8ec
--- /dev/null
+++ b/forum/upfiles/README
@@ -0,0 +1,2 @@
+This directory is to contain uploaded images and other files
+must be writable by the webserver
diff --git a/forum/urls.py b/forum/urls.py
new file mode 100644
index 0000000..f81bad6
--- /dev/null
+++ b/forum/urls.py
@@ -0,0 +1,114 @@
+import os.path
+from django.conf.urls.defaults import *
+from django.contrib import admin
+from forum import views as app
+from forum.feed import RssLastestQuestionsFeed
+from forum.sitemap import QuestionsSitemap
+from django.utils.translation import ugettext as _
+import logging
+
+admin.autodiscover()
+feeds = {
+ 'rss': RssLastestQuestionsFeed
+}
+sitemaps = {
+ 'questions': QuestionsSitemap
+}
+
+APP_PATH = os.path.dirname(__file__)
+urlpatterns = patterns('',
+ url(r'^$', app.readers.index, name='index'),
+ url(r'^sitemap.xml$', 'django.contrib.sitemaps.views.sitemap', {'sitemaps': sitemaps}, name='sitemap'),
+ #(r'^favicon\.ico$', 'django.views.generic.simple.redirect_to', {'url': '/media/images/favicon.ico'}),
+ #(r'^favicon\.gif$', 'django.views.generic.simple.redirect_to', {'url': '/media/images/favicon.gif'}),
+ url(r'^m/(?P.*)$', 'django.views.static.serve',
+ {'document_root': os.path.join(APP_PATH,'skins').replace('\\','/')},
+ name='osqa_media',
+ ),
+ url(r'^%s(?P.*)$' % _('upfiles/'), 'django.views.static.serve',
+ {'document_root': os.path.join(APP_PATH,'upfiles').replace('\\','/')},
+ name='uploaded_file',
+ ),
+ #url(r'^%s/$' % _('signin/'), 'django_authopenid.views.signin', name='signin'),
+ url(r'^%s$' % _('about/'), app.meta.about, name='about'),
+ url(r'^%s$' % _('faq/'), app.meta.faq, name='faq'),
+ url(r'^%s$' % _('privacy/'), app.meta.privacy, name='privacy'),
+ url(r'^%s$' % _('logout/'), app.meta.logout, name='logout'),
+ url(r'^%s(?P\d+)/%s$' % (_('answers/'), _('comments/')), app.writers.answer_comments, name='answer_comments'),
+ url(r'^%s(?P\d+)/%s$' % (_('answers/'), _('edit/')), app.writers.edit_answer, name='edit_answer'),
+ url(r'^%s(?P\d+)/%s$' % (_('answers/'), _('revisions/')), app.readers.answer_revisions, name='answer_revisions'),
+ url(r'^%s$' % _('questions/'), app.readers.questions, name='questions'),
+ url(r'^%s%s$' % (_('questions/'), _('ask/')), app.writers.ask, name='ask'),
+ url(r'^%s%s$' % (_('questions/'), _('unanswered/')), app.readers.unanswered, name='unanswered'),
+ url(r'^%s(?P\d+)/%s$' % (_('questions/'), _('edit/')), app.writers.edit_question, name='edit_question'),
+ url(r'^%s(?P\d+)/%s$' % (_('questions/'), _('close/')), app.commands.close, name='close'),
+ url(r'^%s(?P\d+)/%s$' % (_('questions/'), _('reopen/')), app.commands.reopen, name='reopen'),
+ url(r'^%s(?P\d+)/%s$' % (_('questions/'), _('answer/')), app.writers.answer, name='answer'),
+ url(r'^%s(?P\d+)/%s$' % (_('questions/'), _('vote/')), app.commands.vote, name='vote'),
+ url(r'^%s(?P\d+)/%s$' % (_('questions/'), _('revisions/')), app.readers.question_revisions, name='question_revisions'),
+ url(r'^%s(?P\d+)/%s$' % (_('questions/'), _('comments/')), app.writers.question_comments, name='question_comments'),
+ url(r'^%s$' % _('command/'), app.commands.ajax_command, name='call_ajax'),
+
+ url(r'^%s(?P\d+)/%s(?P\d+)/%s$' % (_('questions/'), _('comments/'),_('delete/')), \
+ app.writers.delete_comment, kwargs={'commented_object_type':'question'},\
+ name='delete_question_comment'),
+
+ url(r'^%s(?P\d+)/%s(?P\d+)/%s$' % (_('answers/'), _('comments/'),_('delete/')), \
+ app.writers.delete_comment, kwargs={'commented_object_type':'answer'}, \
+ name='delete_answer_comment'), \
+ #place general question item in the end of other operations
+ url(r'^%s(?P\d+)/' % _('question/'), app.readers.question, name='question'),
+ url(r'^%s$' % _('tags/'), app.readers.tags, name='tags'),
+ url(r'^%s(?P[^/]+)/$' % _('tags/'), app.readers.tag, name='tag_questions'),
+
+ url(r'^%s%s(?P[^/]+)/$' % (_('mark-tag/'),_('interesting/')), app.commands.mark_tag, \
+ kwargs={'reason':'good','action':'add'}, \
+ name='mark_interesting_tag'),
+
+ url(r'^%s%s(?P[^/]+)/$' % (_('mark-tag/'),_('ignored/')), app.commands.mark_tag, \
+ kwargs={'reason':'bad','action':'add'}, \
+ name='mark_ignored_tag'),
+
+ url(r'^%s(?P[^/]+)/$' % _('unmark-tag/'), app.commands.mark_tag, \
+ kwargs={'action':'remove'}, \
+ name='mark_ignored_tag'),
+
+ url(r'^%s$' % _('users/'),app.users.users, name='users'),
+ url(r'^%s(?P\d+)/$' % _('moderate-user/'), app.users.moderate_user, name='moderate_user'),
+ url(r'^%s(?P\d+)/%s$' % (_('users/'), _('edit/')), app.users.edit_user, name='edit_user'),
+ url(r'^%s(?P\d+)//*' % _('users/'), app.users.user, name='user'),
+ url(r'^%s$' % _('badges/'),app.meta.badges, name='badges'),
+ url(r'^%s(?P\d+)//*' % _('badges/'), app.meta.badge, name='badge'),
+ url(r'^%s%s$' % (_('messages/'), _('markread/')),app.commands.read_message, name='read_message'),
+ # (r'^admin/doc/' % _('admin/doc'), include('django.contrib.admindocs.urls')),
+ url(r'^%s(.*)' % _('nimda/'), admin.site.root, name='osqa_admin'),
+ url(r'^feeds/(?P.*)/$', 'django.contrib.syndication.views.feed', {'feed_dict': feeds}, name='feeds'),
+ url(r'^%s$' % _('upload/'), app.writers.upload, name='upload'),
+ url(r'^%s$' % _('search/'), app.readers.search, name='search'),
+ url(r'^%s$' % _('feedback/'), app.meta.feedback, name='feedback'),
+ #(r'^%sfb/' % _('account/'), include('fbconnect.urls')),
+ #(r'^%s' % _('account/'), include('django_authopenid.urls')),
+ (r'^i18n/', include('django.conf.urls.i18n')),
+
+ url(r'^%s%s$' % (_('account/'), _('signin/')), app.auth.signin_page, name='auth_signin'),
+ url(r'^%s%s$' % (_('account/'), _('signout/')), app.auth.signout, name='user_signout'),
+ url(r'^%s%s(?P\w+)/$' % (_('account/'), _('signin/')), app.auth.signin_page, name='auth_action_signin'),
+ url(r'^%s(?P\w+)/%s$' % (_('account/'), _('signin/')), app.auth.prepare_provider_signin, name='auth_provider_signin'),
+ url(r'^%s(?P\w+)/%s$' % (_('account/'), _('done/')), app.auth.process_provider_signin, name='auth_provider_done'),
+ url(r'^%s%s$' % (_('account/'), _('register/')), app.auth.external_register, name='auth_external_register'),
+
+ url(r'^%s%s$' % (_('account/'), _('password/')), app.users.changepw, name='user_changepw'),
+ #url(r'^%s%s%s$' % (_('accounts/'), _('password/'), _('confirm/')), app.user.confirmchangepw, name='user_confirmchangepw'),
+ url(r'^%s$' % _('account/'), app.users.account_settings, name='user_account_settings'),
+ #url(r'^%s$' % _('delete/'), app.users.delete, name='user_delete'),
+)
+
+from forum.modules import get_modules_script
+
+module_patterns = get_modules_script('urls')
+
+for pattern_file in module_patterns:
+ pattern = getattr(pattern_file, 'urlpatterns', None)
+ if pattern:
+ urlpatterns += pattern
+
diff --git a/forum/user_messages/__init__.py b/forum/user_messages/__init__.py
new file mode 100644
index 0000000..0136c88
--- /dev/null
+++ b/forum/user_messages/__init__.py
@@ -0,0 +1,36 @@
+"""
+Lightweight session-based messaging system.
+
+Time-stamp: <2009-03-10 19:22:29 carljm __init__.py>
+
+"""
+VERSION = (0, 1, 'pre')
+
+def create_message (request, message):
+ """
+ Create a message in the current session.
+
+ """
+ assert hasattr(request, 'session'), "django-session-messages requires session middleware to be installed. Edit your MIDDLEWARE_CLASSES setting to insert 'django.contrib.sessions.middleware.SessionMiddleware'."
+
+ try:
+ request.session['messages'].append(message)
+ except KeyError:
+ request.session['messages'] = [message]
+
+def get_and_delete_messages (request, include_auth=False):
+ """
+ Get and delete all messages for current session.
+
+ Optionally also fetches user messages from django.contrib.auth.
+
+ """
+ assert hasattr(request, 'session'), "django-session-messages requires session middleware to be installed. Edit your MIDDLEWARE_CLASSES setting to insert 'django.contrib.sessions.middleware.SessionMiddleware'."
+
+ messages = request.session.pop('messages', [])
+
+ if include_auth and request.user.is_authenticated():
+ messages.extend(request.user.get_and_delete_messages())
+
+ return messages
+
diff --git a/forum/user_messages/context_processors.py b/forum/user_messages/context_processors.py
new file mode 100644
index 0000000..2bf2626
--- /dev/null
+++ b/forum/user_messages/context_processors.py
@@ -0,0 +1,52 @@
+"""
+Context processor for lightweight session messages.
+
+Time-stamp: <2008-07-19 23:16:19 carljm context_processors.py>
+
+"""
+from django.utils.encoding import StrAndUnicode
+
+from forum.user_messages import get_and_delete_messages
+
+def user_messages (request):
+ """
+ Returns session messages for the current session.
+
+ """
+ messages = request.user.get_and_delete_messages()
+ #if request.user.is_authenticated():
+ #else:
+ # messages = LazyMessages(request)
+ return { 'user_messages': messages }
+
+class LazyMessages (StrAndUnicode):
+ """
+ Lazy message container, so messages aren't actually retrieved from
+ session and deleted until the template asks for them.
+
+ """
+ def __init__(self, request):
+ self.request = request
+
+ def __iter__(self):
+ return iter(self.messages)
+
+ def __len__(self):
+ return len(self.messages)
+
+ def __nonzero__(self):
+ return bool(self.messages)
+
+ def __unicode__(self):
+ return unicode(self.messages)
+
+ def __getitem__(self, *args, **kwargs):
+ return self.messages.__getitem__(*args, **kwargs)
+
+ def _get_messages(self):
+ if hasattr(self, '_messages'):
+ return self._messages
+ self._messages = get_and_delete_messages(self.request)
+ return self._messages
+ messages = property(_get_messages)
+
diff --git a/forum/utils/__init__.py b/forum/utils/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/forum/utils/cache.py b/forum/utils/cache.py
new file mode 100644
index 0000000..410c066
--- /dev/null
+++ b/forum/utils/cache.py
@@ -0,0 +1,92 @@
+"""Utilities for working with Django Models."""
+import itertools
+
+from django.contrib.contenttypes.models import ContentType
+
+from lanai.utils.lists import flatten
+
+def fetch_model_dict(model, ids, fields=None):
+ """
+ Fetches a dict of model details for model instances with the given
+ ids, keyed by their id.
+
+ If a fields list is given, a dict of details will be retrieved for
+ each model, otherwise complete model instances will be retrieved.
+
+ Any fields list given shouldn't contain the primary key attribute for
+ the model, as this can be determined from its Options.
+ """
+ if fields is None:
+ return model._default_manager.in_bulk(ids)
+ else:
+ id_attr = model._meta.pk.attname
+ return dict((obj[id_attr], obj) for obj
+ in model._default_manager.filter(id__in=ids).values(
+ *itertools.chain((id_attr,), fields)))
+
+def populate_foreign_key_caches(model, objects_to_populate, fields=None):
+ """
+ Populates caches for the given related Model in instances of objects
+ which have a ForeignKey relationship to it, specified as a list of
+ (object list, related attribute name list) two-tuples.
+
+ If a list of field names is given, only the given fields will be
+ looked up and related object caches will be populated with a dict of
+ the specified fields. Otherwise, complete model instances will be
+ retrieved.
+ """
+ # Get all related object ids for the appropriate fields
+ related_object_ids = []
+ for objects, attrs in objects_to_populate:
+ related_object_ids.append(tuple(tuple(getattr(obj, '%s_id' % attr)
+ for attr in attrs)
+ for obj in objects))
+ unique_ids = tuple(set(pk for pk in flatten(related_object_ids) if pk))
+ related_objects = fetch_model_dict(model, unique_ids, fields)
+
+ # Fill related object caches
+ for (objects, attrs), related_ids in itertools.izip(objects_to_populate,
+ related_object_ids):
+ for obj, related_ids_for_obj in itertools.izip(objects,
+ related_ids):
+ for attr, related_object in itertools.izip(attrs, (related_objects.get(pk, None)
+ for pk in related_ids_for_obj)):
+ setattr(obj, '_%s_cache' % attr, related_object)
+
+def populate_content_object_caches(generic_related_objects, model_fields=None):
+ """
+ Retrieves ``ContentType`` and content objects for the given list of
+ items which use a generic relation, grouping the retrieval of content
+ objects by model to reduce the number of queries executed.
+
+ This results in ``number_of_content_types + 1`` queries rather than
+ the ``number_of_generic_reL_objects * 2`` queries you'd get by
+ iterating over the list and accessing each item's object attribute.
+
+ If a dict mapping model classes to field names is given, only the
+ given fields will be looked up for each model specified and the
+ object cache will be populated with a dict of the specified fields.
+ Otherwise, complete model instances will be retrieved.
+ """
+ if model_fields is None:
+ model_fields = {}
+
+ # Group content object ids by their content type ids
+ ids_by_content_type = {}
+ for obj in generic_related_objects:
+ ids_by_content_type.setdefault(obj.content_type_id,
+ []).append(obj.object_id)
+
+ # Retrieve content types and content objects in bulk
+ content_types = ContentType.objects.in_bulk(ids_by_content_type.keys())
+ for content_type_id, ids in ids_by_content_type.iteritems():
+ model = content_types[content_type_id].model_class()
+ objects[content_type_id] = fetch_model_dict(
+ model, tuple(set(ids)), model_fields.get(model, None))
+
+ # Set content types and content objects in the appropriate cache
+ # attributes, so accessing the 'content_type' and 'object' attributes
+ # on each object won't result in further database hits.
+ for obj in generic_related_objects:
+ obj._object_cache = objects[obj.content_type_id][obj.object_id]
+ obj._content_type_cache = content_types[obj.content_type_id]
diff --git a/forum/utils/decorators.py b/forum/utils/decorators.py
new file mode 100644
index 0000000..e4e7acb
--- /dev/null
+++ b/forum/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/forum/utils/diff.py b/forum/utils/diff.py
new file mode 100644
index 0000000..d741d78
--- /dev/null
+++ b/forum/utils/diff.py
@@ -0,0 +1,66 @@
+#!/usr/bin/python2.2
+"""HTML Diff: http://www.aaronsw.com/2002/diff
+Rough code, badly documented. Send me comments and patches."""
+
+__author__ = 'Aaron Swartz '
+__copyright__ = '(C) 2003 Aaron Swartz. GNU GPL 2.'
+__version__ = '0.22'
+
+import difflib, string
+
+def isTag(x): return x[0] == "<" and x[-1] == ">"
+
+def textDiff(a, b):
+ """Takes in strings a and b and returns a human-readable HTML diff."""
+
+ out = []
+ a, b = html2list(a), html2list(b)
+ s = difflib.SequenceMatcher(None, a, b)
+ for e in s.get_opcodes():
+ if e[0] == "replace":
+ # @@ need to do something more complicated here
+ # call textDiff but not for html, but for some html... ugh
+ # gonna cop-out for now
+ out.append(''+''.join(a[e[1]:e[2]]) + ''+''.join(b[e[3]:e[4]])+"")
+ elif e[0] == "delete":
+ out.append(''+ ''.join(a[e[1]:e[2]]) + "")
+ elif e[0] == "insert":
+ out.append(''+''.join(b[e[3]:e[4]]) + "")
+ elif e[0] == "equal":
+ out.append(''.join(b[e[3]:e[4]]))
+ else:
+ raise "Um, something's broken. I didn't expect a '" + `e[0]` + "'."
+ return ''.join(out)
+
+def html2list(x, b=0):
+ mode = 'char'
+ cur = ''
+ out = []
+ for c in x:
+ if mode == 'tag':
+ if c == '>':
+ if b: cur += ']'
+ else: cur += c
+ out.append(cur); cur = ''; mode = 'char'
+ else: cur += c
+ elif mode == 'char':
+ if c == '<':
+ out.append(cur)
+ if b: cur = '['
+ else: cur = c
+ mode = 'tag'
+ elif c in string.whitespace: out.append(cur+c); cur = ''
+ else: cur += c
+ out.append(cur)
+ return filter(lambda x: x is not '', out)
+
+if __name__ == '__main__':
+ import sys
+ try:
+ a, b = sys.argv[1:3]
+ except ValueError:
+ print "htmldiff: highlight the differences between two html files"
+ print "usage: " + sys.argv[0] + " a b"
+ sys.exit(1)
+ print textDiff(open(a).read(), open(b).read())
+
diff --git a/forum/utils/forms.py b/forum/utils/forms.py
new file mode 100644
index 0000000..c54056c
--- /dev/null
+++ b/forum/utils/forms.py
@@ -0,0 +1,151 @@
+from django import forms
+import re
+from django.utils.translation import ugettext as _
+from django.utils.safestring import mark_safe
+from django.conf import settings
+from django.http import str_to_unicode
+from django.contrib.auth.models import User
+import urllib
+
+DEFAULT_NEXT = '/' + getattr(settings, 'FORUM_SCRIPT_ALIAS')
+def clean_next(next):
+ if next is None:
+ return DEFAULT_NEXT
+ next = str_to_unicode(urllib.unquote(next), 'utf-8')
+ next = next.strip()
+ if next.startswith('/'):
+ return next
+ return DEFAULT_NEXT
+
+def get_next_url(request):
+ return clean_next(request.REQUEST.get('next'))
+
+class StrippedNonEmptyCharField(forms.CharField):
+ def clean(self,value):
+ value = value.strip()
+ if self.required and value == '':
+ raise forms.ValidationError(_('this field is required'))
+ return value
+
+class NextUrlField(forms.CharField):
+ def __init__(self):
+ super(NextUrlField,self).__init__(max_length = 255,widget = forms.HiddenInput(),required = False)
+ def clean(self,value):
+ return clean_next(value)
+
+login_form_widget_attrs = { 'class': 'required login' }
+username_re = re.compile(r'^[\w ]+$')
+
+class UserNameField(StrippedNonEmptyCharField):
+ RESERVED_NAMES = (u'fuck', u'shit', u'ass', u'sex', u'add',
+ u'edit', u'save', u'delete', u'manage', u'update', 'remove', 'new')
+ def __init__(self,db_model=User, db_field='username', must_exist=False,skip_clean=False,label=_('choose a username'),**kw):
+ self.must_exist = must_exist
+ self.skip_clean = skip_clean
+ self.db_model = db_model
+ self.db_field = db_field
+ error_messages={'required':_('user name is required'),
+ 'taken':_('sorry, this name is taken, please choose another'),
+ 'forbidden':_('sorry, this name is not allowed, please choose another'),
+ 'missing':_('sorry, there is no user with this name'),
+ 'multiple-taken':_('sorry, we have a serious error - user name is taken by several users'),
+ 'invalid':_('user name can only consist of letters, empty space and underscore'),
+ }
+ if 'error_messages' in kw:
+ error_messages.update(kw['error_messages'])
+ del kw['error_messages']
+ super(UserNameField,self).__init__(max_length=30,
+ widget=forms.TextInput(attrs=login_form_widget_attrs),
+ label=label,
+ error_messages=error_messages,
+ **kw
+ )
+
+ def clean(self,username):
+ """ validate username """
+ if self.skip_clean == True:
+ return username
+ if hasattr(self, 'user_instance') and isinstance(self.user_instance, User):
+ if username == self.user_instance.username:
+ return username
+ try:
+ username = super(UserNameField, self).clean(username)
+ except forms.ValidationError:
+ raise forms.ValidationError(self.error_messages['required'])
+ if self.required and not username_re.search(username):
+ raise forms.ValidationError(self.error_messages['invalid'])
+ if username in self.RESERVED_NAMES:
+ raise forms.ValidationError(self.error_messages['forbidden'])
+ try:
+ user = self.db_model.objects.get(
+ **{'%s' % self.db_field : username}
+ )
+ if user:
+ if self.must_exist:
+ return username
+ else:
+ raise forms.ValidationError(self.error_messages['taken'])
+ except self.db_model.DoesNotExist:
+ if self.must_exist:
+ raise forms.ValidationError(self.error_messages['missing'])
+ else:
+ return username
+ except self.db_model.MultipleObjectsReturned:
+ raise forms.ValidationError(self.error_messages['multiple-taken'])
+
+class UserEmailField(forms.EmailField):
+ def __init__(self,skip_clean=False,**kw):
+ self.skip_clean = skip_clean
+ super(UserEmailField,self).__init__(widget=forms.TextInput(attrs=dict(login_form_widget_attrs,
+ maxlength=200)), label=mark_safe(_('your email address')),
+ error_messages={'required':_('email address is required'),
+ 'invalid':_('please enter a valid email address'),
+ 'taken':_('this email is already used by someone else, please choose another'),
+ },
+ **kw
+ )
+
+ def clean(self,email):
+ """ validate if email exist in database
+ from legacy register
+ return: raise error if it exist """
+ email = super(UserEmailField,self).clean(email.strip())
+ if self.skip_clean:
+ return email
+ if settings.EMAIL_UNIQUE == True:
+ try:
+ user = User.objects.get(email = email)
+ raise forms.ValidationError(self.error_messsages['taken'])
+ except User.DoesNotExist:
+ return email
+ except User.MultipleObjectsReturned:
+ raise forms.ValidationError(self.error_messages['taken'])
+ else:
+ return email
+
+class SetPasswordForm(forms.Form):
+ password1 = forms.CharField(widget=forms.PasswordInput(attrs=login_form_widget_attrs),
+ label=_('choose password'),
+ error_messages={'required':_('password is required')},
+ )
+ password2 = forms.CharField(widget=forms.PasswordInput(attrs=login_form_widget_attrs),
+ label=mark_safe(_('retype password')),
+ error_messages={'required':_('please, retype your password'),
+ 'nomatch':_('sorry, entered passwords did not match, please try again')},
+ )
+ def clean_password2(self):
+ """
+ Validates that the two password inputs match.
+
+ """
+ if 'password1' in self.cleaned_data:
+ if self.cleaned_data['password1'] == self.cleaned_data['password2']:
+ self.password = self.cleaned_data['password2']
+ self.cleaned_data['password'] = self.cleaned_data['password2']
+ return self.cleaned_data['password2']
+ else:
+ del self.cleaned_data['password2']
+ raise forms.ValidationError(self.fields['password2'].error_messages['nomatch'])
+ else:
+ return self.cleaned_data['password2']
+
diff --git a/forum/utils/html.py b/forum/utils/html.py
new file mode 100644
index 0000000..25a74a4
--- /dev/null
+++ b/forum/utils/html.py
@@ -0,0 +1,51 @@
+"""Utilities for working with HTML."""
+import html5lib
+from html5lib import sanitizer, serializer, tokenizer, treebuilders, treewalkers
+
+class HTMLSanitizerMixin(sanitizer.HTMLSanitizerMixin):
+ acceptable_elements = ('a', 'abbr', 'acronym', 'address', 'b', 'big',
+ 'blockquote', 'br', 'caption', 'center', 'cite', 'code', 'col',
+ 'colgroup', 'dd', 'del', 'dfn', 'dir', 'div', 'dl', 'dt', 'em', 'font',
+ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'ins', 'kbd',
+ 'li', 'ol', 'p', 'pre', 'q', 's', 'samp', 'small', 'span', 'strike',
+ 'strong', 'sub', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead',
+ 'tr', 'tt', 'u', 'ul', 'var')
+
+ acceptable_attributes = ('abbr', 'align', 'alt', 'axis', 'border',
+ 'cellpadding', 'cellspacing', 'char', 'charoff', 'charset', 'cite',
+ 'cols', 'colspan', 'datetime', 'dir', 'frame', 'headers', 'height',
+ 'href', 'hreflang', 'hspace', 'lang', 'longdesc', 'name', 'nohref',
+ 'noshade', 'nowrap', 'rel', 'rev', 'rows', 'rowspan', 'rules', 'scope',
+ 'span', 'src', 'start', 'summary', 'title', 'type', 'valign', 'vspace',
+ 'width')
+
+ allowed_elements = acceptable_elements
+ allowed_attributes = acceptable_attributes
+ allowed_css_properties = ()
+ allowed_css_keywords = ()
+ allowed_svg_properties = ()
+
+class HTMLSanitizer(tokenizer.HTMLTokenizer, HTMLSanitizerMixin):
+ def __init__(self, stream, encoding=None, parseMeta=True, useChardet=True,
+ lowercaseElementName=True, lowercaseAttrName=True):
+ tokenizer.HTMLTokenizer.__init__(self, stream, encoding, parseMeta,
+ useChardet, lowercaseElementName,
+ lowercaseAttrName)
+
+ def __iter__(self):
+ for token in tokenizer.HTMLTokenizer.__iter__(self):
+ token = self.sanitize_token(token)
+ if token:
+ yield token
+
+def sanitize_html(html):
+ """Sanitizes an HTML fragment."""
+ p = html5lib.HTMLParser(tokenizer=HTMLSanitizer,
+ tree=treebuilders.getTreeBuilder("dom"))
+ dom_tree = p.parseFragment(html)
+ walker = treewalkers.getTreeWalker("dom")
+ stream = walker(dom_tree)
+ s = serializer.HTMLSerializer(omit_optional_tags=False,
+ quote_attr_values=True)
+ output_generator = s.serialize(stream)
+ return u''.join(output_generator)
diff --git a/forum/utils/lists.py b/forum/utils/lists.py
new file mode 100644
index 0000000..bbcfae9
--- /dev/null
+++ b/forum/utils/lists.py
@@ -0,0 +1,86 @@
+"""Utilities for working with lists and sequences."""
+
+def flatten(x):
+ """
+ Returns a single, flat list which contains all elements retrieved
+ from the sequence and all recursively contained sub-sequences
+ (iterables).
+
+ Examples:
+ >>> [1, 2, [3, 4], (5, 6)]
+ [1, 2, [3, 4], (5, 6)]
+
+ From http://kogs-www.informatik.uni-hamburg.de/~meine/python_tricks
+ """
+ result = []
+ for el in x:
+ if hasattr(el, '__iter__') and not isinstance(el, basestring):
+ result.extend(flatten(el))
+ else:
+ result.append(el)
+ return result
+
+def batch_size(items, size):
+ """
+ Retrieves items in batches of the given size.
+
+ >>> l = range(1, 11)
+ >>> batch_size(l, 3)
+ [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]]
+ >>> batch_size(l, 5)
+ [[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]]
+ """
+ return [items[i:i+size] for i in xrange(0, len(items), size)]
+
+def batches(items, number):
+ """
+ Retrieves items in the given number of batches.
+
+ >>> l = range(1, 11)
+ >>> batches(l, 1)
+ [[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]]
+ >>> batches(l, 2)
+ [[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]]
+ >>> batches(l, 3)
+ [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10]]
+ >>> batches(l, 4)
+ [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]]
+ >>> batches(l, 5)
+ [[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]]
+
+ Initial batches will contain as many items as possible in cases where
+ there are not enough items to be distributed evenly.
+
+ >>> batches(l, 6)
+ [[1, 2], [3, 4], [5, 6], [7, 8], [9], [10]]
+ >>> batches(l, 7)
+ [[1, 2], [3, 4], [5, 6], [7], [8], [9], [10]]
+ >>> batches(l, 8)
+ [[1, 2], [3, 4], [5], [6], [7], [8], [9], [10]]
+ >>> batches(l, 9)
+ [[1, 2], [3], [4], [5], [6], [7], [8], [9], [10]]
+ >>> batches(l, 10)
+ [[1], [2], [3], [4], [5], [6], [7], [8], [9], [10]]
+
+ If there are more batches than items, empty batches will be appended
+ to the batch list.
+
+ >>> batches(l, 11)
+ [[1], [2], [3], [4], [5], [6], [7], [8], [9], [10], []]
+ >>> batches(l, 12)
+ [[1], [2], [3], [4], [5], [6], [7], [8], [9], [10], [], []]
+ """
+ div, mod= divmod(len(items), number)
+ if div > 1:
+ if mod:
+ div += 1
+ return batch_size(items, div)
+ else:
+ if not div:
+ return [[item] for item in items] + [[]] * (number - mod)
+ elif div == 1 and not mod:
+ return [[item] for item in items]
+ else:
+ # mod now tells you how many lists of 2 you can fit in
+ return ([items[i*2:(i*2)+2] for i in xrange(0, mod)] +
+ [[item] for item in items[mod*2:]])
diff --git a/forum/utils/odict.py b/forum/utils/odict.py
new file mode 100644
index 0000000..2c8391d
--- /dev/null
+++ b/forum/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 ,'
+ 'Michael Foord ')
+
+__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])
+
+ """
+ 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)
+
diff --git a/forum/views/README b/forum/views/README
new file mode 100644
index 0000000..5416f88
--- /dev/null
+++ b/forum/views/README
@@ -0,0 +1,12 @@
+readers.py - views strictly reading main content: questions, answers, tags and comments
+
+writers.py - views that write main content, with possible reading
+ note: deletion counts as writing in this case
+
+commands.py - data status changing commands, votes, question close/reopen
+
+users.py - user views - user listing and profiles
+
+meta.py - privacy, about, faq, feedback, logout, badges
+
+auth.py - Authentication related views
diff --git a/forum/views/__init__.py b/forum/views/__init__.py
new file mode 100644
index 0000000..a5f6f99
--- /dev/null
+++ b/forum/views/__init__.py
@@ -0,0 +1,6 @@
+import readers
+import writers
+import commands
+import users
+import meta
+import auth
diff --git a/forum/views/auth.py b/forum/views/auth.py
new file mode 100755
index 0000000..2e95316
--- /dev/null
+++ b/forum/views/auth.py
@@ -0,0 +1,212 @@
+from django.shortcuts import render_to_response
+from django.template import RequestContext
+from django.core.urlresolvers import reverse
+from django.contrib.auth.models import User
+from django.http import HttpResponseRedirect
+from django.utils.safestring import mark_safe
+from django.utils.translation import ugettext as _
+from django.contrib.auth.decorators import login_required
+from django.contrib.auth import login, logout
+from django.http import get_host
+import types
+
+from forum.models import AuthKeyUserAssociation
+from forum.authentication.forms import SimpleRegistrationForm, SimpleEmailSubscribeForm
+
+from forum.authentication.base import InvalidAuthentication
+from forum.authentication import AUTH_PROVIDERS
+
+from forum.models import Question, Answer
+
+def signin_page(request, action=None):
+ if action is None:
+ request.session['on_signin_url'] = request.META.get('HTTP_REFERER', '/')
+ else:
+ request.session['on_signin_action'] = action
+
+ all_providers = [provider.context for provider in AUTH_PROVIDERS.values()]
+
+ sort = lambda c1, c2: c1.weight - c2.weight
+ can_show = lambda c: not request.user.is_authenticated() or c.show_to_logged_in_user
+
+ bigicon_providers = sorted([
+ context for context in all_providers if context.mode == 'BIGICON' and can_show(context)
+ ], sort)
+
+ smallicon_providers = sorted([
+ context for context in all_providers if context.mode == 'SMALLICON' and can_show(context)
+ ], sort)
+
+ stackitem_providers = sorted([
+ context for context in all_providers if context.mode == 'STACK_ITEM' and can_show(context)
+ ], sort)
+
+ try:
+ msg = request.session['auth_error']
+ del request.session['auth_error']
+ except:
+ msg = None
+
+ return render_to_response(
+ 'auth/signin.html',
+ {
+ 'msg': msg,
+ 'all_providers': all_providers,
+ 'bigicon_providers': bigicon_providers,
+ 'stackitem_providers': stackitem_providers,
+ 'smallicon_providers': smallicon_providers,
+ },
+ RequestContext(request))
+
+def prepare_provider_signin(request, provider):
+ force_email_request = request.REQUEST.get('validate_email', 'yes') == 'yes'
+ request.session['force_email_request'] = force_email_request
+
+ if provider in AUTH_PROVIDERS:
+ provider_class = AUTH_PROVIDERS[provider].consumer
+
+ try:
+ request_url = provider_class.prepare_authentication_request(request,
+ reverse('auth_provider_done', kwargs={'provider': provider}))
+
+ return HttpResponseRedirect(request_url)
+ except NotImplementedError, e:
+ return process_provider_signin(request, provider)
+ except InvalidAuthentication, e:
+ request.session['auth_error'] = e.message
+
+ return HttpResponseRedirect(reverse('auth_signin'))
+
+
+def process_provider_signin(request, provider):
+ if provider in AUTH_PROVIDERS:
+ provider_class = AUTH_PROVIDERS[provider].consumer
+
+ try:
+ assoc_key = provider_class.process_authentication_request(request)
+ except InvalidAuthentication, e:
+ request.session['auth_error'] = e.message
+ return HttpResponseRedirect(reverse('auth_signin'))
+
+ if request.user.is_authenticated():
+ if isinstance(assoc_key, (type, User)):
+ if request.user != assoc_key:
+ request.session['auth_error'] = _("Sorry, these login credentials belong to anoother user. Plese terminate your current session and try again.")
+ else:
+ request.session['auth_error'] = _("You are already logged in with that user.")
+ else:
+ try:
+ assoc = AuthKeyUserAssociation.objects.get(key=assoc_key)
+ if assoc.user == request.user:
+ request.session['auth_error'] = _("These login credentials are already associated with your account.")
+ else:
+ request.session['auth_error'] = _("Sorry, these login credentials belong to anoother user. Plese terminate your current session and try again.")
+ except:
+ uassoc = AuthKeyUserAssociation(user=request.user, key=assoc_key, provider=provider)
+ uassoc.save()
+ request.session['auth_error'] = _("These new credentials are now associated with your account.")
+ return HttpResponseRedirect(reverse('auth_signin'))
+
+ try:
+ assoc = AuthKeyUserAssociation.objects.get(key=assoc_key)
+ user_ = assoc.user
+ return login_and_forward(request, user_)
+ except:
+ request.session['assoc_key'] = assoc_key
+ request.session['auth_provider'] = provider
+ return HttpResponseRedirect(reverse('auth_external_register'))
+
+ return HttpResponseRedirect(reverse('auth_signin'))
+
+def external_register(request):
+ if request.method == 'POST' and 'bnewaccount' in request.POST:
+ form1 = SimpleRegistrationForm(request.POST)
+ email_feeds_form = SimpleEmailSubscribeForm(request.POST)
+
+ if (form1.is_valid() and email_feeds_form.is_valid()):
+ tmp_pwd = User.objects.make_random_password()
+ user_ = User.objects.create_user(form1.cleaned_data['username'],
+ form1.cleaned_data['email'], tmp_pwd)
+
+ user_.set_unusable_password()
+
+ uassoc = AuthKeyUserAssociation(user=user_, key=request.session['assoc_key'], provider=request.session['auth_provider'])
+ uassoc.save()
+
+ email_feeds_form.save(user_)
+
+ del request.session['assoc_key']
+ del request.session['auth_provider']
+ return login_and_forward(request, user_)
+ else:
+ provider_class = AUTH_PROVIDERS[request.session['auth_provider']].consumer
+ user_data = provider_class.get_user_data(request.session['assoc_key'])
+
+ username = user_data.get('username', '')
+ email = user_data.get('email', '')
+
+ if not email:
+ email = request.session.get('auth_email_request', '')
+
+ form1 = SimpleRegistrationForm(initial={
+ 'next': '/',
+ 'username': username,
+ 'email': email,
+ })
+ email_feeds_form = SimpleEmailSubscribeForm()
+
+ provider_context = AUTH_PROVIDERS[request.session['auth_provider']].context
+
+ return render_to_response('auth/complete.html', {
+ 'form1': form1,
+ 'email_feeds_form': email_feeds_form,
+ 'provider':mark_safe(provider_context.human_name),
+ 'login_type':provider_context.id,
+ 'gravatar_faq_url':reverse('faq') + '#gravatar',
+ }, context_instance=RequestContext(request))
+
+def newquestion_signin_action(user):
+ question = Question.objects.filter(author=user).order_by('-added_at')[0]
+ return question.get_absolute_url()
+
+def newanswer_signin_action(user):
+ answer = Answer.objects.filter(author=user).order_by('-added_at')[0]
+ return answer.get_absolute_url()
+
+POST_SIGNIN_ACTIONS = {
+ 'newquestion': newquestion_signin_action,
+ 'newanswer': newanswer_signin_action,
+}
+
+def login_and_forward(request, user):
+ old_session = request.session.session_key
+ user.backend = "django.contrib.auth.backends.ModelBackend"
+ login(request, user)
+
+ from forum.models import user_logged_in
+ user_logged_in.send(user=user,session_key=old_session,sender=None)
+
+ redirect = request.session.get('on_signin_url', None)
+
+ if not redirect:
+ signin_action = request.session.get('on_signin_action', None)
+ if not signin_action:
+ redirect = reverse('index')
+ else:
+ try:
+ redirect = POST_SIGNIN_ACTIONS[signin_action](user)
+ except:
+ redirect = reverse('index')
+
+ return HttpResponseRedirect(redirect)
+
+@login_required
+def signout(request):
+ """
+ signout from the website. Remove openid from session and kill it.
+
+ url : /signout/"
+ """
+
+ logout(request)
+ return HttpResponseRedirect(reverse('index'))
\ No newline at end of file
diff --git a/forum/views/commands.py b/forum/views/commands.py
new file mode 100644
index 0000000..88c2c07
--- /dev/null
+++ b/forum/views/commands.py
@@ -0,0 +1,335 @@
+import datetime
+from django.conf import settings
+from django.utils import simplejson
+from django.http import HttpResponse, HttpResponseRedirect
+from django.shortcuts import get_object_or_404
+from django.utils.translation import ugettext as _
+from django.template import RequestContext
+from forum.models import *
+from forum.forms import CloseForm
+from forum import auth
+from django.core.urlresolvers import reverse
+from django.contrib.auth.decorators import login_required
+from forum.utils.decorators import ajax_method, ajax_login_required
+import logging
+
+def vote(request, id):#refactor - pretty incomprehensible view used by various ajax calls
+#issues: this subroutine is too long, contains many magic numbers and other issues
+#it's called "vote" but many actions processed here have nothing to do with voting
+ """
+ vote_type:
+ acceptAnswer : 0,
+ questionUpVote : 1,
+ questionDownVote : 2,
+ favorite : 4,
+ answerUpVote: 5,
+ answerDownVote:6,
+ offensiveQuestion : 7,
+ offensiveAnswer:8,
+ removeQuestion: 9,
+ removeAnswer:10
+ questionSubscribeUpdates:11
+ questionUnSubscribeUpdates:12
+
+ accept answer code:
+ response_data['allowed'] = -1, Accept his own answer 0, no allowed - Anonymous 1, Allowed - by default
+ response_data['success'] = 0, failed 1, Success - by default
+ response_data['status'] = 0, By default 1, Answer has been accepted already(Cancel)
+
+ vote code:
+ allowed = -3, Don't have enough votes left
+ -2, Don't have enough reputation score
+ -1, Vote his own post
+ 0, no allowed - Anonymous
+ 1, Allowed - by default
+ status = 0, By default
+ 1, Cancel
+ 2, Vote is too old to be canceled
+
+ offensive code:
+ allowed = -3, Don't have enough flags left
+ -2, Don't have enough reputation score to do this
+ 0, not allowed
+ 1, allowed
+ status = 0, by default
+ 1, can't do it again
+ """
+ response_data = {
+ "allowed": 1,
+ "success": 1,
+ "status" : 0,
+ "count" : 0,
+ "message" : ''
+ }
+
+ def __can_vote(vote_score, user):#refactor - belongs to auth.py
+ if vote_score == 1:#refactor magic number
+ return auth.can_vote_up(request.user)
+ else:
+ return auth.can_vote_down(request.user)
+
+ try:
+ if not request.user.is_authenticated():
+ response_data['allowed'] = 0
+ response_data['success'] = 0
+
+ elif request.is_ajax() and request.method == 'POST':
+ question = get_object_or_404(Question, id=id)
+ vote_type = request.POST.get('type')
+
+ #accept answer
+ if vote_type == '0':
+ answer_id = request.POST.get('postId')
+ answer = get_object_or_404(Answer, id=answer_id)
+ # make sure question author is current user
+ if question.author == request.user:
+ # answer user who is also question author is not allow to accept answer
+ if answer.author == question.author:
+ response_data['success'] = 0
+ response_data['allowed'] = -1
+ # check if answer has been accepted already
+ elif answer.accepted:
+ auth.onAnswerAcceptCanceled(answer, request.user)
+ response_data['status'] = 1
+ else:
+ # set other answers in this question not accepted first
+ for answer_of_question in Answer.objects.get_answers_from_question(question, request.user):
+ if answer_of_question != answer and answer_of_question.accepted:
+ auth.onAnswerAcceptCanceled(answer_of_question, request.user)
+
+ #make sure retrieve data again after above author changes, they may have related data
+ answer = get_object_or_404(Answer, id=answer_id)
+ auth.onAnswerAccept(answer, request.user)
+ else:
+ response_data['allowed'] = 0
+ response_data['success'] = 0
+ # favorite
+ elif vote_type == '4':
+ has_favorited = False
+ fav_questions = FavoriteQuestion.objects.filter(question=question)
+ # if the same question has been favorited before, then delete it
+ if fav_questions is not None:
+ for item in fav_questions:
+ if item.user == request.user:
+ item.delete()
+ response_data['status'] = 1
+ response_data['count'] = len(fav_questions) - 1
+ if response_data['count'] < 0:
+ response_data['count'] = 0
+ has_favorited = True
+ # if above deletion has not been executed, just insert a new favorite question
+ if not has_favorited:
+ new_item = FavoriteQuestion(question=question, user=request.user)
+ new_item.save()
+ response_data['count'] = FavoriteQuestion.objects.filter(question=question).count()
+ Question.objects.update_favorite_count(question)
+
+ elif vote_type in ['1', '2', '5', '6']:
+ post_id = id
+ post = question
+ vote_score = 1
+ if vote_type in ['5', '6']:
+ answer_id = request.POST.get('postId')
+ answer = get_object_or_404(Answer, id=answer_id)
+ post_id = answer_id
+ post = answer
+ if vote_type in ['2', '6']:
+ vote_score = -1
+
+ if post.author == request.user:
+ response_data['allowed'] = -1
+ elif not __can_vote(vote_score, request.user):
+ response_data['allowed'] = -2
+ elif post.votes.filter(user=request.user).count() > 0:
+ vote = post.votes.filter(user=request.user)[0]
+ # unvote should be less than certain time
+ if (datetime.datetime.now().day - vote.voted_at.day) >= auth.VOTE_RULES['scope_deny_unvote_days']:
+ response_data['status'] = 2
+ else:
+ voted = vote.vote
+ if voted > 0:
+ # cancel upvote
+ auth.onUpVotedCanceled(vote, post, request.user)
+
+ else:
+ # cancel downvote
+ auth.onDownVotedCanceled(vote, post, request.user)
+
+ response_data['status'] = 1
+ response_data['count'] = post.score
+ elif Vote.objects.get_votes_count_today_from_user(request.user) >= auth.VOTE_RULES['scope_votes_per_user_per_day']:
+ response_data['allowed'] = -3
+ else:
+ vote = Vote(user=request.user, content_object=post, vote=vote_score, voted_at=datetime.datetime.now())
+ if vote_score > 0:
+ # upvote
+ auth.onUpVoted(vote, post, request.user)
+ else:
+ # downvote
+ auth.onDownVoted(vote, post, request.user)
+
+ votes_left = auth.VOTE_RULES['scope_votes_per_user_per_day'] - Vote.objects.get_votes_count_today_from_user(request.user)
+ if votes_left <= auth.VOTE_RULES['scope_warn_votes_left']:
+ response_data['message'] = u'%s votes left' % votes_left
+ response_data['count'] = post.score
+ elif vote_type in ['7', '8']:
+ post = question
+ post_id = id
+ if vote_type == '8':
+ post_id = request.POST.get('postId')
+ post = get_object_or_404(Answer, id=post_id)
+
+ if FlaggedItem.objects.get_flagged_items_count_today(request.user) >= auth.VOTE_RULES['scope_flags_per_user_per_day']:
+ response_data['allowed'] = -3
+ elif not auth.can_flag_offensive(request.user):
+ response_data['allowed'] = -2
+ elif post.flagged_items.filter(user=request.user).count() > 0:
+ response_data['status'] = 1
+ else:
+ item = FlaggedItem(user=request.user, content_object=post, flagged_at=datetime.datetime.now())
+ auth.onFlaggedItem(item, post, request.user)
+ response_data['count'] = post.offensive_flag_count
+ # send signal when question or answer be marked offensive
+ mark_offensive.send(sender=post.__class__, instance=post, mark_by=request.user)
+ elif vote_type in ['9', '10']:
+ post = question
+ post_id = id
+ if vote_type == '10':
+ post_id = request.POST.get('postId')
+ post = get_object_or_404(Answer, id=post_id)
+
+ if not auth.can_delete_post(request.user, post):
+ response_data['allowed'] = -2
+ elif post.deleted == True:
+ logging.debug('debug restoring post in view')
+ auth.onDeleteCanceled(post, request.user)
+ response_data['status'] = 1
+ else:
+ auth.onDeleted(post, request.user)
+ delete_post_or_answer.send(sender=post.__class__, instance=post, delete_by=request.user)
+ elif vote_type == '11':#subscribe q updates
+ user = request.user
+ if user.is_authenticated():
+ if user not in question.followed_by.all():
+ question.followed_by.add(user)
+ if settings.EMAIL_VALIDATION == 'on' and user.email_isvalid == False:
+ response_data['message'] = \
+ _('subscription saved, %(email)s needs validation, see %(details_url)s') \
+ % {'email':user.email,'details_url':reverse('faq') + '#validate'}
+ feed_setting = EmailFeedSetting.objects.get(subscriber=user,feed_type='q_sel')
+ if feed_setting.frequency == 'n':
+ feed_setting.frequency = 'd'
+ feed_setting.save()
+ if 'message' in response_data:
+ response_data['message'] += ' '
+ response_data['message'] = _('email update frequency has been set to daily')
+ #response_data['status'] = 1
+ #responst_data['allowed'] = 1
+ else:
+ pass
+ #response_data['status'] = 0
+ #response_data['allowed'] = 0
+ elif vote_type == '12':#unsubscribe q updates
+ user = request.user
+ if user.is_authenticated():
+ if user in question.followed_by.all():
+ question.followed_by.remove(user)
+ else:
+ response_data['success'] = 0
+ response_data['message'] = u'Request mode is not supported. Please try again.'
+
+ data = simplejson.dumps(response_data)
+
+ except Exception, e:
+ response_data['message'] = str(e)
+ data = simplejson.dumps(response_data)
+ return HttpResponse(data, mimetype="application/json")
+
+#internally grouped views - used by the tagging system
+@ajax_login_required
+def mark_tag(request, tag=None, **kwargs):#tagging system
+ 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):#ajax tagging and tag-filtering system
+ 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):#refactor? view processing ajax commands - note "vote" and view others do it too
+ if 'command' not in request.POST:
+ return HttpResponseForbidden(mimetype="application/json")
+ if request.POST['command'] == 'toggle-ignored-questions':
+ return ajax_toggle_ignored_questions(request)
+
+@login_required
+def close(request, id):#close question
+ """view to initiate and process
+ question close
+ """
+ question = get_object_or_404(Question, id=id)
+ if not auth.can_close_question(request.user, question):
+ return HttpResponse('Permission denied.')
+ if request.method == 'POST':
+ form = CloseForm(request.POST)
+ if form.is_valid():
+ reason = form.cleaned_data['reason']
+ question.closed = True
+ question.closed_by = request.user
+ question.closed_at = datetime.datetime.now()
+ question.close_reason = reason
+ question.save()
+ return HttpResponseRedirect(question.get_absolute_url())
+ else:
+ form = CloseForm()
+ return render_to_response('close.html', {
+ 'form' : form,
+ 'question' : question,
+ }, context_instance=RequestContext(request))
+
+@login_required
+def reopen(request, id):#re-open question
+ """view to initiate and process
+ question close
+ """
+ question = get_object_or_404(Question, id=id)
+ # open question
+ if not auth.can_reopen_question(request.user, question):
+ return HttpResponse('Permission denied.')
+ if request.method == 'POST' :
+ Question.objects.filter(id=question.id).update(closed=False,
+ closed_by=None, closed_at=None, close_reason=None)
+ return HttpResponseRedirect(question.get_absolute_url())
+ else:
+ return render_to_response('reopen.html', {
+ 'question' : question,
+ }, context_instance=RequestContext(request))
+
+#osqa-user communication system
+def read_message(request):#marks message a read
+ if request.method == "POST":
+ if request.POST['formdata'] == 'required':
+ request.session['message_silent'] = 1
+ if request.user.is_authenticated():
+ request.user.delete_messages()
+ return HttpResponse('')
diff --git a/forum/views/meta.py b/forum/views/meta.py
new file mode 100644
index 0000000..6417e8f
--- /dev/null
+++ b/forum/views/meta.py
@@ -0,0 +1,91 @@
+from django.shortcuts import render_to_response, get_object_or_404
+from django.core.urlresolvers import reverse
+from django.template import RequestContext
+from django.http import HttpResponseRedirect, HttpResponse
+from forum.forms import FeedbackForm
+from django.core.urlresolvers import reverse
+from django.core.mail import mail_admins
+from django.utils.translation import ugettext as _
+from forum.utils.forms import get_next_url
+from forum.models import Badge, Award
+
+def about(request):
+ return render_to_response('about.html', context_instance=RequestContext(request))
+
+def faq(request):
+ data = {
+ 'gravatar_faq_url': reverse('faq') + '#gravatar',
+ #'send_email_key_url': reverse('send_email_key'),
+ 'ask_question_url': reverse('ask'),
+ }
+ return render_to_response('faq.html', data, context_instance=RequestContext(request))
+
+def feedback(request):
+ data = {}
+ form = None
+ if request.method == "POST":
+ form = FeedbackForm(request.POST)
+ if form.is_valid():
+ if not request.user.is_authenticated:
+ data['email'] = form.cleaned_data.get('email',None)
+ data['message'] = form.cleaned_data['message']
+ data['name'] = form.cleaned_data.get('name',None)
+ message = render_to_response('feedback_email.txt',data,context_instance=RequestContext(request))
+ mail_admins(_('Q&A forum feedback'), message)
+ msg = _('Thanks for the feedback!')
+ request.user.message_set.create(message=msg)
+ return HttpResponseRedirect(get_next_url(request))
+ else:
+ form = FeedbackForm(initial={'next':get_next_url(request)})
+
+ data['form'] = form
+ return render_to_response('feedback.html', data, context_instance=RequestContext(request))
+feedback.CANCEL_MESSAGE=_('We look forward to hearing your feedback! Please, give it next time :)')
+
+def privacy(request):
+ return render_to_response('privacy.html', context_instance=RequestContext(request))
+
+def logout(request):#refactor/change behavior?
+#currently you click logout and you get
+#to this view which actually asks you again - do you really want to log out?
+#I guess rationale was to tell the user that s/he may be still logged in
+#through their external login sytem and we'd want to remind them about it
+#however it might be a little annoying
+#why not just show a message: you are logged out of osqa, but
+#if you really want to log out -> go to your openid provider
+ return render_to_response('logout.html', {
+ 'next' : get_next_url(request),
+ }, context_instance=RequestContext(request))
+
+def badges(request):#user status/reputation system
+ badges = Badge.objects.all().order_by('name')
+ my_badges = []
+ if request.user.is_authenticated():
+ my_badges = Award.objects.filter(user=request.user).values('badge_id')
+ #my_badges.query.group_by = ['badge_id']
+
+ return render_to_response('badges.html', {
+ 'badges' : badges,
+ 'mybadges' : my_badges,
+ 'feedback_faq_url' : reverse('feedback'),
+ }, context_instance=RequestContext(request))
+
+def badge(request, id):
+ badge = get_object_or_404(Badge, id=id)
+ awards = Award.objects.extra(
+ select={'id': 'auth_user.id',
+ 'name': 'auth_user.username',
+ 'rep':'auth_user.reputation',
+ 'gold': 'auth_user.gold',
+ 'silver': 'auth_user.silver',
+ 'bronze': 'auth_user.bronze'},
+ tables=['award', 'auth_user'],
+ where=['badge_id=%s AND user_id=auth_user.id'],
+ params=[id]
+ ).distinct('id')
+
+ return render_to_response('badge.html', {
+ 'awards' : awards,
+ 'badge' : badge,
+ }, context_instance=RequestContext(request))
+
diff --git a/forum/views/readers.py b/forum/views/readers.py
new file mode 100644
index 0000000..6b0da47
--- /dev/null
+++ b/forum/views/readers.py
@@ -0,0 +1,588 @@
+# encoding:utf-8
+import datetime
+import logging
+from urllib import unquote
+from django.conf import settings
+from django.shortcuts import render_to_response, get_object_or_404
+from django.http import HttpResponseRedirect, HttpResponse, HttpResponseForbidden, Http404
+from django.core.paginator import Paginator, EmptyPage, InvalidPage
+from django.template import RequestContext
+from django.utils.html import *
+from django.utils import simplejson
+from django.db.models import Q
+from django.utils.translation import ugettext as _
+from django.template.defaultfilters import slugify
+from django.core.urlresolvers import reverse
+from django.utils.datastructures import SortedDict
+
+from forum.utils.html import sanitize_html
+from markdown2 import Markdown
+#from lxml.html.diff import htmldiff
+from forum.utils.diff import textDiff as htmldiff
+from forum.forms import *
+from forum.models import *
+from forum.auth import *
+from forum.const import *
+from forum import auth
+from forum.utils.forms import get_next_url
+
+# used in index page
+#refactor - move these numbers somewhere?
+INDEX_PAGE_SIZE = 30
+INDEX_AWARD_SIZE = 15
+INDEX_TAGS_SIZE = 25
+# used in tags list
+DEFAULT_PAGE_SIZE = 60
+# used in questions
+QUESTIONS_PAGE_SIZE = 30
+# used in answers
+ANSWERS_PAGE_SIZE = 10
+
+markdowner = Markdown(html4tags=True)
+
+#system to display main content
+def _get_tags_cache_json():#service routine used by views requiring tag list in the javascript space
+ """returns list of all tags in json format
+ no caching yet, actually
+ """
+ tags = Tag.objects.filter(deleted=False).all()
+ tags_list = []
+ for tag in tags:
+ dic = {'n': tag.name, 'c': tag.used_count}
+ tags_list.append(dic)
+ tags = simplejson.dumps(tags_list)
+ return tags
+
+def _get_and_remember_questions_sort_method(request, view_dic, default):#service routine used by q listing views and question view
+ """manages persistence of post sort order
+ it is assumed that when user wants newest question -
+ then he/she wants newest answers as well, etc.
+ how far should this assumption actually go - may be a good question
+ """
+ if default not in view_dic:
+ raise Exception('default value must be in view_dic')
+
+ q_sort_method = request.REQUEST.get('sort', None)
+ if q_sort_method == None:
+ q_sort_method = request.session.get('questions_sort_method', default)
+
+ if q_sort_method not in view_dic:
+ q_sort_method = default
+ request.session['questions_sort_method'] = q_sort_method
+ return q_sort_method, view_dic[q_sort_method]
+
+#refactor? - we have these
+#views that generate a listing of questions in one way or another:
+#index, unanswered, questions, search, tag
+#should we dry them up?
+#related topics - information drill-down, search refinement
+
+def index(request):#generates front page - shows listing of questions sorted in various ways
+ """index view mapped to the root url of the Q&A site
+ """
+ view_dic = {
+ "latest":"-last_activity_at",
+ "hottest":"-answer_count",
+ "mostvoted":"-score",
+ }
+ view_id, orderby = _get_and_remember_questions_sort_method(request, view_dic, 'latest')
+
+ pagesize = request.session.get("pagesize",QUESTIONS_PAGE_SIZE)
+ try:
+ page = int(request.GET.get('page', '1'))
+ except ValueError:
+ page = 1
+
+ qs = Question.objects.exclude(deleted=True).order_by(orderby)
+
+ objects_list = Paginator(qs, pagesize)
+ questions = objects_list.page(page)
+
+ # 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,
+ "awards" : awards[:INDEX_AWARD_SIZE],
+ "context" : {
+ 'is_paginated' : True,
+ 'pages': objects_list.num_pages,
+ 'page': page,
+ 'has_previous': questions.has_previous(),
+ 'has_next': questions.has_next(),
+ 'previous': questions.previous_page_number(),
+ 'next': questions.next_page_number(),
+ 'base_url' : request.path + '?sort=%s&' % view_id,
+ 'pagesize' : pagesize
+ }}, context_instance=RequestContext(request))
+
+def unanswered(request):#generates listing of unanswered questions
+ return questions(request, unanswered=True)
+
+def questions(request, tagname=None, unanswered=False):#a view generating listing of questions, used by 'unanswered' too
+ """
+ List of Questions, Tagged questions, and Unanswered questions.
+ """
+ # template file
+ # "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
+ # get pagesize from session, if failed then get default value
+ pagesize = request.session.get("pagesize",QUESTIONS_PAGE_SIZE)
+ try:
+ page = int(request.GET.get('page', '1'))
+ except ValueError:
+ page = 1
+
+ view_dic = {"latest":"-added_at", "active":"-last_activity_at", "hottest":"-answer_count", "mostvoted":"-score" }
+ view_id, orderby = _get_and_remember_questions_sort_method(request,view_dic,'latest')
+
+ # check if request is from tagged questions
+ qs = Question.objects.exclude(deleted=True)
+
+ if tagname is not None:
+ qs = qs.filter(tags__name = unquote(tagname))
+
+ 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
+ if questions.object_list.count() > 0:
+ 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,
+ 'page': page,
+ 'has_previous': questions.has_previous(),
+ 'has_next': questions.has_next(),
+ 'previous': questions.previous_page_number(),
+ 'next': questions.next_page_number(),
+ 'base_url' : request.path + '?sort=%s&' % view_id,
+ 'pagesize' : pagesize
+ }}, context_instance=RequestContext(request))
+
+def search(request): #generates listing of questions matching a search query - including tags and just words
+ """generates listing of questions matching a search query
+ supports full text search in mysql db using sphinx and internally in postgresql
+ falls back on simple partial string matching approach if
+ full text search function is not available
+ """
+ if request.method == "GET":
+ keywords = request.GET.get("q")
+ search_type = request.GET.get("t")
+ try:
+ page = int(request.GET.get('page', '1'))
+ except ValueError:
+ page = 1
+ if keywords is None:
+ return HttpResponseRedirect(reverse(index))
+ if search_type == 'tag':
+ return HttpResponseRedirect(reverse('tags') + '?q=%s&page=%s' % (keywords.strip(), page))
+ elif search_type == "user":
+ return HttpResponseRedirect(reverse('users') + '?q=%s&page=%s' % (keywords.strip(), page))
+ elif search_type == "question":
+
+ template_file = "questions.html"
+ # Set flag to False by default. If it is equal to True, then need to be saved.
+ pagesize_changed = False
+ # get pagesize from session, if failed then get default value
+ user_page_size = request.session.get("pagesize", QUESTIONS_PAGE_SIZE)
+ # set pagesize equal to logon user specified value in database
+ if request.user.is_authenticated() and request.user.questions_per_page > 0:
+ user_page_size = request.user.questions_per_page
+
+ try:
+ page = int(request.GET.get('page', '1'))
+ # get new pagesize from UI selection
+ pagesize = int(request.GET.get('pagesize', user_page_size))
+ if pagesize <> user_page_size:
+ pagesize_changed = True
+
+ except ValueError:
+ page = 1
+ pagesize = user_page_size
+
+ # save this pagesize to user database
+ if pagesize_changed:
+ request.session["pagesize"] = pagesize
+ if request.user.is_authenticated():
+ user = request.user
+ user.questions_per_page = pagesize
+ user.save()
+
+ view_id = request.GET.get('sort', None)
+ view_dic = {"latest":"-added_at", "active":"-last_activity_at", "hottest":"-answer_count", "mostvoted":"-score" }
+ try:
+ orderby = view_dic[view_id]
+ except KeyError:
+ view_id = "latest"
+ orderby = "-added_at"
+
+ def question_search(keywords, orderby):
+ objects = Question.objects.filter(deleted=False).extra(where=['title like %s'], params=['%' + keywords + '%']).order_by(orderby)
+ # RISK - inner join queries
+ return objects.select_related();
+
+ from forum.modules import get_handler
+
+ question_search = get_handler('question_search', question_search)
+
+ objects = question_search(keywords, orderby)
+
+ objects_list = Paginator(objects, pagesize)
+ questions = objects_list.page(page)
+
+ # Get related tags from this page objects
+ related_tags = []
+ for question in questions.object_list:
+ tags = list(question.tags.all())
+ for tag in tags:
+ if tag not in related_tags:
+ related_tags.append(tag)
+
+ #if is_search is true in the context, prepend this string to soting tabs urls
+ search_uri = "?q=%s&page=%d&t=question" % ("+".join(keywords.split()), page)
+
+ return render_to_response(template_file, {
+ "questions" : questions,
+ "tab_id" : view_id,
+ "questions_count" : objects_list.count,
+ "tags" : related_tags,
+ "searchtag" : None,
+ "searchtitle" : keywords,
+ "keywords" : keywords,
+ "is_unanswered" : False,
+ "is_search": True,
+ "search_uri": search_uri,
+ "context" : {
+ 'is_paginated' : True,
+ 'pages': objects_list.num_pages,
+ 'page': page,
+ 'has_previous': questions.has_previous(),
+ 'has_next': questions.has_next(),
+ 'previous': questions.previous_page_number(),
+ 'next': questions.next_page_number(),
+ 'base_url' : request.path + '?t=question&q=%s&sort=%s&' % (keywords, view_id),
+ 'pagesize' : pagesize
+ }}, context_instance=RequestContext(request))
+
+ else:
+ raise Http404
+
+def tag(request, tag):#stub generates listing of questions tagged with a single tag
+ return questions(request, tagname=tag)
+
+def tags(request):#view showing a listing of available tags - plain list
+ stag = ""
+ is_paginated = True
+ sortby = request.GET.get('sort', 'used')
+ try:
+ page = int(request.GET.get('page', '1'))
+ except ValueError:
+ page = 1
+
+ if request.method == "GET":
+ stag = request.GET.get("q", "").strip()
+ if stag != '':
+ objects_list = Paginator(Tag.objects.filter(deleted=False).exclude(used_count=0).extra(where=['name like %s'], params=['%' + stag + '%']), DEFAULT_PAGE_SIZE)
+ else:
+ if sortby == "name":
+ objects_list = Paginator(Tag.objects.all().filter(deleted=False).exclude(used_count=0).order_by("name"), DEFAULT_PAGE_SIZE)
+ else:
+ objects_list = Paginator(Tag.objects.all().filter(deleted=False).exclude(used_count=0).order_by("-used_count"), DEFAULT_PAGE_SIZE)
+
+ try:
+ tags = objects_list.page(page)
+ except (EmptyPage, InvalidPage):
+ 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))
+
+def question(request, id):#refactor - long subroutine. display question body, answers and comments
+ """view that displays body of the question and
+ all answers to it
+ """
+ try:
+ page = int(request.GET.get('page', '1'))
+ except ValueError:
+ page = 1
+
+ view_id = request.GET.get('sort', None)
+ view_dic = {"latest":"-added_at", "oldest":"added_at", "votes":"-score" }
+ try:
+ orderby = view_dic[view_id]
+ except KeyError:
+ qsm = request.session.get('questions_sort_method',None)
+ if qsm in ('mostvoted','latest'):
+ logging.debug('loaded from session ' + qsm)
+ if qsm == 'mostvoted':
+ view_id = 'votes'
+ orderby = '-score'
+ else:
+ view_id = 'latest'
+ orderby = '-added_at'
+ else:
+ view_id = "votes"
+ orderby = "-score"
+
+ logging.debug('view_id=' + str(view_id))
+
+ question = get_object_or_404(Question, id=id)
+ try:
+ pattern = r'/%s%s%d/([\w-]+)' % (settings.FORUM_SCRIPT_ALIAS,_('question/'), question.id)
+ path_re = re.compile(pattern)
+ logging.debug(pattern)
+ logging.debug(request.path)
+ m = path_re.match(request.path)
+ if m:
+ slug = m.group(1)
+ logging.debug('have slug %s' % slug)
+ assert(slug == slugify(question.title))
+ else:
+ logging.debug('no match!')
+ except:
+ return HttpResponseRedirect(question.get_absolute_url())
+
+ if question.deleted and not auth.can_view_deleted_post(request.user, question):
+ raise Http404
+ answer_form = AnswerForm(question,request.user)
+ answers = Answer.objects.get_answers_from_question(question, request.user)
+ answers = answers.select_related(depth=1)
+
+ favorited = question.has_favorite_by_user(request.user)
+ if request.user.is_authenticated():
+ question_vote = question.votes.select_related().filter(user=request.user)
+ else:
+ question_vote = None #is this correct?
+ if question_vote is not None and question_vote.count() > 0:
+ question_vote = question_vote[0]
+
+ user_answer_votes = {}
+ for answer in answers:
+ vote = answer.get_user_vote(request.user)
+ if vote is not None and not user_answer_votes.has_key(answer.id):
+ vote_value = -1
+ if vote.is_upvote():
+ vote_value = 1
+ user_answer_votes[answer.id] = vote_value
+
+ if answers is not None:
+ answers = answers.order_by("-accepted", orderby)
+
+ filtered_answers = []
+ for answer in answers:
+ if answer.deleted == True:
+ if answer.author_id == request.user.id:
+ filtered_answers.append(answer)
+ else:
+ filtered_answers.append(answer)
+
+ objects_list = Paginator(filtered_answers, ANSWERS_PAGE_SIZE)
+ page_objects = objects_list.page(page)
+
+ #todo: merge view counts per user and per session
+ #1) view count per session
+ update_view_count = False
+ if 'question_view_times' not in request.session:
+ request.session['question_view_times'] = {}
+
+ last_seen = request.session['question_view_times'].get(question.id,None)
+ updated_when, updated_who = question.get_last_update_info()
+
+ if updated_who != request.user:
+ if last_seen:
+ if last_seen < updated_when:
+ update_view_count = True
+ else:
+ update_view_count = True
+
+ request.session['question_view_times'][question.id] = datetime.datetime.now()
+
+ if update_view_count:
+ question.view_count += 1
+ question.save()
+
+ #2) question view count per user
+ if request.user.is_authenticated():
+ try:
+ question_view = QuestionView.objects.get(who=request.user, question=question)
+ except QuestionView.DoesNotExist:
+ question_view = QuestionView(who=request.user, question=question)
+ question_view.when = datetime.datetime.now()
+ question_view.save()
+
+ return render_to_response('question.html', {
+ "question" : question,
+ "question_vote" : question_vote,
+ "question_comment_count":question.comments.count(),
+ "answer" : answer_form,
+ "answers" : page_objects.object_list,
+ "user_answer_votes": user_answer_votes,
+ "tags" : question.tags.all(),
+ "tab_id" : view_id,
+ "favorited" : favorited,
+ "similar_questions" : Question.objects.get_similar_questions(question),
+ "context" : {
+ 'is_paginated' : True,
+ 'pages': objects_list.num_pages,
+ 'page': page,
+ 'has_previous': page_objects.has_previous(),
+ 'has_next': page_objects.has_next(),
+ 'previous': page_objects.previous_page_number(),
+ 'next': page_objects.next_page_number(),
+ 'base_url' : request.path + '?sort=%s&' % view_id,
+ 'extend_url' : "#sort-top"
+ }
+ }, context_instance=RequestContext(request))
+
+QUESTION_REVISION_TEMPLATE = ('
%(title)s
\n'
+ '
%(html)s
\n'
+ '
%(tags)s
')
+def question_revisions(request, id):
+ post = get_object_or_404(Question, id=id)
+ revisions = list(post.revisions.all())
+ revisions.reverse()
+ for i, revision in enumerate(revisions):
+ revision.html = QUESTION_REVISION_TEMPLATE % {
+ 'title': revision.title,
+ 'html': sanitize_html(markdowner.convert(revision.text)),
+ 'tags': ' '.join(['%s' % tag
+ for tag in revision.tagnames.split(' ')]),
+ }
+ if i > 0:
+ revisions[i].diff = htmldiff(revisions[i-1].html, revision.html)
+ else:
+ revisions[i].diff = QUESTION_REVISION_TEMPLATE % {
+ 'title': revisions[0].title,
+ 'html': sanitize_html(markdowner.convert(revisions[0].text)),
+ 'tags': ' '.join(['%s' % tag
+ 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))
+
+ANSWER_REVISION_TEMPLATE = ('
%(html)s
')
+def answer_revisions(request, id):
+ post = get_object_or_404(Answer, id=id)
+ revisions = list(post.revisions.all())
+ revisions.reverse()
+ for i, revision in enumerate(revisions):
+ revision.html = ANSWER_REVISION_TEMPLATE % {
+ 'html': sanitize_html(markdowner.convert(revision.text))
+ }
+ if i > 0:
+ revisions[i].diff = htmldiff(revisions[i-1].html, revision.html)
+ else:
+ 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))
+
diff --git a/forum/views/users.py b/forum/views/users.py
new file mode 100644
index 0000000..baa8090
--- /dev/null
+++ b/forum/views/users.py
@@ -0,0 +1,1009 @@
+from django.contrib.auth.decorators import login_required
+from django.contrib.auth.models import User
+from django.core.paginator import Paginator, EmptyPage, InvalidPage
+from django.template.defaultfilters import slugify
+from django.contrib.contenttypes.models import ContentType
+from django.core.urlresolvers import reverse
+from django.shortcuts import render_to_response, get_object_or_404
+from django.template import RequestContext
+from django.http import HttpResponse, HttpResponseForbidden, HttpResponseRedirect, Http404
+from django.utils.translation import ugettext as _
+from django.utils.http import urlquote_plus
+from django.utils.html import strip_tags
+from django.core.urlresolvers import reverse
+from forum.forms import *#incomplete list is EditUserForm, ModerateUserForm, TagFilterSelectionForm,
+from forum.utils.html import sanitize_html
+from forum import auth
+import calendar
+from django.contrib.contenttypes.models import ContentType
+
+question_type = ContentType.objects.get_for_model(Question)
+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)
+question_type_id = question_type.id
+answer_type_id = answer_type.id
+comment_type_id = comment_type.id
+question_revision_type_id = question_revision_type.id
+answer_revision_type_id = answer_revision_type.id
+repute_type_id = repute_type.id
+
+USERS_PAGE_SIZE = 35# refactor - move to some constants file
+
+def users(request):
+ is_paginated = True
+ sortby = request.GET.get('sort', 'reputation')
+ suser = request.REQUEST.get('q', "")
+ try:
+ page = int(request.GET.get('page', '1'))
+ except ValueError:
+ page = 1
+
+ if suser == "":
+ if sortby == "newest":
+ objects_list = Paginator(User.objects.all().order_by('-date_joined'), USERS_PAGE_SIZE)
+ elif sortby == "last":
+ objects_list = Paginator(User.objects.all().order_by('date_joined'), USERS_PAGE_SIZE)
+ elif sortby == "user":
+ objects_list = Paginator(User.objects.all().order_by('username'), USERS_PAGE_SIZE)
+ # default
+ else:
+ objects_list = Paginator(User.objects.all().order_by('-reputation'), USERS_PAGE_SIZE)
+ base_url = reverse('users') + '?sort=%s&' % sortby
+ else:
+ sortby = "reputation"
+ objects_list = Paginator(User.objects.extra(where=['username like %s'], params=['%' + suser + '%']).order_by('-reputation'), USERS_PAGE_SIZE)
+ base_url = reverse('users') + '?name=%s&sort=%s&' % (suser, sortby)
+
+ try:
+ users = objects_list.page(page)
+ except (EmptyPage, InvalidPage):
+ 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))
+
+@login_required
+def moderate_user(request, id):
+ """ajax handler of user moderation
+ """
+ if not auth.can_moderate_users(request.user) or request.method != 'POST':
+ raise Http404
+ if not request.is_ajax():
+ return HttpResponseForbidden(mimetype="application/json")
+
+ user = get_object_or_404(User, id=id)
+ form = ModerateUserForm(request.POST, instance=user)
+
+ if form.is_valid():
+ form.save()
+ logging.debug('data saved')
+ response = HttpResponse(simplejson.dumps(''), mimetype="application/json")
+ else:
+ response = HttpResponseForbidden(mimetype="application/json")
+ return response
+
+def set_new_email(user, new_email, nomessage=False):
+ if new_email != user.email:
+ user.email = new_email
+ user.email_isvalid = False
+ user.save()
+ #if settings.EMAIL_VALIDATION == 'on':
+ # send_new_email_key(user,nomessage=nomessage)
+
+@login_required
+def edit_user(request, id):
+ user = get_object_or_404(User, id=id)
+ if request.user != user:
+ raise Http404
+ if request.method == "POST":
+ form = EditUserForm(user, request.POST)
+ if form.is_valid():
+ new_email = sanitize_html(form.cleaned_data['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'])
+ user.date_of_birth = sanitize_html(form.cleaned_data['birthday'])
+ if len(user.date_of_birth) == 0:
+ user.date_of_birth = '1900-01-01'
+ user.about = sanitize_html(form.cleaned_data['about'])
+
+ user.save()
+ # send user updated singal if full fields have been updated
+ if user.email and user.real_name and user.website and user.location and \
+ user.date_of_birth and user.about:
+ user_updated.send(sender=user.__class__, instance=user, updated_by=user)
+ return HttpResponseRedirect(user.get_profile_url())
+ else:
+ form = EditUserForm(user)
+ return render_to_response('user_edit.html', {
+ 'form' : form,
+ 'gravatar_faq_url' : reverse('faq') + '#gravatar',
+ }, context_instance=RequestContext(request))
+
+def user_stats(request, user_id, user_view):
+ user = get_object_or_404(User, id=user_id)
+ questions = Question.objects.extra(
+ select={
+ 'vote_count' : 'question.score',
+ 'favorited_myself' : 'SELECT count(*) FROM favorite_question f WHERE f.user_id = %s AND f.question_id = question.id',
+ 'la_user_id' : 'auth_user.id',
+ 'la_username' : 'auth_user.username',
+ 'la_user_gold' : 'auth_user.gold',
+ 'la_user_silver' : 'auth_user.silver',
+ 'la_user_bronze' : 'auth_user.bronze',
+ 'la_user_reputation' : 'auth_user.reputation'
+ },
+ select_params=[user_id],
+ tables=['question', 'auth_user'],
+ where=['question.deleted=False AND question.author_id=%s AND question.last_activity_by_id = auth_user.id'],
+ params=[user_id],
+ order_by=['-vote_count', '-last_activity_at']
+ ).values('vote_count',
+ 'favorited_myself',
+ 'id',
+ 'title',
+ 'author_id',
+ 'added_at',
+ 'answer_accepted',
+ 'answer_count',
+ 'comment_count',
+ 'view_count',
+ 'favourite_count',
+ 'summary',
+ 'tagnames',
+ 'vote_up_count',
+ 'vote_down_count',
+ 'last_activity_at',
+ 'la_user_id',
+ 'la_username',
+ 'la_user_gold',
+ 'la_user_silver',
+ 'la_user_bronze',
+ 'la_user_reputation')[:100]
+
+ answered_questions = Question.objects.extra(
+ select={
+ 'vote_up_count' : 'answer.vote_up_count',
+ 'vote_down_count' : 'answer.vote_down_count',
+ 'answer_id' : 'answer.id',
+ 'accepted' : 'answer.accepted',
+ 'vote_count' : 'answer.score',
+ 'comment_count' : 'answer.comment_count'
+ },
+ tables=['question', 'answer'],
+ where=['answer.deleted=False AND question.deleted=False AND answer.author_id=%s AND answer.question_id=question.id'],
+ params=[user_id],
+ order_by=['-vote_count', '-answer_id'],
+ select_params=[user_id]
+ ).distinct().values('comment_count',
+ 'id',
+ 'answer_id',
+ 'title',
+ 'author_id',
+ 'accepted',
+ 'vote_count',
+ 'answer_count',
+ 'vote_up_count',
+ 'vote_down_count')[:100]
+
+ up_votes = Vote.objects.get_up_vote_count_from_user(user)
+ down_votes = Vote.objects.get_down_vote_count_from_user(user)
+ votes_today = Vote.objects.get_votes_count_today_from_user(user)
+ votes_total = auth.VOTE_RULES['scope_votes_per_user_per_day']
+
+ question_id_set = set(map(lambda v: v['id'], list(questions))) \
+ | set(map(lambda v: v['id'], list(answered_questions)))
+
+ user_tags = Tag.objects.filter(questions__id__in = question_id_set)
+ try:
+ from django.db.models import Count
+ awards = Award.objects.extra(
+ select={'id': 'badge.id',
+ 'name':'badge.name',
+ 'description': 'badge.description',
+ 'type': 'badge.type'},
+ tables=['award', 'badge'],
+ order_by=['-awarded_at'],
+ where=['user_id=%s AND badge_id=badge.id'],
+ params=[user.id]
+ ).values('id', 'name', 'description', 'type')
+ total_awards = awards.count()
+ awards = awards.annotate(count = Count('badge__id'))
+ user_tags = user_tags.annotate(user_tag_usage_count=Count('name'))
+
+ except ImportError:
+ awards = Award.objects.extra(
+ select={'id': 'badge.id',
+ 'count': 'count(badge_id)',
+ 'name':'badge.name',
+ 'description': 'badge.description',
+ 'type': 'badge.type'},
+ tables=['award', 'badge'],
+ order_by=['-awarded_at'],
+ where=['user_id=%s AND badge_id=badge.id'],
+ params=[user.id]
+ ).values('id', 'count', 'name', 'description', 'type')
+ total_awards = awards.count()
+ awards.query.group_by = ['badge_id']
+
+ user_tags = user_tags.extra(
+ select={'user_tag_usage_count': 'COUNT(1)',},
+ order_by=['-user_tag_usage_count'],
+ )
+ user_tags.query.group_by = ['name']
+
+ if auth.can_moderate_users(request.user):
+ moderate_user_form = ModerateUserForm(instance=user)
+ else:
+ moderate_user_form = None
+
+ return render_to_response(user_view.template_file,{
+ 'moderate_user_form': moderate_user_form,
+ "tab_name" : user_view.id,
+ "tab_description" : user_view.tab_description,
+ "page_title" : user_view.page_title,
+ "view_user" : user,
+ "questions" : questions,
+ "answered_questions" : answered_questions,
+ "up_votes" : up_votes,
+ "down_votes" : down_votes,
+ "total_votes": up_votes + down_votes,
+ "votes_today_left": votes_total-votes_today,
+ "votes_total_per_day": votes_total,
+ "user_tags" : user_tags[:50],
+ "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)
+ def get_type_name(type_id):
+ for item in TYPE_ACTIVITY:
+ if type_id in item:
+ return item[1]
+
+ class Event:
+ def __init__(self, time, type, title, summary, answer_id, question_id):
+ self.time = time
+ self.type = get_type_name(type)
+ self.type_id = type
+ self.title = title
+ self.summary = summary
+ slug_title = slugify(title)
+ self.title_link = reverse('question', kwargs={'id':question_id}) + u'%s' % slug_title
+ if int(answer_id) > 0:
+ self.title_link += '#%s' % answer_id
+
+ class AwardEvent:
+ def __init__(self, time, type, id):
+ self.time = time
+ self.type = get_type_name(type)
+ self.type_id = type
+ self.badge = get_object_or_404(Badge, id=id)
+
+ activities = []
+ # ask questions
+ questions = Activity.objects.extra(
+ select={
+ 'title' : 'question.title',
+ 'question_id' : 'question.id',
+ 'active_at' : 'activity.active_at',
+ 'activity_type' : 'activity.activity_type'
+ },
+ tables=['activity', 'question'],
+ where=['activity.content_type_id = %s AND activity.object_id = ' +
+ 'question.id AND question.deleted=False AND activity.user_id = %s AND activity.activity_type = %s'],
+ params=[question_type_id, user_id, TYPE_ACTIVITY_ASK_QUESTION],
+ order_by=['-activity.active_at']
+ ).values(
+ 'title',
+ 'question_id',
+ 'active_at',
+ 'activity_type'
+ )
+ if len(questions) > 0:
+ questions = [(Event(q['active_at'], q['activity_type'], q['title'], '', '0', \
+ q['question_id'])) for q in questions]
+ activities.extend(questions)
+
+ # answers
+ answers = Activity.objects.extra(
+ select={
+ 'title' : 'question.title',
+ 'question_id' : 'question.id',
+ 'answer_id' : 'answer.id',
+ 'active_at' : 'activity.active_at',
+ 'activity_type' : 'activity.activity_type'
+ },
+ tables=['activity', 'answer', 'question'],
+ where=['activity.content_type_id = %s AND activity.object_id = answer.id AND ' +
+ 'answer.question_id=question.id AND answer.deleted=False AND activity.user_id=%s AND '+
+ 'activity.activity_type=%s AND question.deleted=False'],
+ params=[answer_type_id, user_id, TYPE_ACTIVITY_ANSWER],
+ order_by=['-activity.active_at']
+ ).values(
+ 'title',
+ 'question_id',
+ 'answer_id',
+ 'active_at',
+ 'activity_type'
+ )
+ if len(answers) > 0:
+ answers = [(Event(q['active_at'], q['activity_type'], q['title'], '', q['answer_id'], \
+ q['question_id'])) for q in answers]
+ activities.extend(answers)
+
+ # question comments
+ comments = Activity.objects.extra(
+ select={
+ 'title' : 'question.title',
+ 'question_id' : 'comment.object_id',
+ 'added_at' : 'comment.added_at',
+ 'activity_type' : 'activity.activity_type'
+ },
+ tables=['activity', 'question', 'comment'],
+
+ where=['activity.content_type_id = %s AND activity.object_id = comment.id AND '+
+ 'activity.user_id = comment.user_id AND comment.object_id=question.id AND '+
+ 'comment.content_type_id=%s AND activity.user_id = %s AND activity.activity_type=%s AND ' +
+ 'question.deleted=False'],
+ params=[comment_type_id, question_type_id, user_id, TYPE_ACTIVITY_COMMENT_QUESTION],
+ order_by=['-comment.added_at']
+ ).values(
+ 'title',
+ 'question_id',
+ 'added_at',
+ 'activity_type'
+ )
+
+ if len(comments) > 0:
+ comments = [(Event(q['added_at'], q['activity_type'], q['title'], '', '0', \
+ q['question_id'])) for q in comments]
+ activities.extend(comments)
+
+ # answer comments
+ comments = Activity.objects.extra(
+ select={
+ 'title' : 'question.title',
+ 'question_id' : 'question.id',
+ 'answer_id' : 'answer.id',
+ 'added_at' : 'comment.added_at',
+ 'activity_type' : 'activity.activity_type'
+ },
+ tables=['activity', 'question', 'answer', 'comment'],
+
+ where=['activity.content_type_id = %s AND activity.object_id = comment.id AND '+
+ 'activity.user_id = comment.user_id AND comment.object_id=answer.id AND '+
+ 'comment.content_type_id=%s AND question.id = answer.question_id AND '+
+ 'activity.user_id = %s AND activity.activity_type=%s AND '+
+ 'answer.deleted=False AND question.deleted=False'],
+ params=[comment_type_id, answer_type_id, user_id, TYPE_ACTIVITY_COMMENT_ANSWER],
+ order_by=['-comment.added_at']
+ ).values(
+ 'title',
+ 'question_id',
+ 'answer_id',
+ 'added_at',
+ 'activity_type'
+ )
+
+ if len(comments) > 0:
+ comments = [(Event(q['added_at'], q['activity_type'], q['title'], '', q['answer_id'], \
+ q['question_id'])) for q in comments]
+ activities.extend(comments)
+
+ # question revisions
+ revisions = Activity.objects.extra(
+ select={
+ 'title' : 'question_revision.title',
+ 'question_id' : 'question_revision.question_id',
+ 'added_at' : 'activity.active_at',
+ 'activity_type' : 'activity.activity_type',
+ 'summary' : 'question_revision.summary'
+ },
+ tables=['activity', 'question_revision', 'question'],
+ where=['activity.content_type_id = %s AND activity.object_id = question_revision.id AND '+
+ 'question_revision.id=question.id AND question.deleted=False AND '+
+ 'activity.user_id = question_revision.author_id AND activity.user_id = %s AND '+
+ 'activity.activity_type=%s'],
+ params=[question_revision_type_id, user_id, TYPE_ACTIVITY_UPDATE_QUESTION],
+ order_by=['-activity.active_at']
+ ).values(
+ 'title',
+ 'question_id',
+ 'added_at',
+ 'activity_type',
+ 'summary'
+ )
+
+ if len(revisions) > 0:
+ revisions = [(Event(q['added_at'], q['activity_type'], q['title'], q['summary'], '0', \
+ q['question_id'])) for q in revisions]
+ activities.extend(revisions)
+
+ # answer revisions
+ revisions = Activity.objects.extra(
+ select={
+ 'title' : 'question.title',
+ 'question_id' : 'question.id',
+ 'answer_id' : 'answer.id',
+ 'added_at' : 'activity.active_at',
+ 'activity_type' : 'activity.activity_type',
+ 'summary' : 'answer_revision.summary'
+ },
+ tables=['activity', 'answer_revision', 'question', 'answer'],
+
+ where=['activity.content_type_id = %s AND activity.object_id = answer_revision.id AND '+
+ 'activity.user_id = answer_revision.author_id AND activity.user_id = %s AND '+
+ 'answer_revision.answer_id=answer.id AND answer.question_id = question.id AND '+
+ 'question.deleted=False AND answer.deleted=False AND '+
+ 'activity.activity_type=%s'],
+ params=[answer_revision_type_id, user_id, TYPE_ACTIVITY_UPDATE_ANSWER],
+ order_by=['-activity.active_at']
+ ).values(
+ 'title',
+ 'question_id',
+ 'added_at',
+ 'answer_id',
+ 'activity_type',
+ 'summary'
+ )
+
+ if len(revisions) > 0:
+ revisions = [(Event(q['added_at'], q['activity_type'], q['title'], q['summary'], \
+ q['answer_id'], q['question_id'])) for q in revisions]
+ activities.extend(revisions)
+
+ # accepted answers
+ accept_answers = Activity.objects.extra(
+ select={
+ 'title' : 'question.title',
+ 'question_id' : 'question.id',
+ 'added_at' : 'activity.active_at',
+ 'activity_type' : 'activity.activity_type',
+ },
+ tables=['activity', 'answer', 'question'],
+ where=['activity.content_type_id = %s AND activity.object_id = answer.id AND '+
+ 'activity.user_id = question.author_id AND activity.user_id = %s AND '+
+ 'answer.deleted=False AND question.deleted=False AND '+
+ 'answer.question_id=question.id AND activity.activity_type=%s'],
+ params=[answer_type_id, user_id, TYPE_ACTIVITY_MARK_ANSWER],
+ order_by=['-activity.active_at']
+ ).values(
+ 'title',
+ 'question_id',
+ 'added_at',
+ 'activity_type',
+ )
+ if len(accept_answers) > 0:
+ accept_answers = [(Event(q['added_at'], q['activity_type'], q['title'], '', '0', \
+ q['question_id'])) for q in accept_answers]
+ activities.extend(accept_answers)
+ #award history
+ awards = Activity.objects.extra(
+ select={
+ 'badge_id' : 'badge.id',
+ 'awarded_at': 'award.awarded_at',
+ 'activity_type' : 'activity.activity_type'
+ },
+ tables=['activity', 'award', 'badge'],
+ where=['activity.user_id = award.user_id AND activity.user_id = %s AND '+
+ 'award.badge_id=badge.id AND activity.object_id=award.id AND activity.activity_type=%s'],
+ params=[user_id, TYPE_ACTIVITY_PRIZE],
+ order_by=['-activity.active_at']
+ ).values(
+ 'badge_id',
+ 'awarded_at',
+ 'activity_type'
+ )
+ if len(awards) > 0:
+ awards = [(AwardEvent(q['awarded_at'], q['activity_type'], q['badge_id'])) for q in awards]
+ activities.extend(awards)
+
+ 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))
+
+def user_responses(request, user_id, user_view):
+ """
+ We list answers for question, comments, and answer accepted by others for this user.
+ """
+ class Response:
+ def __init__(self, type, title, question_id, answer_id, time, username, user_id, content):
+ self.type = type
+ self.title = title
+ 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
+ self.content = u'%s ...' % strip_tags(content)[:300]
+
+ def __unicode__(self):
+ return u'%s %s' % (self.type, self.titlelink)
+
+ 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=False AND question.deleted=False 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]
+ responses.extend(answers)
+
+
+ # 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=False 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'],
+ '', c['added_at'], c['username'], c['user_id'], c['comment'])) for c in comments]
+ responses.extend(comments)
+
+ # answer comments
+ comments = Comment.objects.extra(
+ select={
+ 'title' : 'question.title',
+ 'question_id' : 'question.id',
+ 'answer_id' : 'answer.id',
+ 'added_at' : 'comment.added_at',
+ 'comment' : 'comment.comment',
+ 'username' : 'auth_user.username',
+ 'user_id' : 'auth_user.id'
+ },
+ tables=['answer', 'auth_user', 'comment', 'question'],
+ where=['answer.deleted=False AND answer.author_id = %s AND comment.object_id=answer.id AND '+
+ 'comment.content_type_id=%s AND comment.user_id <> %s AND comment.user_id = auth_user.id '+
+ 'AND question.id = answer.question_id'],
+ params=[user_id, answer_type_id, user_id],
+ order_by=['-comment.added_at']
+ ).values(
+ 'title',
+ 'question_id',
+ 'answer_id',
+ 'added_at',
+ 'comment',
+ 'username',
+ 'user_id'
+ )
+
+ if len(comments) > 0:
+ comments = [(Response(TYPE_RESPONSE['ANSWER_COMMENTED'], c['title'], c['question_id'],
+ c['answer_id'], c['added_at'], c['username'], c['user_id'], c['comment'])) for c in comments]
+ responses.extend(comments)
+
+ # answer has been accepted
+ answers = Answer.objects.extra(
+ select={
+ 'title' : 'question.title',
+ 'question_id' : 'question.id',
+ 'answer_id' : 'answer.id',
+ 'added_at' : 'answer.accepted_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=False AND question.deleted=False AND '+
+ 'answer.author_id = %s AND answer.accepted=True AND question.author_id=auth_user.id'],
+ params=[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['ANSWER_ACCEPTED'], a['title'], a['question_id'],
+ a['answer_id'], a['added_at'], a['username'], a['user_id'], a['html'])) for a in answers]
+ responses.extend(answers)
+
+ # sort posts by time
+ responses.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,
+ "responses" : responses[:user_view.data_size],
+
+ }, context_instance=RequestContext(request))
+
+def user_votes(request, user_id, user_view):
+ user = get_object_or_404(User, id=user_id)
+ if not auth.can_view_user_votes(request.user, user):
+ raise Http404
+ votes = []
+ question_votes = Vote.objects.extra(
+ select={
+ 'title' : 'question.title',
+ 'question_id' : 'question.id',
+ 'answer_id' : 0,
+ 'voted_at' : 'vote.voted_at',
+ 'vote' : 'vote',
+ },
+ select_params=[user_id],
+ tables=['vote', 'question', 'auth_user'],
+ where=['vote.content_type_id = %s AND vote.user_id = %s AND vote.object_id = question.id '+
+ 'AND vote.user_id=auth_user.id'],
+ params=[question_type_id, user_id],
+ order_by=['-vote.id']
+ ).values(
+ 'title',
+ 'question_id',
+ 'answer_id',
+ 'voted_at',
+ 'vote',
+ )
+ if(len(question_votes) > 0):
+ votes.extend(question_votes)
+
+ answer_votes = Vote.objects.extra(
+ select={
+ 'title' : 'question.title',
+ 'question_id' : 'question.id',
+ 'answer_id' : 'answer.id',
+ 'voted_at' : 'vote.voted_at',
+ 'vote' : 'vote',
+ },
+ select_params=[user_id],
+ tables=['vote', 'answer', 'question', 'auth_user'],
+ where=['vote.content_type_id = %s AND vote.user_id = %s AND vote.object_id = answer.id '+
+ 'AND answer.question_id = question.id AND vote.user_id=auth_user.id'],
+ params=[answer_type_id, user_id],
+ order_by=['-vote.id']
+ ).values(
+ 'title',
+ 'question_id',
+ 'answer_id',
+ 'voted_at',
+ 'vote',
+ )
+ if(len(answer_votes) > 0):
+ votes.extend(answer_votes)
+ votes.sort(lambda x,y: cmp(y['voted_at'], x['voted_at']))
+ 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,
+ "votes" : votes[:user_view.data_size]
+
+ }, context_instance=RequestContext(request))
+
+def user_reputation(request, user_id, user_view):
+ user = get_object_or_404(User, id=user_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'):
+ dic = '[%s,%s]' % (calendar.timegm(rep.reputed_at.timetuple()) * 1000, rep.reputation)
+ rep_list.append(dic)
+ 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))
+
+def user_favorites(request, user_id, user_view):
+ user = get_object_or_404(User, id=user_id)
+ questions = Question.objects.extra(
+ select={
+ 'vote_count' : 'question.vote_up_count + question.vote_down_count',
+ 'favorited_myself' : 'SELECT count(*) FROM favorite_question f WHERE f.user_id = %s '+
+ 'AND f.question_id = question.id',
+ 'la_user_id' : 'auth_user.id',
+ 'la_username' : 'auth_user.username',
+ 'la_user_gold' : 'auth_user.gold',
+ 'la_user_silver' : 'auth_user.silver',
+ 'la_user_bronze' : 'auth_user.bronze',
+ 'la_user_reputation' : 'auth_user.reputation'
+ },
+ select_params=[user_id],
+ tables=['question', 'auth_user', 'favorite_question'],
+ where=['question.deleted=True AND question.last_activity_by_id = auth_user.id '+
+ 'AND favorite_question.question_id = question.id AND favorite_question.user_id = %s'],
+ params=[user_id],
+ order_by=['-vote_count', '-question.id']
+ ).values('vote_count',
+ 'favorited_myself',
+ 'id',
+ 'title',
+ 'author_id',
+ 'added_at',
+ 'answer_accepted',
+ 'answer_count',
+ 'comment_count',
+ 'view_count',
+ 'favourite_count',
+ 'summary',
+ 'tagnames',
+ 'vote_up_count',
+ 'vote_down_count',
+ 'last_activity_at',
+ 'la_user_id',
+ 'la_username',
+ 'la_user_gold',
+ 'la_user_silver',
+ 'la_user_bronze',
+ 'la_user_reputation')
+ 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,
+ "questions" : questions[:user_view.data_size],
+ "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':
+ email_feeds_form = EditUserEmailFeedsForm(request.POST)
+ tag_filter_form = TagFilterSelectionForm(request.POST, instance=user)
+ if email_feeds_form.is_valid() and tag_filter_form.is_valid():
+
+ action_status = None
+ tag_filter_saved = tag_filter_form.save()
+ if tag_filter_saved:
+ action_status = _('changes saved')
+ if 'save' in request.POST:
+ feeds_saved = email_feeds_form.save(user)
+ if feeds_saved:
+ action_status = _('changes saved')
+ elif 'stop_email' in request.POST:
+ email_stopped = email_feeds_form.reset().save(user)
+ initial_values = EditUserEmailFeedsForm.NO_EMAIL_INITIAL
+ email_feeds_form = EditUserEmailFeedsForm(initial=initial_values)
+ if email_stopped:
+ action_status = _('email updates canceled')
+ else:
+ email_feeds_form = EditUserEmailFeedsForm()
+ email_feeds_form.set_initial_values(user)
+ tag_filter_form = TagFilterSelectionForm(instance=user)
+ action_status = None
+ return render_to_response(user_view.template_file,{
+ 'tab_name':user_view.id,
+ 'tab_description':user_view.tab_description,
+ 'page_title':user_view.page_title,
+ 'view_user':user,
+ 'email_feeds_form':email_feeds_form,
+ 'tag_filter_selection_form':tag_filter_form,
+ 'action_status':action_status,
+ }, context_instance=RequestContext(request))
+
+class UserView:
+ def __init__(self, id, tab_title, tab_description, page_title, view_func, template_file, data_size=0):
+ self.id = id
+ self.tab_title = tab_title
+ self.tab_description = tab_description
+ self.page_title = page_title
+ self.view_func = view_func
+ self.template_file = template_file
+ self.data_size = data_size
+
+USER_TEMPLATE_VIEWS = (
+ UserView(
+ id = 'stats',
+ tab_title = _('overview'),
+ tab_description = _('user profile'),
+ page_title = _('user profile overview'),
+ view_func = user_stats,
+ template_file = 'user_stats.html'
+ ),
+ UserView(
+ id = 'recent',
+ tab_title = _('recent activity'),
+ tab_description = _('recent user activity'),
+ page_title = _('profile - recent activity'),
+ view_func = user_recent,
+ template_file = 'user_recent.html',
+ data_size = 50
+ ),
+ UserView(
+ id = 'responses',
+ tab_title = _('responses'),
+ tab_description = _('comments and answers to others questions'),
+ page_title = _('profile - responses'),
+ view_func = user_responses,
+ template_file = 'user_responses.html',
+ data_size = 50
+ ),
+ UserView(
+ id = 'reputation',
+ tab_title = _('reputation'),
+ tab_description = _('user reputation in the community'),
+ page_title = _('profile - user reputation'),
+ view_func = user_reputation,
+ template_file = 'user_reputation.html'
+ ),
+ UserView(
+ id = 'favorites',
+ tab_title = _('favorite questions'),
+ tab_description = _('users favorite questions'),
+ page_title = _('profile - favorite questions'),
+ view_func = user_favorites,
+ template_file = 'user_favorites.html',
+ data_size = 50
+ ),
+ UserView(
+ id = 'votes',
+ tab_title = _('casted votes'),
+ tab_description = _('user vote record'),
+ page_title = _('profile - votes'),
+ view_func = user_votes,
+ template_file = 'user_votes.html',
+ data_size = 50
+ ),
+ UserView(
+ id = 'email_subscriptions',
+ tab_title = _('email subscriptions'),
+ tab_description = _('email subscription settings'),
+ page_title = _('profile - email subscriptions'),
+ view_func = user_email_subscriptions,
+ template_file = 'user_email_subscriptions.html'
+ )
+)
+
+def user(request, id):
+ sort = request.GET.get('sort', 'stats')
+ user_view = dict((v.id, v) for v in USER_TEMPLATE_VIEWS).get(sort, USER_TEMPLATE_VIEWS[0])
+ from forum.views import users
+ func = user_view.view_func
+ return func(request, id, user_view)
+
+
+@login_required
+def changepw(request):
+ """
+ change password view.
+
+ url : /changepw/
+ template: authopenid/changepw.html
+ """
+ logging.debug('')
+ user_ = request.user
+
+ if not user_.has_usable_password():
+ raise Http404
+
+ if request.POST:
+ form = ChangePasswordForm(request.POST, user=user_)
+ if form.is_valid():
+ user_.set_password(form.cleaned_data['password1'])
+ user_.save()
+ msg = _("Password changed.")
+ redirect = "%s?msg=%s" % (
+ reverse('user_account_settings'),
+ urlquote_plus(msg))
+ return HttpResponseRedirect(redirect)
+ else:
+ form = ChangePasswordForm(user=user_)
+
+ return render_to_response('changepw.html', {'form': form },
+ context_instance=RequestContext(request))
+
+@login_required
+def account_settings(request):
+ """
+ index pages to changes some basic account settings :
+ - change password
+ - change email
+ - associate a new openid
+ - delete account
+
+ url : /
+
+ template : authopenid/settings.html
+ """
+ logging.debug('')
+ msg = request.GET.get('msg', '')
+ is_openid = False
+
+ return render_to_response('account_settings.html', {
+ 'msg': msg,
+ 'is_openid': is_openid
+ }, context_instance=RequestContext(request))
+
diff --git a/forum/views/writers.py b/forum/views/writers.py
new file mode 100644
index 0000000..2b2461d
--- /dev/null
+++ b/forum/views/writers.py
@@ -0,0 +1,442 @@
+# encoding:utf-8
+import os.path
+import time, datetime, random
+import logging
+from django.core.files.storage import default_storage
+from django.shortcuts import render_to_response, get_object_or_404
+from django.contrib.auth.decorators import login_required
+from django.http import HttpResponseRedirect, HttpResponse, HttpResponseForbidden, Http404
+from django.template import RequestContext
+from django.utils.html import *
+from django.utils import simplejson
+from django.utils.translation import ugettext as _
+from django.core.urlresolvers import reverse
+from django.core.exceptions import PermissionDenied
+
+from forum.utils.html import sanitize_html
+from markdown2 import Markdown
+from forum.forms import *
+from forum.models import *
+from forum.auth import *
+from forum.const import *
+from forum import auth
+from forum.utils.forms import get_next_url
+from forum.views.readers import _get_tags_cache_json
+
+# used in index page
+INDEX_PAGE_SIZE = 20
+INDEX_AWARD_SIZE = 15
+INDEX_TAGS_SIZE = 100
+# used in tags list
+DEFAULT_PAGE_SIZE = 60
+# used in questions
+QUESTIONS_PAGE_SIZE = 10
+# used in answers
+ANSWERS_PAGE_SIZE = 10
+
+markdowner = Markdown(html4tags=True)
+
+def upload(request):#ajax upload file to a question or answer
+ class FileTypeNotAllow(Exception):
+ pass
+ class FileSizeNotAllow(Exception):
+ pass
+ class UploadPermissionNotAuthorized(Exception):
+ pass
+
+ #%s
+ xml_template = "%s"
+
+ try:
+ f = request.FILES['file-upload']
+ # check upload permission
+ if not auth.can_upload_files(request.user):
+ raise UploadPermissionNotAuthorized
+
+ # check file type
+ file_name_suffix = os.path.splitext(f.name)[1].lower()
+ if not file_name_suffix in settings.ALLOW_FILE_TYPES:
+ raise FileTypeNotAllow
+
+ # generate new file name
+ new_file_name = str(time.time()).replace('.', str(random.randint(0,100000))) + file_name_suffix
+ # use default storage to store file
+ default_storage.save(new_file_name, f)
+ # check file size
+ # byte
+ size = default_storage.size(new_file_name)
+ if size > settings.ALLOW_MAX_FILE_SIZE:
+ default_storage.delete(new_file_name)
+ raise FileSizeNotAllow
+
+ result = xml_template % ('Good', '', default_storage.url(new_file_name))
+ except UploadPermissionNotAuthorized:
+ result = xml_template % ('', _('uploading images is limited to users with >60 reputation points'), '')
+ except FileTypeNotAllow:
+ result = xml_template % ('', _("allowed file types are 'jpg', 'jpeg', 'gif', 'bmp', 'png', 'tiff'"), '')
+ except FileSizeNotAllow:
+ result = xml_template % ('', _("maximum upload file size is %sK") % settings.ALLOW_MAX_FILE_SIZE / 1024, '')
+ except Exception:
+ result = xml_template % ('', _('Error uploading file. Please contact the site administrator. Thank you. %s' % Exception), '')
+
+ return HttpResponse(result, mimetype="application/xml")
+
+#@login_required #actually you can post anonymously, but then must register
+def ask(request):#view used to ask a new question
+ """a view to ask a new question
+ gives space for q title, body, tags and checkbox for to post as wiki
+
+ user can start posting a question anonymously but then
+ must login/register in order for the question go be shown
+ """
+ if request.method == "POST":
+ form = AskForm(request.POST)
+ if form.is_valid():
+
+ added_at = datetime.datetime.now()
+ title = strip_tags(form.cleaned_data['title'].strip())
+ wiki = form.cleaned_data['wiki']
+ tagnames = form.cleaned_data['tags'].strip()
+ text = form.cleaned_data['text']
+ html = sanitize_html(markdowner.convert(text))
+ summary = strip_tags(html)[:120]
+
+ if request.user.is_authenticated():
+ author = request.user
+
+ question = Question.objects.create_new(
+ title = title,
+ author = author,
+ added_at = added_at,
+ wiki = wiki,
+ tagnames = tagnames,
+ summary = summary,
+ text = sanitize_html(markdowner.convert(text))
+ )
+
+ return HttpResponseRedirect(question.get_absolute_url())
+ else:
+ request.session.flush()
+ session_key = request.session.session_key
+ question = AnonymousQuestion(
+ session_key = session_key,
+ title = title,
+ tagnames = tagnames,
+ wiki = wiki,
+ text = text,
+ summary = summary,
+ added_at = added_at,
+ ip_addr = request.META['REMOTE_ADDR'],
+ )
+ question.save()
+ return HttpResponseRedirect(reverse('auth_action_signin', kwargs={'action': 'newquestion'}))
+ else:
+ form = AskForm()
+
+ tags = _get_tags_cache_json()
+ return render_to_response('ask.html', {
+ 'form' : form,
+ 'tags' : tags,
+ 'email_validation_faq_url':reverse('faq') + '#validate',
+ }, context_instance=RequestContext(request))
+
+@login_required
+def edit_question(request, id):#edit or retag a question
+ """view to edit question
+ """
+ question = get_object_or_404(Question, id=id)
+ if question.deleted and not auth.can_view_deleted_post(request.user, question):
+ raise Http404
+ if auth.can_edit_post(request.user, question):
+ return _edit_question(request, question)
+ elif auth.can_retag_questions(request.user):
+ return _retag_question(request, question)
+ else:
+ raise Http404
+
+def _retag_question(request, question):#non-url subview of edit question - just retag
+ """retag question sub-view used by
+ view "edit_question"
+ """
+ if request.method == 'POST':
+ form = RetagQuestionForm(question, request.POST)
+ if form.is_valid():
+ if form.has_changed():
+ latest_revision = question.get_latest_revision()
+ retagged_at = datetime.datetime.now()
+ # Update the Question itself
+ Question.objects.filter(id=question.id).update(
+ tagnames = form.cleaned_data['tags'],
+ last_edited_at = retagged_at,
+ last_edited_by = request.user,
+ last_activity_at = retagged_at,
+ last_activity_by = request.user
+ )
+ # Update the Question's tag associations
+ tags_updated = Question.objects.update_tags(question,
+ form.cleaned_data['tags'], request.user)
+ # Create a new revision
+ QuestionRevision.objects.create(
+ question = question,
+ title = latest_revision.title,
+ author = request.user,
+ revised_at = retagged_at,
+ tagnames = form.cleaned_data['tags'],
+ summary = CONST['retagged'],
+ text = latest_revision.text
+ )
+ # send tags updated singal
+ tags_updated.send(sender=question.__class__, question=question)
+
+ return HttpResponseRedirect(question.get_absolute_url())
+ else:
+ form = RetagQuestionForm(question)
+ return render_to_response('question_retag.html', {
+ 'question': question,
+ 'form' : form,
+ 'tags' : _get_tags_cache_json(),
+ }, context_instance=RequestContext(request))
+
+def _edit_question(request, question):#non-url subview of edit_question - just edit the body/title
+ latest_revision = question.get_latest_revision()
+ revision_form = None
+ if request.method == 'POST':
+ if 'select_revision' in request.POST:
+ # user has changed revistion number
+ revision_form = RevisionForm(question, latest_revision, request.POST)
+ if revision_form.is_valid():
+ # Replace with those from the selected revision
+ form = EditQuestionForm(question,
+ QuestionRevision.objects.get(question=question,
+ revision=revision_form.cleaned_data['revision']))
+ else:
+ form = EditQuestionForm(question, latest_revision, request.POST)
+ else:
+ # Always check modifications against the latest revision
+ form = EditQuestionForm(question, latest_revision, request.POST)
+ if form.is_valid():
+ html = sanitize_html(markdowner.convert(form.cleaned_data['text']))
+ if form.has_changed():
+ edited_at = datetime.datetime.now()
+ tags_changed = (latest_revision.tagnames !=
+ form.cleaned_data['tags'])
+ tags_updated = False
+ # Update the Question itself
+ updated_fields = {
+ 'title': form.cleaned_data['title'],
+ 'last_edited_at': edited_at,
+ 'last_edited_by': request.user,
+ 'last_activity_at': edited_at,
+ 'last_activity_by': request.user,
+ 'tagnames': form.cleaned_data['tags'],
+ 'summary': strip_tags(html)[:120],
+ 'html': html,
+ }
+
+ # only save when it's checked
+ # because wiki doesn't allow to be edited if last version has been enabled already
+ # and we make sure this in forms.
+ if ('wiki' in form.cleaned_data and
+ form.cleaned_data['wiki']):
+ updated_fields['wiki'] = True
+ updated_fields['wikified_at'] = edited_at
+
+ Question.objects.filter(
+ id=question.id).update(**updated_fields)
+ # Update the Question's tag associations
+ if tags_changed:
+ tags_updated = Question.objects.update_tags(
+ question, form.cleaned_data['tags'], request.user)
+ # Create a new revision
+ revision = QuestionRevision(
+ question = question,
+ title = form.cleaned_data['title'],
+ author = request.user,
+ revised_at = edited_at,
+ tagnames = form.cleaned_data['tags'],
+ text = form.cleaned_data['text'],
+ )
+ if form.cleaned_data['summary']:
+ revision.summary = form.cleaned_data['summary']
+ else:
+ revision.summary = 'No.%s Revision' % latest_revision.revision
+ revision.save()
+
+ return HttpResponseRedirect(question.get_absolute_url())
+ else:
+
+ revision_form = RevisionForm(question, latest_revision)
+ form = EditQuestionForm(question, latest_revision)
+ return render_to_response('question_edit.html', {
+ 'question': question,
+ 'revision_form': revision_form,
+ 'form' : form,
+ 'tags' : _get_tags_cache_json()
+ }, context_instance=RequestContext(request))
+
+@login_required
+def edit_answer(request, id):
+ answer = get_object_or_404(Answer, id=id)
+ if answer.deleted and not auth.can_view_deleted_post(request.user, answer):
+ raise Http404
+ elif not auth.can_edit_post(request.user, answer):
+ raise Http404
+ else:
+ latest_revision = answer.get_latest_revision()
+ if request.method == "POST":
+ if 'select_revision' in request.POST:
+ # user has changed revistion number
+ revision_form = RevisionForm(answer, latest_revision, request.POST)
+ 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']))
+ else:
+ form = EditAnswerForm(answer, latest_revision, request.POST)
+ else:
+ form = EditAnswerForm(answer, latest_revision, request.POST)
+ if form.is_valid():
+ html = sanitize_html(markdowner.convert(form.cleaned_data['text']))
+ if form.has_changed():
+ edited_at = datetime.datetime.now()
+ updated_fields = {
+ 'last_edited_at': edited_at,
+ 'last_edited_by': request.user,
+ 'html': html,
+ }
+ 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']
+ )
+
+ if form.cleaned_data['summary']:
+ revision.summary = form.cleaned_data['summary']
+ else:
+ revision.summary = 'No.%s Revision' % latest_revision.revision
+ revision.save()
+
+ answer.question.last_activity_at = edited_at
+ answer.question.last_activity_by = request.user
+ answer.question.save()
+
+ return HttpResponseRedirect(answer.get_absolute_url())
+ else:
+ 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))
+
+def answer(request, id):#process a new answer
+ question = get_object_or_404(Question, id=id)
+ if request.method == "POST":
+ form = AnswerForm(question, request.user, request.POST)
+ if form.is_valid():
+ wiki = form.cleaned_data['wiki']
+ text = form.cleaned_data['text']
+ update_time = datetime.datetime.now()
+
+ if request.user.is_authenticated():
+ Answer.objects.create_new(
+ question=question,
+ author=request.user,
+ added_at=update_time,
+ wiki=wiki,
+ text=sanitize_html(markdowner.convert(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'],
+ )
+ anon.save()
+ return HttpResponseRedirect(reverse('auth_action_signin', kwargs={'action': 'newanswer'}))
+
+ return HttpResponseRedirect(question.get_absolute_url())
+
+def __generate_comments_json(obj, type, user):#non-view generates json data for the post comments
+ comments = obj.comments.all().order_by('id')
+ # {"Id":6,"PostId":38589,"CreationDate":"an hour ago","Text":"hello there!","UserDisplayName":"Jarrod Dixon","UserUrl":"/users/3/jarrod-dixon","DeleteUrl":null}
+ json_comments = []
+ from forum.templatetags.extra_tags import diff_date
+ for comment in comments:
+ comment_user = comment.user
+ delete_url = ""
+ if user != None and auth.can_delete_comment(user, comment):
+ #/posts/392845/comments/219852/delete
+ #todo translate this url
+ delete_url = reverse('index') + type + "s/%s/comments/%s/delete/" % (obj.id, comment.id)
+ json_comments.append({"id" : comment.id,
+ "object_id" : obj.id,
+ "comment_age" : diff_date(comment.added_at),
+ "text" : comment.comment,
+ "user_display_name" : comment_user.username,
+ "user_url" : comment_user.get_profile_url(),
+ "delete_url" : delete_url
+ })
+
+ data = simplejson.dumps(json_comments)
+ return HttpResponse(data, mimetype="application/json")
+
+
+def question_comments(request, id):#ajax handler for loading comments to question
+ question = get_object_or_404(Question, id=id)
+ user = request.user
+ return __comments(request, question, 'question')
+
+def answer_comments(request, id):#ajax handler for loading comments on answer
+ answer = get_object_or_404(Answer, id=id)
+ user = request.user
+ return __comments(request, answer, 'answer')
+
+def __comments(request, obj, type):#non-view generic ajax handler to load comments to an object
+ # only support get post comments by ajax now
+ user = request.user
+ if request.is_ajax():
+ if request.method == "GET":
+ response = __generate_comments_json(obj, type, user)
+ elif request.method == "POST":
+ if auth.can_add_comments(user,obj):
+ comment_data = request.POST.get('comment')
+ comment = Comment(content_object=obj, comment=comment_data, user=request.user)
+ comment.save()
+ obj.comment_count = obj.comment_count + 1
+ obj.save()
+ response = __generate_comments_json(obj, type, user)
+ else:
+ response = HttpResponseForbidden(mimetype="application/json")
+ return response
+
+def delete_comment(request, object_id='', comment_id='', commented_object_type=None):#ajax handler to delete comment
+ response = None
+ commented_object = None
+ if commented_object_type == 'question':
+ commented_object = Question
+ elif commented_object_type == 'answer':
+ commented_object = Answer
+
+ if request.is_ajax():
+ comment = get_object_or_404(Comment, id=comment_id)
+ if auth.can_delete_comment(request.user, comment):
+ obj = get_object_or_404(commented_object, id=object_id)
+ obj.comments.remove(comment)
+ obj.comment_count = obj.comment_count - 1
+ obj.save()
+ user = request.user
+ return __generate_comments_json(obj, commented_object_type, user)
+ raise PermissionDenied()
diff --git a/forum_modules/__init__.py b/forum_modules/__init__.py
new file mode 100755
index 0000000..e69de29
diff --git a/forum_modules/books/__init__.py b/forum_modules/books/__init__.py
new file mode 100755
index 0000000..bdd27b2
--- /dev/null
+++ b/forum_modules/books/__init__.py
@@ -0,0 +1,3 @@
+NAME = 'Osqa Books'
+DESCRIPTION = "Allows discussion around books."
+CAN_ENABLE = True
diff --git a/forum_modules/books/models.py b/forum_modules/books/models.py
new file mode 100755
index 0000000..ecde34b
--- /dev/null
+++ b/forum_modules/books/models.py
@@ -0,0 +1,63 @@
+from django.db import models
+from django.contrib.auth.models import User
+from forum.models import Question
+from django.core.urlresolvers import reverse
+from django.utils.http import urlquote as django_urlquote
+from django.template.defaultfilters import slugify
+
+class Book(models.Model):
+ """
+ Model for book info
+ """
+ user = models.ForeignKey(User)
+ title = models.CharField(max_length=255)
+ short_name = models.CharField(max_length=255)
+ author = models.CharField(max_length=255)
+ price = models.DecimalField(max_digits=6, decimal_places=2)
+ pages = models.SmallIntegerField()
+ published_at = models.DateTimeField()
+ publication = models.CharField(max_length=255)
+ cover_img = models.CharField(max_length=255)
+ tagnames = models.CharField(max_length=125)
+ added_at = models.DateTimeField()
+ last_edited_at = models.DateTimeField()
+ questions = models.ManyToManyField(Question, related_name='book', db_table='book_question')
+
+ def get_absolute_url(self):
+ return reverse('book', args=[django_urlquote(slugify(self.short_name))])
+
+ def __unicode__(self):
+ return self.title
+
+ class Meta:
+ app_label = 'forum'
+ db_table = u'book'
+
+class BookAuthorInfo(models.Model):
+ """
+ Model for book author info
+ """
+ user = models.ForeignKey(User)
+ book = models.ForeignKey(Book)
+ blog_url = models.CharField(max_length=255)
+ added_at = models.DateTimeField()
+ last_edited_at = models.DateTimeField()
+
+ class Meta:
+ app_label = 'forum'
+ db_table = u'book_author_info'
+
+class BookAuthorRss(models.Model):
+ """
+ Model for book author blog rss
+ """
+ user = models.ForeignKey(User)
+ book = models.ForeignKey(Book)
+ title = models.CharField(max_length=255)
+ url = models.CharField(max_length=255)
+ rss_created_at = models.DateTimeField()
+ added_at = models.DateTimeField()
+
+ class Meta:
+ app_label = 'forum'
+ db_table = u'book_author_rss'
\ No newline at end of file
diff --git a/forum_modules/books/urls.py b/forum_modules/books/urls.py
new file mode 100755
index 0000000..0e0432b
--- /dev/null
+++ b/forum_modules/books/urls.py
@@ -0,0 +1,10 @@
+from django.conf.urls.defaults import *
+from django.utils.translation import ugettext as _
+
+import views as app
+
+urlpatterns = patterns('',
+ url(r'^%s$' % _('books/'), app.books, name='books'),
+ url(r'^%s%s(?P[^/]+)/$' % (_('books/'), _('ask/')), app.ask_book, name='ask_book'),
+ url(r'^%s(?P[^/]+)/$' % _('books/'), app.book, name='book'),
+)
\ No newline at end of file
diff --git a/forum_modules/books/views.py b/forum_modules/books/views.py
new file mode 100755
index 0000000..31c8297
--- /dev/null
+++ b/forum_modules/books/views.py
@@ -0,0 +1,142 @@
+from django.shortcuts import render_to_response, get_object_or_404
+from django.http import HttpResponseRedirect, HttpResponse, HttpResponseForbidden, Http404
+from django.template import RequestContext
+from django.contrib.auth.decorators import login_required
+from django.core.urlresolvers import reverse
+from django.utils.html import *
+
+from models import *
+
+from forum.forms import AskForm
+from forum.views.readers import _get_tags_cache_json
+from forum.models import *
+from forum.utils.html import sanitize_html
+
+def books(request):
+ return HttpResponseRedirect(reverse('books') + '/mysql-zhaoyang')
+
+def book(request, short_name, unanswered=False):
+ """
+ 1. questions list
+ 2. book info
+ 3. author info and blog rss items
+ """
+ """
+ List of Questions, Tagged questions, and Unanswered questions.
+ """
+ books = Book.objects.extra(where=['short_name = %s'], params=[short_name])
+ match_count = len(books)
+ if match_count == 0:
+ raise Http404
+ else:
+ # the book info
+ book = books[0]
+ # get author info
+ author_info = BookAuthorInfo.objects.get(book=book)
+ # get author rss info
+ author_rss = BookAuthorRss.objects.filter(book=book)
+
+ # get pagesize from session, if failed then get default value
+ user_page_size = request.session.get("pagesize", QUESTIONS_PAGE_SIZE)
+ # set pagesize equal to logon user specified value in database
+ if request.user.is_authenticated() and request.user.questions_per_page > 0:
+ user_page_size = request.user.questions_per_page
+
+ try:
+ page = int(request.GET.get('page', '1'))
+ except ValueError:
+ page = 1
+
+ view_id = request.GET.get('sort', None)
+ view_dic = {"latest":"-added_at", "active":"-last_activity_at", "hottest":"-answer_count", "mostvoted":"-score" }
+ try:
+ orderby = view_dic[view_id]
+ except KeyError:
+ view_id = "latest"
+ orderby = "-added_at"
+
+ # check if request is from tagged questions
+ if unanswered:
+ # check if request is from unanswered questions
+ # Article.objects.filter(publications__id__exact=1)
+ objects = Question.objects.filter(book__id__exact=book.id, deleted=False, answer_count=0).order_by(orderby)
+ else:
+ objects = Question.objects.filter(book__id__exact=book.id, deleted=False).order_by(orderby)
+
+ # RISK - inner join queries
+ objects = objects.select_related();
+ objects_list = Paginator(objects, user_page_size)
+ questions = objects_list.page(page)
+
+ return render_to_response('book.html', {
+ "book" : book,
+ "author_info" : author_info,
+ "author_rss" : author_rss,
+ "questions" : questions,
+ "context" : {
+ 'is_paginated' : True,
+ 'pages': objects_list.num_pages,
+ 'page': page,
+ 'has_previous': questions.has_previous(),
+ 'has_next': questions.has_next(),
+ 'previous': questions.previous_page_number(),
+ 'next': questions.next_page_number(),
+ 'base_url' : request.path + '?sort=%s&' % view_id,
+ 'pagesize' : user_page_size
+ }
+ }, context_instance=RequestContext(request))
+
+@login_required
+def ask_book(request, short_name):
+ if request.method == "POST":
+ form = AskForm(request.POST)
+ if form.is_valid():
+ added_at = datetime.datetime.now()
+ html = sanitize_html(markdowner.convert(form.cleaned_data['text']))
+ question = Question(
+ title = strip_tags(form.cleaned_data['title']),
+ author = request.user,
+ added_at = added_at,
+ last_activity_at = added_at,
+ last_activity_by = request.user,
+ wiki = form.cleaned_data['wiki'],
+ tagnames = form.cleaned_data['tags'].strip(),
+ html = html,
+ summary = strip_tags(html)[:120]
+ )
+ if question.wiki:
+ question.last_edited_by = question.author
+ question.last_edited_at = added_at
+ question.wikified_at = added_at
+
+ question.save()
+
+ # create the first revision
+ QuestionRevision.objects.create(
+ question = question,
+ revision = 1,
+ title = question.title,
+ author = request.user,
+ revised_at = added_at,
+ tagnames = question.tagnames,
+ summary = CONST['default_version'],
+ text = form.cleaned_data['text']
+ )
+
+ books = Book.objects.extra(where=['short_name = %s'], params=[short_name])
+ match_count = len(books)
+ if match_count == 1:
+ # the book info
+ book = books[0]
+ book.questions.add(question)
+
+ return HttpResponseRedirect(question.get_absolute_url())
+ else:
+ form = AskForm()
+
+ tags = _get_tags_cache_json()
+ return render_to_response('ask.html', {
+ 'form' : form,
+ 'tags' : tags,
+ 'email_validation_faq_url': reverse('faq') + '#validate',
+ }, context_instance=RequestContext(request))
\ No newline at end of file
diff --git a/forum_modules/facebookauth/__init__.py b/forum_modules/facebookauth/__init__.py
new file mode 100755
index 0000000..e69de29
diff --git a/forum_modules/facebookauth/authentication.py b/forum_modules/facebookauth/authentication.py
new file mode 100755
index 0000000..f2c5b6b
--- /dev/null
+++ b/forum_modules/facebookauth/authentication.py
@@ -0,0 +1,85 @@
+import hashlib
+from time import time
+from datetime import datetime
+from urllib import urlopen, urlencode
+from forum.authentication.base import AuthenticationConsumer, ConsumerTemplateContext, InvalidAuthentication
+from django.utils.translation import ugettext as _
+
+import settings
+
+try:
+ from json import load as load_json
+except:
+ from django.utils.simplejson import JSONDecoder
+
+ def load_json(json):
+ decoder = JSONDecoder()
+ return decoder.decode(json.read())
+
+class FacebookAuthConsumer(AuthenticationConsumer):
+
+ def process_authentication_request(self, request):
+ API_KEY = settings.FB_API_KEY
+
+ if API_KEY in request.COOKIES:
+ if self.check_cookies_signature(request.COOKIES):
+ if self.check_session_expiry(request.COOKIES):
+ return request.COOKIES[API_KEY + '_user']
+ else:
+ raise InvalidAuthentication(_('Sorry, your Facebook session has expired, please try again'))
+ else:
+ raise InvalidAuthentication(_('The authentication with Facebook connect failed due to an invalid signature'))
+ else:
+ raise InvalidAuthentication(_('The authentication with Facebook connect failed, cannot find authentication tokens'))
+
+ def generate_signature(self, values):
+ keys = []
+
+ for key in sorted(values.keys()):
+ keys.append(key)
+
+ signature = ''.join(['%s=%s' % (key, values[key]) for key in keys]) + settings.FB_APP_SECRET
+ return hashlib.md5(signature).hexdigest()
+
+ def check_session_expiry(self, cookies):
+ return datetime.fromtimestamp(float(cookies[settings.FB_API_KEY+'_expires'])) > datetime.now()
+
+ def check_cookies_signature(self, cookies):
+ API_KEY = settings.FB_API_KEY
+
+ values = {}
+
+ for key in cookies.keys():
+ if (key.startswith(API_KEY + '_')):
+ values[key.replace(API_KEY + '_', '')] = cookies[key]
+
+ return self.generate_signature(values) == cookies[API_KEY]
+
+ def get_user_data(self, key):
+ request_data = {
+ 'method': 'Users.getInfo',
+ 'api_key': settings.FB_API_KEY,
+ 'call_id': time(),
+ 'v': '1.0',
+ 'uids': key,
+ 'fields': 'name,first_name,last_name,email',
+ 'format': 'json',
+ }
+
+ request_data['sig'] = self.generate_signature(request_data)
+ fb_response = load_json(urlopen(settings.REST_SERVER, urlencode(request_data)))[0]
+
+ return {
+ 'username': fb_response['first_name'] + ' ' + fb_response['last_name'],
+ 'email': fb_response['email']
+ }
+
+class FacebookAuthContext(ConsumerTemplateContext):
+ mode = 'BIGICON'
+ type = 'CUSTOM'
+ weight = 100
+ human_name = 'Facebook'
+ code_template = 'modules/facebookauth/button.html'
+ extra_css = ["http://www.facebook.com/css/connect/connect_button.css"]
+
+ API_KEY = settings.FB_API_KEY
\ No newline at end of file
diff --git a/forum_modules/facebookauth/settings.py b/forum_modules/facebookauth/settings.py
new file mode 100755
index 0000000..b9a0101
--- /dev/null
+++ b/forum_modules/facebookauth/settings.py
@@ -0,0 +1,3 @@
+REST_SERVER = 'http://api.facebook.com/restserver.php'
+FB_API_KEY = 'f773fab7be12aea689948208f37ad336'
+FB_APP_SECRET = '894547c1b8db54d77f919b1695ae879c'
\ No newline at end of file
diff --git a/forum_modules/facebookauth/templates/button.html b/forum_modules/facebookauth/templates/button.html
new file mode 100755
index 0000000..ceae1fc
--- /dev/null
+++ b/forum_modules/facebookauth/templates/button.html
@@ -0,0 +1,38 @@
+
+
+Facebook
\ No newline at end of file
diff --git a/forum_modules/facebookauth/templates/xd_receiver.html b/forum_modules/facebookauth/templates/xd_receiver.html
new file mode 100755
index 0000000..9c1664d
--- /dev/null
+++ b/forum_modules/facebookauth/templates/xd_receiver.html
@@ -0,0 +1 @@
+
diff --git a/forum_modules/facebookauth/urls.py b/forum_modules/facebookauth/urls.py
new file mode 100755
index 0000000..de1715c
--- /dev/null
+++ b/forum_modules/facebookauth/urls.py
@@ -0,0 +1,9 @@
+from django.conf.urls.defaults import *
+from django.views.generic.simple import direct_to_template
+
+from views import user_is_registered
+
+urlpatterns = patterns('',
+ url(r'^xd_receiver.htm$', direct_to_template, {'template': 'modules/facebookauth/xd_receiver.html'}, name='xd_receiver'),
+ url(r'^facebook/user_is_registered/', user_is_registered, name="facebook_user_is_registered"),
+)
\ No newline at end of file
diff --git a/forum_modules/facebookauth/views.py b/forum_modules/facebookauth/views.py
new file mode 100755
index 0000000..f1e8f3b
--- /dev/null
+++ b/forum_modules/facebookauth/views.py
@@ -0,0 +1,11 @@
+from forum.models import AuthKeyUserAssociation
+from django.http import HttpResponse
+
+def user_is_registered(request):
+ try:
+ fb_uid = request.POST['fb_uid']
+ print fb_uid
+ AuthKeyUserAssociation.objects.get(key=fb_uid)
+ return HttpResponse('yes')
+ except:
+ return HttpResponse('no')
\ No newline at end of file
diff --git a/forum_modules/localauth/__init__.py b/forum_modules/localauth/__init__.py
new file mode 100755
index 0000000..e69de29
diff --git a/forum_modules/localauth/authentication.py b/forum_modules/localauth/authentication.py
new file mode 100755
index 0000000..3169a99
--- /dev/null
+++ b/forum_modules/localauth/authentication.py
@@ -0,0 +1,18 @@
+from forum.authentication.base import AuthenticationConsumer, ConsumerTemplateContext, InvalidAuthentication
+from forms import ClassicLoginForm
+
+class LocalAuthConsumer(AuthenticationConsumer):
+ def process_authentication_request(self, request):
+ form_auth = ClassicLoginForm(request.POST)
+
+ if form_auth.is_valid():
+ return form_auth.get_user()
+ else:
+ raise InvalidAuthentication(" ".join(form_auth.errors.values()[0]))
+
+class LocalAuthContext(ConsumerTemplateContext):
+ mode = 'STACK_ITEM'
+ weight = 1000
+ human_name = 'Local authentication'
+ stack_item_template = 'modules/localauth/loginform.html'
+ show_to_logged_in_user = False
\ No newline at end of file
diff --git a/forum_modules/localauth/forms.py b/forum_modules/localauth/forms.py
new file mode 100755
index 0000000..59f5d56
--- /dev/null
+++ b/forum_modules/localauth/forms.py
@@ -0,0 +1,77 @@
+from forum.utils.forms import NextUrlField, UserNameField, UserEmailField, SetPasswordForm
+from forum.models import EmailFeedSetting, Question
+from django.contrib.contenttypes.models import ContentType
+from django.utils.translation import ugettext as _
+from django.contrib.auth import authenticate
+from django import forms
+import logging
+
+class ClassicRegisterForm(SetPasswordForm):
+ """ legacy registration form """
+
+ next = NextUrlField()
+ username = UserNameField()
+ email = UserEmailField()
+ #fields password1 and password2 are inherited
+ #recaptcha = ReCaptchaField()
+
+class ClassicLoginForm(forms.Form):
+ """ legacy account signin form """
+ next = NextUrlField()
+ username = UserNameField(required=False,skip_clean=True)
+ password = forms.CharField(max_length=128,
+ widget=forms.widgets.PasswordInput(attrs={'class':'required login'}),
+ required=False)
+
+ def __init__(self, data=None, files=None, auto_id='id_%s',
+ prefix=None, initial=None):
+ super(ClassicLoginForm, self).__init__(data, files, auto_id,
+ prefix, initial)
+ self.user_cache = None
+
+ def _clean_nonempty_field(self,field):
+ value = None
+ if field in self.cleaned_data:
+ value = str(self.cleaned_data[field]).strip()
+ if value == '':
+ value = None
+ self.cleaned_data[field] = value
+ return value
+
+ def clean_username(self):
+ return self._clean_nonempty_field('username')
+
+ def clean_password(self):
+ return self._clean_nonempty_field('password')
+
+ def clean(self):
+ error_list = []
+ username = self.cleaned_data['username']
+ password = self.cleaned_data['password']
+
+ self.user_cache = None
+ if username and password:
+ self.user_cache = authenticate(username=username, password=password)
+
+ if self.user_cache is None:
+ del self.cleaned_data['username']
+ del self.cleaned_data['password']
+ error_list.insert(0,(_("Please enter valid username and password "
+ "(both are case-sensitive).")))
+ elif self.user_cache.is_active == False:
+ error_list.append(_("This account is inactive."))
+ if len(error_list) > 0:
+ error_list.insert(0,_('Login failed.'))
+ elif password == None and username == None:
+ error_list.append(_('Please enter username and password'))
+ elif password == None:
+ error_list.append(_('Please enter your password'))
+ elif username == None:
+ error_list.append(_('Please enter user name'))
+ if len(error_list) > 0:
+ self._errors['__all__'] = forms.util.ErrorList(error_list)
+ return self.cleaned_data
+
+ def get_user(self):
+ """ get authenticated user """
+ return self.user_cache
\ No newline at end of file
diff --git a/forum_modules/localauth/templates/loginform.html b/forum_modules/localauth/templates/loginform.html
new file mode 100755
index 0000000..0d95a2f
--- /dev/null
+++ b/forum_modules/localauth/templates/loginform.html
@@ -0,0 +1,31 @@
+{% load i18n %}
+
+
\ No newline at end of file
diff --git a/forum_modules/localauth/urls.py b/forum_modules/localauth/urls.py
new file mode 100755
index 0000000..e2a3b26
--- /dev/null
+++ b/forum_modules/localauth/urls.py
@@ -0,0 +1,8 @@
+from django.conf.urls.defaults import *
+from django.views.generic.simple import direct_to_template
+from django.utils.translation import ugettext as _
+import views as app
+
+urlpatterns = patterns('',
+ url(r'^%s%s%s$' % (_('account/'), _('local/'), _('register/')), app.register, name='auth_local_register'),
+)
\ No newline at end of file
diff --git a/forum_modules/localauth/views.py b/forum_modules/localauth/views.py
new file mode 100755
index 0000000..acad16c
--- /dev/null
+++ b/forum_modules/localauth/views.py
@@ -0,0 +1,30 @@
+from django.contrib.auth.models import User
+from django.shortcuts import render_to_response
+from django.template import RequestContext
+
+from forms import ClassicRegisterForm
+from forum.authentication.forms import SimpleEmailSubscribeForm
+from forum.views.auth import login_and_forward
+
+def register(request):
+ if request.method == 'POST':
+ form = ClassicRegisterForm(request.POST)
+ email_feeds_form = SimpleEmailSubscribeForm(request.POST)
+
+ if form.is_valid() and email_feeds_form.is_valid():
+ username = form.cleaned_data['username']
+ password = form.cleaned_data['password1']
+ email = form.cleaned_data['email']
+
+ user_ = User.objects.create_user( username,email,password )
+ email_feeds_form.save(user_)
+ #todo: email validation
+ return login_and_forward(request, user_)
+ else:
+ form = ClassicRegisterForm(initial={'next':'/'})
+ email_feeds_form = SimpleEmailSubscribeForm()
+
+ return render_to_response('auth/signup.html', {
+ 'form': form,
+ 'email_feeds_form': email_feeds_form
+ }, context_instance=RequestContext(request))
\ No newline at end of file
diff --git a/forum_modules/oauthauth/__init__.py b/forum_modules/oauthauth/__init__.py
new file mode 100755
index 0000000..e69de29
diff --git a/forum_modules/oauthauth/authentication.py b/forum_modules/oauthauth/authentication.py
new file mode 100755
index 0000000..cfe8156
--- /dev/null
+++ b/forum_modules/oauthauth/authentication.py
@@ -0,0 +1,41 @@
+from consumer import OAuthAbstractAuthConsumer
+from forum.authentication.base import ConsumerTemplateContext
+
+try:
+ import json as simplejson
+except ImportError:
+ from django.utils import simplejson
+
+from lib import oauth
+import settings
+
+class TwitterAuthConsumer(OAuthAbstractAuthConsumer):
+ def __init__(self):
+ OAuthAbstractAuthConsumer.__init__(self,
+ settings.TWITTER_CONSUMER_KEY,
+ settings.TWITTER_CONSUMER_SECRET,
+ "twitter.com",
+ "https://twitter.com/oauth/request_token",
+ "https://twitter.com/oauth/access_token",
+ "https://twitter.com/oauth/authorize",
+ )
+
+ def get_user_data(self, key):
+ json = self.fetch_data(key, "https://twitter.com/account/verify_credentials.json")
+
+ if 'screen_name' in json:
+ creds = simplejson.loads(json)
+
+ return {
+ 'username': creds['screen_name']
+ }
+
+
+ return {}
+
+class TwitterAuthContext(ConsumerTemplateContext):
+ mode = 'BIGICON'
+ type = 'DIRECT'
+ weight = 150
+ human_name = 'Twitter'
+ icon = '/media/images/openid/twitter.png'
\ No newline at end of file
diff --git a/forum_modules/oauthauth/consumer.py b/forum_modules/oauthauth/consumer.py
new file mode 100755
index 0000000..e3cbda6
--- /dev/null
+++ b/forum_modules/oauthauth/consumer.py
@@ -0,0 +1,87 @@
+import urllib
+import urllib2
+import httplib
+import time
+
+from forum.authentication.base import AuthenticationConsumer, InvalidAuthentication
+from django.utils.translation import ugettext as _
+
+from lib import oauth
+
+class OAuthAbstractAuthConsumer(AuthenticationConsumer):
+
+ def __init__(self, consumer_key, consumer_secret, server_url, request_token_url, access_token_url, authorization_url):
+ self.consumer_secret = consumer_secret
+ self.consumer_key = consumer_key
+
+ self.consumer = oauth.OAuthConsumer(consumer_key, consumer_secret)
+ self.signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1()
+
+ self.server_url = server_url
+ self.request_token_url = request_token_url
+ self.access_token_url = access_token_url
+ self.authorization_url = authorization_url
+
+ def prepare_authentication_request(self, request, redirect_to):
+ request_token = self.fetch_request_token()
+ request.session['unauthed_token'] = request_token.to_string()
+ return self.authorize_token_url(request_token)
+
+ def process_authentication_request(self, request):
+ unauthed_token = request.session.get('unauthed_token', None)
+ if not unauthed_token:
+ raise InvalidAuthentication(_('Error, the oauth token is not on the server'))
+
+ token = oauth.OAuthToken.from_string(unauthed_token)
+
+ if token.key != request.GET.get('oauth_token', 'no-token'):
+ raise InvalidAuthentication(_("Something went wrong! Auth tokens do not match"))
+
+ access_token = self.fetch_access_token(token)
+
+ return access_token.to_string()
+
+ def get_user_data(self, key):
+ #token = oauth.OAuthToken.from_string(access_token)
+ return {}
+
+ def fetch_request_token(self):
+ oauth_request = oauth.OAuthRequest.from_consumer_and_token(self.consumer, http_url=self.request_token_url)
+ oauth_request.sign_request(self.signature_method, self.consumer, None)
+ params = oauth_request.parameters
+ data = urllib.urlencode(params)
+ full_url='%s?%s'%(self.request_token_url, data)
+ response = urllib2.urlopen(full_url)
+ return oauth.OAuthToken.from_string(response.read())
+
+ def authorize_token_url(self, token, callback_url=None):
+ oauth_request = oauth.OAuthRequest.from_token_and_callback(token=token,\
+ callback=callback_url, http_url=self.authorization_url)
+ params = oauth_request.parameters
+ data = urllib.urlencode(params)
+ full_url='%s?%s'%(self.authorization_url, data)
+ return full_url
+
+ def fetch_access_token(self, token):
+ oauth_request = oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=token, http_url=self.access_token_url)
+ oauth_request.sign_request(self.signature_method, self.consumer, token)
+ params = oauth_request.parameters
+ data = urllib.urlencode(params)
+ full_url='%s?%s'%(self.access_token_url, data)
+ response = urllib2.urlopen(full_url)
+ return oauth.OAuthToken.from_string(response.read())
+
+ def fetch_data(self, token, http_url, parameters=None):
+ access_token = oauth.OAuthToken.from_string(token)
+ oauth_request = oauth.OAuthRequest.from_consumer_and_token(
+ self.consumer, token=access_token, http_method="GET",
+ http_url=http_url, parameters=parameters,
+ )
+ oauth_request.sign_request(self.signature_method, self.consumer, access_token)
+
+ url = oauth_request.to_url()
+ connection = httplib.HTTPSConnection(self.server_url)
+ connection.request(oauth_request.http_method, url)
+
+ return connection.getresponse().read()
+
diff --git a/forum_modules/oauthauth/lib/__init__.py b/forum_modules/oauthauth/lib/__init__.py
new file mode 100755
index 0000000..e69de29
diff --git a/forum_modules/oauthauth/lib/oauth.py b/forum_modules/oauthauth/lib/oauth.py
new file mode 100755
index 0000000..e2af5c0
--- /dev/null
+++ b/forum_modules/oauthauth/lib/oauth.py
@@ -0,0 +1,594 @@
+"""
+The MIT License
+
+Copyright (c) 2007 Leah Culver
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+"""
+
+import cgi
+import urllib
+import time
+import random
+import urlparse
+import hmac
+import binascii
+
+
+VERSION = '1.0' # Hi Blaine!
+HTTP_METHOD = 'GET'
+SIGNATURE_METHOD = 'PLAINTEXT'
+
+
+class OAuthError(RuntimeError):
+ """Generic exception class."""
+ def __init__(self, message='OAuth error occured.'):
+ self.message = message
+
+def build_authenticate_header(realm=''):
+ """Optional WWW-Authenticate header (401 error)"""
+ return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
+
+def escape(s):
+ """Escape a URL including any /."""
+ return urllib.quote(s, safe='~')
+
+def _utf8_str(s):
+ """Convert unicode to utf-8."""
+ if isinstance(s, unicode):
+ return s.encode("utf-8")
+ else:
+ return str(s)
+
+def generate_timestamp():
+ """Get seconds since epoch (UTC)."""
+ return int(time.time())
+
+def generate_nonce(length=8):
+ """Generate pseudorandom number."""
+ return ''.join([str(random.randint(0, 9)) for i in range(length)])
+
+
+class OAuthConsumer(object):
+ """Consumer of OAuth authentication.
+
+ OAuthConsumer is a data type that represents the identity of the Consumer
+ via its shared secret with the Service Provider.
+
+ """
+ key = None
+ secret = None
+
+ def __init__(self, key, secret):
+ self.key = key
+ self.secret = secret
+
+
+class OAuthToken(object):
+ """OAuthToken is a data type that represents an End User via either an access
+ or request token.
+
+ key -- the token
+ secret -- the token secret
+
+ """
+ key = None
+ secret = None
+
+ def __init__(self, key, secret):
+ self.key = key
+ self.secret = secret
+
+ def to_string(self):
+ return urllib.urlencode({'oauth_token': self.key,
+ 'oauth_token_secret': self.secret})
+
+ def from_string(s):
+ """ Returns a token from something like:
+ oauth_token_secret=xxx&oauth_token=xxx
+ """
+ params = cgi.parse_qs(s, keep_blank_values=False)
+ key = params['oauth_token'][0]
+ secret = params['oauth_token_secret'][0]
+ return OAuthToken(key, secret)
+ from_string = staticmethod(from_string)
+
+ def __str__(self):
+ return self.to_string()
+
+
+class OAuthRequest(object):
+ """OAuthRequest represents the request and can be serialized.
+
+ OAuth parameters:
+ - oauth_consumer_key
+ - oauth_token
+ - oauth_signature_method
+ - oauth_signature
+ - oauth_timestamp
+ - oauth_nonce
+ - oauth_version
+ ... any additional parameters, as defined by the Service Provider.
+ """
+ parameters = None # OAuth parameters.
+ http_method = HTTP_METHOD
+ http_url = None
+ version = VERSION
+
+ def __init__(self, http_method=HTTP_METHOD, http_url=None, parameters=None):
+ self.http_method = http_method
+ self.http_url = http_url
+ self.parameters = parameters or {}
+
+ def set_parameter(self, parameter, value):
+ self.parameters[parameter] = value
+
+ def get_parameter(self, parameter):
+ try:
+ return self.parameters[parameter]
+ except:
+ raise OAuthError('Parameter not found: %s' % parameter)
+
+ def _get_timestamp_nonce(self):
+ return self.get_parameter('oauth_timestamp'), self.get_parameter(
+ 'oauth_nonce')
+
+ def get_nonoauth_parameters(self):
+ """Get any non-OAuth parameters."""
+ parameters = {}
+ for k, v in self.parameters.iteritems():
+ # Ignore oauth parameters.
+ if k.find('oauth_') < 0:
+ parameters[k] = v
+ return parameters
+
+ def to_header(self, realm=''):
+ """Serialize as a header for an HTTPAuth request."""
+ auth_header = 'OAuth realm="%s"' % realm
+ # Add the oauth parameters.
+ if self.parameters:
+ for k, v in self.parameters.iteritems():
+ if k[:6] == 'oauth_':
+ auth_header += ', %s="%s"' % (k, escape(str(v)))
+ return {'Authorization': auth_header}
+
+ def to_postdata(self):
+ """Serialize as post data for a POST request."""
+ return '&'.join(['%s=%s' % (escape(str(k)), escape(str(v))) \
+ for k, v in self.parameters.iteritems()])
+
+ def to_url(self):
+ """Serialize as a URL for a GET request."""
+ return '%s?%s' % (self.get_normalized_http_url(), self.to_postdata())
+
+ def get_normalized_parameters(self):
+ """Return a string that contains the parameters that must be signed."""
+ params = self.parameters
+ try:
+ # Exclude the signature if it exists.
+ del params['oauth_signature']
+ except:
+ pass
+ # Escape key values before sorting.
+ key_values = [(escape(_utf8_str(k)), escape(_utf8_str(v))) \
+ for k,v in params.items()]
+ # Sort lexicographically, first after key, then after value.
+ key_values.sort()
+ # Combine key value pairs into a string.
+ return '&'.join(['%s=%s' % (k, v) for k, v in key_values])
+
+ def get_normalized_http_method(self):
+ """Uppercases the http method."""
+ return self.http_method.upper()
+
+ def get_normalized_http_url(self):
+ """Parses the URL and rebuilds it to be scheme://host/path."""
+ parts = urlparse.urlparse(self.http_url)
+ scheme, netloc, path = parts[:3]
+ # Exclude default port numbers.
+ if scheme == 'http' and netloc[-3:] == ':80':
+ netloc = netloc[:-3]
+ elif scheme == 'https' and netloc[-4:] == ':443':
+ netloc = netloc[:-4]
+ return '%s://%s%s' % (scheme, netloc, path)
+
+ def sign_request(self, signature_method, consumer, token):
+ """Set the signature parameter to the result of build_signature."""
+ # Set the signature method.
+ self.set_parameter('oauth_signature_method',
+ signature_method.get_name())
+ # Set the signature.
+ self.set_parameter('oauth_signature',
+ self.build_signature(signature_method, consumer, token))
+
+ def build_signature(self, signature_method, consumer, token):
+ """Calls the build signature method within the signature method."""
+ return signature_method.build_signature(self, consumer, token)
+
+ def from_request(http_method, http_url, headers=None, parameters=None,
+ query_string=None):
+ """Combines multiple parameter sources."""
+ if parameters is None:
+ parameters = {}
+
+ # Headers
+ if headers and 'Authorization' in headers:
+ auth_header = headers['Authorization']
+ # Check that the authorization header is OAuth.
+ if auth_header.index('OAuth') > -1:
+ auth_header = auth_header.lstrip('OAuth ')
+ try:
+ # Get the parameters from the header.
+ header_params = OAuthRequest._split_header(auth_header)
+ parameters.update(header_params)
+ except:
+ raise OAuthError('Unable to parse OAuth parameters from '
+ 'Authorization header.')
+
+ # GET or POST query string.
+ if query_string:
+ query_params = OAuthRequest._split_url_string(query_string)
+ parameters.update(query_params)
+
+ # URL parameters.
+ param_str = urlparse.urlparse(http_url)[4] # query
+ url_params = OAuthRequest._split_url_string(param_str)
+ parameters.update(url_params)
+
+ if parameters:
+ return OAuthRequest(http_method, http_url, parameters)
+
+ return None
+ from_request = staticmethod(from_request)
+
+ def from_consumer_and_token(oauth_consumer, token=None,
+ http_method=HTTP_METHOD, http_url=None, parameters=None):
+ if not parameters:
+ parameters = {}
+
+ defaults = {
+ 'oauth_consumer_key': oauth_consumer.key,
+ 'oauth_timestamp': generate_timestamp(),
+ 'oauth_nonce': generate_nonce(),
+ 'oauth_version': OAuthRequest.version,
+ }
+
+ defaults.update(parameters)
+ parameters = defaults
+
+ if token:
+ parameters['oauth_token'] = token.key
+
+ return OAuthRequest(http_method, http_url, parameters)
+ from_consumer_and_token = staticmethod(from_consumer_and_token)
+
+ def from_token_and_callback(token, callback=None, http_method=HTTP_METHOD,
+ http_url=None, parameters=None):
+ if not parameters:
+ parameters = {}
+
+ parameters['oauth_token'] = token.key
+
+ if callback:
+ parameters['oauth_callback'] = callback
+
+ return OAuthRequest(http_method, http_url, parameters)
+ from_token_and_callback = staticmethod(from_token_and_callback)
+
+ def _split_header(header):
+ """Turn Authorization: header into parameters."""
+ params = {}
+ parts = header.split(',')
+ for param in parts:
+ # Ignore realm parameter.
+ if param.find('realm') > -1:
+ continue
+ # Remove whitespace.
+ param = param.strip()
+ # Split key-value.
+ param_parts = param.split('=', 1)
+ # Remove quotes and unescape the value.
+ params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"'))
+ return params
+ _split_header = staticmethod(_split_header)
+
+ def _split_url_string(param_str):
+ """Turn URL string into parameters."""
+ parameters = cgi.parse_qs(param_str, keep_blank_values=False)
+ for k, v in parameters.iteritems():
+ parameters[k] = urllib.unquote(v[0])
+ return parameters
+ _split_url_string = staticmethod(_split_url_string)
+
+class OAuthServer(object):
+ """A worker to check the validity of a request against a data store."""
+ timestamp_threshold = 300 # In seconds, five minutes.
+ version = VERSION
+ signature_methods = None
+ data_store = None
+
+ def __init__(self, data_store=None, signature_methods=None):
+ self.data_store = data_store
+ self.signature_methods = signature_methods or {}
+
+ def set_data_store(self, data_store):
+ self.data_store = data_store
+
+ def get_data_store(self):
+ return self.data_store
+
+ def add_signature_method(self, signature_method):
+ self.signature_methods[signature_method.get_name()] = signature_method
+ return self.signature_methods
+
+ def fetch_request_token(self, oauth_request):
+ """Processes a request_token request and returns the
+ request token on success.
+ """
+ try:
+ # Get the request token for authorization.
+ token = self._get_token(oauth_request, 'request')
+ except OAuthError:
+ # No token required for the initial token request.
+ version = self._get_version(oauth_request)
+ consumer = self._get_consumer(oauth_request)
+ self._check_signature(oauth_request, consumer, None)
+ # Fetch a new token.
+ token = self.data_store.fetch_request_token(consumer)
+ return token
+
+ def fetch_access_token(self, oauth_request):
+ """Processes an access_token request and returns the
+ access token on success.
+ """
+ version = self._get_version(oauth_request)
+ consumer = self._get_consumer(oauth_request)
+ # Get the request token.
+ token = self._get_token(oauth_request, 'request')
+ self._check_signature(oauth_request, consumer, token)
+ new_token = self.data_store.fetch_access_token(consumer, token)
+ return new_token
+
+ def verify_request(self, oauth_request):
+ """Verifies an api call and checks all the parameters."""
+ # -> consumer and token
+ version = self._get_version(oauth_request)
+ consumer = self._get_consumer(oauth_request)
+ # Get the access token.
+ token = self._get_token(oauth_request, 'access')
+ self._check_signature(oauth_request, consumer, token)
+ parameters = oauth_request.get_nonoauth_parameters()
+ return consumer, token, parameters
+
+ def authorize_token(self, token, user):
+ """Authorize a request token."""
+ return self.data_store.authorize_request_token(token, user)
+
+ def get_callback(self, oauth_request):
+ """Get the callback URL."""
+ return oauth_request.get_parameter('oauth_callback')
+
+ def build_authenticate_header(self, realm=''):
+ """Optional support for the authenticate header."""
+ return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
+
+ def _get_version(self, oauth_request):
+ """Verify the correct version request for this server."""
+ try:
+ version = oauth_request.get_parameter('oauth_version')
+ except:
+ version = VERSION
+ if version and version != self.version:
+ raise OAuthError('OAuth version %s not supported.' % str(version))
+ return version
+
+ def _get_signature_method(self, oauth_request):
+ """Figure out the signature with some defaults."""
+ try:
+ signature_method = oauth_request.get_parameter(
+ 'oauth_signature_method')
+ except:
+ signature_method = SIGNATURE_METHOD
+ try:
+ # Get the signature method object.
+ signature_method = self.signature_methods[signature_method]
+ except:
+ signature_method_names = ', '.join(self.signature_methods.keys())
+ raise OAuthError('Signature method %s not supported try one of the '
+ 'following: %s' % (signature_method, signature_method_names))
+
+ return signature_method
+
+ def _get_consumer(self, oauth_request):
+ consumer_key = oauth_request.get_parameter('oauth_consumer_key')
+ consumer = self.data_store.lookup_consumer(consumer_key)
+ if not consumer:
+ raise OAuthError('Invalid consumer.')
+ return consumer
+
+ def _get_token(self, oauth_request, token_type='access'):
+ """Try to find the token for the provided request token key."""
+ token_field = oauth_request.get_parameter('oauth_token')
+ token = self.data_store.lookup_token(token_type, token_field)
+ if not token:
+ raise OAuthError('Invalid %s token: %s' % (token_type, token_field))
+ return token
+
+ def _check_signature(self, oauth_request, consumer, token):
+ timestamp, nonce = oauth_request._get_timestamp_nonce()
+ self._check_timestamp(timestamp)
+ self._check_nonce(consumer, token, nonce)
+ signature_method = self._get_signature_method(oauth_request)
+ try:
+ signature = oauth_request.get_parameter('oauth_signature')
+ except:
+ raise OAuthError('Missing signature.')
+ # Validate the signature.
+ valid_sig = signature_method.check_signature(oauth_request, consumer,
+ token, signature)
+ if not valid_sig:
+ key, base = signature_method.build_signature_base_string(
+ oauth_request, consumer, token)
+ raise OAuthError('Invalid signature. Expected signature base '
+ 'string: %s' % base)
+ built = signature_method.build_signature(oauth_request, consumer, token)
+
+ def _check_timestamp(self, timestamp):
+ """Verify that timestamp is recentish."""
+ timestamp = int(timestamp)
+ now = int(time.time())
+ lapsed = now - timestamp
+ if lapsed > self.timestamp_threshold:
+ raise OAuthError('Expired timestamp: given %d and now %s has a '
+ 'greater difference than threshold %d' %
+ (timestamp, now, self.timestamp_threshold))
+
+ def _check_nonce(self, consumer, token, nonce):
+ """Verify that the nonce is uniqueish."""
+ nonce = self.data_store.lookup_nonce(consumer, token, nonce)
+ if nonce:
+ raise OAuthError('Nonce already used: %s' % str(nonce))
+
+
+class OAuthClient(object):
+ """OAuthClient is a worker to attempt to execute a request."""
+ consumer = None
+ token = None
+
+ def __init__(self, oauth_consumer, oauth_token):
+ self.consumer = oauth_consumer
+ self.token = oauth_token
+
+ def get_consumer(self):
+ return self.consumer
+
+ def get_token(self):
+ return self.token
+
+ def fetch_request_token(self, oauth_request):
+ """-> OAuthToken."""
+ raise NotImplementedError
+
+ def fetch_access_token(self, oauth_request):
+ """-> OAuthToken."""
+ raise NotImplementedError
+
+ def access_resource(self, oauth_request):
+ """-> Some protected resource."""
+ raise NotImplementedError
+
+
+class OAuthDataStore(object):
+ """A database abstraction used to lookup consumers and tokens."""
+
+ def lookup_consumer(self, key):
+ """-> OAuthConsumer."""
+ raise NotImplementedError
+
+ def lookup_token(self, oauth_consumer, token_type, token_token):
+ """-> OAuthToken."""
+ raise NotImplementedError
+
+ def lookup_nonce(self, oauth_consumer, oauth_token, nonce):
+ """-> OAuthToken."""
+ raise NotImplementedError
+
+ def fetch_request_token(self, oauth_consumer):
+ """-> OAuthToken."""
+ raise NotImplementedError
+
+ def fetch_access_token(self, oauth_consumer, oauth_token):
+ """-> OAuthToken."""
+ raise NotImplementedError
+
+ def authorize_request_token(self, oauth_token, user):
+ """-> OAuthToken."""
+ raise NotImplementedError
+
+
+class OAuthSignatureMethod(object):
+ """A strategy class that implements a signature method."""
+ def get_name(self):
+ """-> str."""
+ raise NotImplementedError
+
+ def build_signature_base_string(self, oauth_request, oauth_consumer, oauth_token):
+ """-> str key, str raw."""
+ raise NotImplementedError
+
+ def build_signature(self, oauth_request, oauth_consumer, oauth_token):
+ """-> str."""
+ raise NotImplementedError
+
+ def check_signature(self, oauth_request, consumer, token, signature):
+ built = self.build_signature(oauth_request, consumer, token)
+ return built == signature
+
+
+class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod):
+
+ def get_name(self):
+ return 'HMAC-SHA1'
+
+ def build_signature_base_string(self, oauth_request, consumer, token):
+ sig = (
+ escape(oauth_request.get_normalized_http_method()),
+ escape(oauth_request.get_normalized_http_url()),
+ escape(oauth_request.get_normalized_parameters()),
+ )
+
+ key = '%s&' % escape(consumer.secret)
+ if token:
+ key += escape(token.secret)
+ raw = '&'.join(sig)
+ return key, raw
+
+ def build_signature(self, oauth_request, consumer, token):
+ """Builds the base signature string."""
+ key, raw = self.build_signature_base_string(oauth_request, consumer,
+ token)
+
+ # HMAC object.
+ try:
+ import hashlib # 2.5
+ hashed = hmac.new(key, raw, hashlib.sha1)
+ except:
+ import sha # Deprecated
+ hashed = hmac.new(key, raw, sha)
+
+ # Calculate the digest base 64.
+ return binascii.b2a_base64(hashed.digest())[:-1]
+
+
+class OAuthSignatureMethod_PLAINTEXT(OAuthSignatureMethod):
+
+ def get_name(self):
+ return 'PLAINTEXT'
+
+ def build_signature_base_string(self, oauth_request, consumer, token):
+ """Concatenates the consumer key and secret."""
+ sig = '%s&' % escape(consumer.secret)
+ if token:
+ sig = sig + escape(token.secret)
+ return sig, sig
+
+ def build_signature(self, oauth_request, consumer, token):
+ key, raw = self.build_signature_base_string(oauth_request, consumer,
+ token)
+ return key
\ No newline at end of file
diff --git a/forum_modules/oauthauth/settings.py b/forum_modules/oauthauth/settings.py
new file mode 100755
index 0000000..d503fef
--- /dev/null
+++ b/forum_modules/oauthauth/settings.py
@@ -0,0 +1,3 @@
+TWITTER_CONSUMER_KEY = "sAAGwWILliIbgbrG37GztQ"
+TWITTER_CONSUMER_SECRET = "AZv0pHTZQaf4rxxZOrj3Jm1RKgmlV4MnYJAsrY7M0"
+
diff --git a/forum_modules/openidauth/__init__.py b/forum_modules/openidauth/__init__.py
new file mode 100755
index 0000000..e69de29
diff --git a/forum_modules/openidauth/authentication.py b/forum_modules/openidauth/authentication.py
new file mode 100755
index 0000000..d34591e
--- /dev/null
+++ b/forum_modules/openidauth/authentication.py
@@ -0,0 +1,196 @@
+from consumer import OpenIdAbstractAuthConsumer
+from forum.authentication.base import ConsumerTemplateContext
+
+class GoogleAuthConsumer(OpenIdAbstractAuthConsumer):
+ def get_user_url(self, request):
+ return 'https://www.google.com/accounts/o8/id'
+
+class GoogleAuthContext(ConsumerTemplateContext):
+ mode = 'BIGICON'
+ type = 'DIRECT'
+ weight = 200
+ human_name = 'Google'
+ icon = '/media/images/openid/google.gif'
+
+
+
+class YahooAuthConsumer(OpenIdAbstractAuthConsumer):
+ def get_user_url(self, request):
+ return 'http://yahoo.com/'
+
+class YahooAuthContext(ConsumerTemplateContext):
+ mode = 'BIGICON'
+ type = 'DIRECT'
+ weight = 300
+ human_name = 'Yahoo'
+ icon = '/media/images/openid/yahoo.gif'
+
+
+
+class AolAuthConsumer(OpenIdAbstractAuthConsumer):
+ def get_user_url(self, request):
+ uname = request.POST['input_field']
+ return 'http://openid.aol.com/' + uname
+
+class AolAuthContext(ConsumerTemplateContext):
+ mode = 'BIGICON'
+ type = 'SIMPLE_FORM'
+ simple_form_context = {
+ 'your_what': 'AOL screen name'
+ }
+ weight = 400
+ human_name = 'AOL'
+ icon = '/media/images/openid/aol.gif'
+
+
+class MyOpenIdAuthConsumer(OpenIdAbstractAuthConsumer):
+ def get_user_url(self, request):
+ blog_name = request.POST['input_field']
+ return "http://%s.myopenid.com/" % blog_name
+
+class MyOpenIdAuthContext(ConsumerTemplateContext):
+ mode = 'SMALLICON'
+ type = 'SIMPLE_FORM'
+ simple_form_context = {
+ 'your_what': 'MyOpenID user name'
+ }
+ weight = 200
+ human_name = 'MyOpenID'
+ icon = '/media/images/openid/myopenid.ico'
+
+
+class FlickrAuthConsumer(OpenIdAbstractAuthConsumer):
+ def get_user_url(self, request):
+ blog_name = request.POST['input_field']
+ return "http://flickr.com/%s/" % blog_name
+
+class FlickrAuthContext(ConsumerTemplateContext):
+ mode = 'SMALLICON'
+ type = 'SIMPLE_FORM'
+ simple_form_context = {
+ 'your_what': 'Flickr user name'
+ }
+ weight = 250
+ human_name = 'Flickr'
+ icon = '/media/images/openid/flickr.ico'
+
+
+class TechnoratiAuthConsumer(OpenIdAbstractAuthConsumer):
+ def get_user_url(self, request):
+ blog_name = request.POST['input_field']
+ return "http://technorati.com/people/technorati/%s/" % blog_name
+
+class TechnoratiAuthContext(ConsumerTemplateContext):
+ mode = 'SMALLICON'
+ type = 'SIMPLE_FORM'
+ simple_form_context = {
+ 'your_what': 'Technorati user name'
+ }
+ weight = 260
+ human_name = 'Technorati'
+ icon = '/media/images/openid/technorati.ico'
+
+
+class WordpressAuthConsumer(OpenIdAbstractAuthConsumer):
+ def get_user_url(self, request):
+ blog_name = request.POST['input_field']
+ return "http://%s.wordpress.com/" % blog_name
+
+class WordpressAuthContext(ConsumerTemplateContext):
+ mode = 'SMALLICON'
+ type = 'SIMPLE_FORM'
+ simple_form_context = {
+ 'your_what': 'Wordpress blog name'
+ }
+ weight = 270
+ human_name = 'Wordpress'
+ icon = '/media/images/openid/wordpress.ico'
+
+
+class BloggerAuthConsumer(OpenIdAbstractAuthConsumer):
+ def get_user_url(self, request):
+ blog_name = request.POST['input_field']
+ return "http://%s.blogspot.com/" % blog_name
+
+class BloggerAuthContext(ConsumerTemplateContext):
+ mode = 'SMALLICON'
+ type = 'SIMPLE_FORM'
+ simple_form_context = {
+ 'your_what': 'Blogger blog name'
+ }
+ weight = 300
+ human_name = 'Blogger'
+ icon = '/media/images/openid/blogger.ico'
+
+
+class LiveJournalAuthConsumer(OpenIdAbstractAuthConsumer):
+ def get_user_url(self, request):
+ blog_name = request.POST['input_field']
+ return "http://%s.livejournal.com/" % blog_name
+
+class LiveJournalAuthContext(ConsumerTemplateContext):
+ mode = 'SMALLICON'
+ type = 'SIMPLE_FORM'
+ simple_form_context = {
+ 'your_what': 'LiveJournal blog name'
+ }
+ weight = 310
+ human_name = 'LiveJournal'
+ icon = '/media/images/openid/livejournal.ico'
+
+
+class ClaimIdAuthConsumer(OpenIdAbstractAuthConsumer):
+ def get_user_url(self, request):
+ blog_name = request.POST['input_field']
+ return "http://claimid.com/%s" % blog_name
+
+class ClaimIdAuthContext(ConsumerTemplateContext):
+ mode = 'SMALLICON'
+ type = 'SIMPLE_FORM'
+ simple_form_context = {
+ 'your_what': 'ClaimID user name'
+ }
+ weight = 320
+ human_name = 'ClaimID'
+ icon = '/media/images/openid/claimid.ico'
+
+class VidoopAuthConsumer(OpenIdAbstractAuthConsumer):
+ def get_user_url(self, request):
+ blog_name = request.POST['input_field']
+ return "http://%s.myvidoop.com/" % blog_name
+
+class VidoopAuthContext(ConsumerTemplateContext):
+ mode = 'SMALLICON'
+ type = 'SIMPLE_FORM'
+ simple_form_context = {
+ 'your_what': 'Vidoop user name'
+ }
+ weight = 330
+ human_name = 'Vidoop'
+ icon = '/media/images/openid/vidoop.ico'
+
+class VerisignAuthConsumer(OpenIdAbstractAuthConsumer):
+ def get_user_url(self, request):
+ blog_name = request.POST['input_field']
+ return "http://%s.pip.verisignlabs.com/" % blog_name
+
+class VerisignAuthContext(ConsumerTemplateContext):
+ mode = 'SMALLICON'
+ type = 'SIMPLE_FORM'
+ simple_form_context = {
+ 'your_what': 'Verisign user name'
+ }
+ weight = 340
+ human_name = 'Verisign'
+ icon = '/media/images/openid/verisign.ico'
+
+
+class OpenIdUrlAuthConsumer(OpenIdAbstractAuthConsumer):
+ pass
+
+class OpenIdUrlAuthContext(ConsumerTemplateContext):
+ mode = 'STACK_ITEM'
+ weight = 300
+ human_name = 'OpenId url'
+ stack_item_template = 'modules/openidauth/openidurl.html'
+ icon = '/media/images/openid/openid-inputicon.gif'
\ No newline at end of file
diff --git a/forum_modules/openidauth/consumer.py b/forum_modules/openidauth/consumer.py
new file mode 100755
index 0000000..17d1378
--- /dev/null
+++ b/forum_modules/openidauth/consumer.py
@@ -0,0 +1,112 @@
+from django.utils.html import escape
+from django.http import get_host
+
+from forum.authentication.base import AuthenticationConsumer, InvalidAuthentication
+import settings
+
+from openid.yadis import xri
+from openid.consumer.consumer import Consumer, SUCCESS, CANCEL, FAILURE, SETUP_NEEDED
+from openid.consumer.discover import DiscoveryFailure
+from openid.extensions.sreg import SRegRequest, SRegResponse
+from openid.extensions.ax import FetchRequest as AXFetchRequest, AttrInfo, FetchResponse as AXFetchResponse
+from django.utils.translation import ugettext as _
+
+from store import OsqaOpenIDStore
+
+class OpenIdAbstractAuthConsumer(AuthenticationConsumer):
+
+ def get_user_url(self, request):
+ try:
+ return request.POST['openid_identifier']
+ except:
+ raise NotImplementedError()
+
+ def prepare_authentication_request(self, request, redirect_to):
+ if not redirect_to.startswith('http://') or redirect_to.startswith('https://'):
+ redirect_to = get_url_host(request) + redirect_to
+
+ user_url = self.get_user_url(request)
+
+ if xri.identifierScheme(user_url) == 'XRI' and getattr(
+ settings, 'OPENID_DISALLOW_INAMES', False
+ ):
+ raise InvalidAuthentication('i-names are not supported')
+
+ consumer = Consumer(request.session, OsqaOpenIDStore())
+
+ try:
+ auth_request = consumer.begin(user_url)
+ except DiscoveryFailure:
+ raise InvalidAuthentication(_('Sorry, but your input is not a valid OpenId'))
+
+ #sreg = getattr(settings, 'OPENID_SREG', False)
+
+ #if sreg:
+ # s = SRegRequest()
+ # for sarg in sreg:
+ # if sarg.lower().lstrip() == "policy_url":
+ # s.policy_url = sreg[sarg]
+ # else:
+ # for v in sreg[sarg].split(','):
+ # s.requestField(field_name=v.lower().lstrip(), required=(sarg.lower().lstrip() == "required"))
+ # auth_request.addExtension(s)
+
+ #auth_request.addExtension(SRegRequest(required=['email']))
+
+ if request.session.get('force_email_request', True):
+ axr = AXFetchRequest()
+ axr.add(AttrInfo("http://axschema.org/contact/email", 1, True, "email"))
+ auth_request.addExtension(axr)
+
+ trust_root = getattr(
+ settings, 'OPENID_TRUST_ROOT', get_url_host(request) + '/'
+ )
+
+
+ return auth_request.redirectURL(trust_root, redirect_to)
+
+ def process_authentication_request(self, request):
+ consumer = Consumer(request.session, OsqaOpenIDStore())
+
+ query_dict = dict([
+ (k.encode('utf8'), v.encode('utf8')) for k, v in request.GET.items()
+ ])
+
+ #for i in query_dict.items():
+ # print "%s : %s" % i
+
+ url = get_url_host(request) + request.path
+ openid_response = consumer.complete(query_dict, url)
+
+ if openid_response.status == SUCCESS:
+ if request.session.get('force_email_request', True):
+ try:
+ ax = AXFetchResponse.fromSuccessResponse(openid_response)
+ email = ax.getExtensionArgs()['value.ext0.1']
+ request.session['auth_email_request'] = email
+ except Exception, e:
+ pass
+
+ return request.GET['openid.identity']
+ elif openid_response.status == CANCEL:
+ raise InvalidAuthentication(_('The OpenId authentication request was canceled'))
+ elif openid_response.status == FAILURE:
+ raise InvalidAuthentication(_('The OpenId authentication failed: ') + openid_response.message)
+ elif openid_response.status == SETUP_NEEDED:
+ raise InvalidAuthentication(_('Setup needed'))
+ else:
+ raise InvalidAuthentication(_('The OpenId authentication failed with an unknown status: ') + openid_response.status)
+
+ def get_user_data(self, key):
+ return {}
+
+def get_url_host(request):
+ if request.is_secure():
+ protocol = 'https'
+ else:
+ protocol = 'http'
+ host = escape(get_host(request))
+ return '%s://%s' % (protocol, host)
+
+def get_full_url(request):
+ return get_url_host(request) + request.get_full_path()
\ No newline at end of file
diff --git a/forum_modules/openidauth/models.py b/forum_modules/openidauth/models.py
new file mode 100755
index 0000000..d6cc991
--- /dev/null
+++ b/forum_modules/openidauth/models.py
@@ -0,0 +1,26 @@
+from django.db import models
+
+class OpenIdNonce(models.Model):
+ server_url = models.URLField()
+ timestamp = models.IntegerField()
+ salt = models.CharField( max_length=50 )
+
+ def __unicode__(self):
+ return "Nonce: %s" % self.nonce
+
+ class Meta:
+ app_label = 'forum'
+
+class OpenIdAssociation(models.Model):
+ server_url = models.TextField(max_length=2047)
+ handle = models.CharField(max_length=255)
+ secret = models.TextField(max_length=255) # Stored base64 encoded
+ issued = models.IntegerField()
+ lifetime = models.IntegerField()
+ assoc_type = models.TextField(max_length=64)
+
+ def __unicode__(self):
+ return "Association: %s, %s" % (self.server_url, self.handle)
+
+ class Meta:
+ app_label = 'forum'
diff --git a/forum_modules/openidauth/settings.py b/forum_modules/openidauth/settings.py
new file mode 100755
index 0000000..3b1c2ee
--- /dev/null
+++ b/forum_modules/openidauth/settings.py
@@ -0,0 +1,9 @@
+OPENID_SREG = {
+ "required": "nickname, email",
+ "optional": "postcode, country",
+ "policy_url": ""
+}
+OPENID_AX = [
+ {"type_uri": "http://axschema.org/contact/email", "count": 1, "required": True, "alias": "email"},
+ {"type_uri": "fullname", "count":1 , "required": False, "alias": "fullname"}
+ ]
\ No newline at end of file
diff --git a/forum_modules/openidauth/store.py b/forum_modules/openidauth/store.py
new file mode 100755
index 0000000..93d38a5
--- /dev/null
+++ b/forum_modules/openidauth/store.py
@@ -0,0 +1,79 @@
+import time, base64, md5
+
+from openid.store import nonce as oid_nonce
+from openid.store.interface import OpenIDStore
+from openid.association import Association as OIDAssociation
+from django.conf import settings
+
+from models import OpenIdNonce as Nonce, OpenIdAssociation as Association
+
+class OsqaOpenIDStore(OpenIDStore):
+ def __init__(self):
+ self.max_nonce_age = 6 * 60 * 60 # Six hours
+
+ def storeAssociation(self, server_url, association):
+ assoc = Association(
+ server_url = server_url,
+ handle = association.handle,
+ secret = base64.encodestring(association.secret),
+ issued = association.issued,
+ lifetime = association.issued,
+ assoc_type = association.assoc_type
+ )
+ assoc.save()
+
+ def getAssociation(self, server_url, handle=None):
+ assocs = []
+ if handle is not None:
+ assocs = Association.objects.filter(
+ server_url = server_url, handle = handle
+ )
+ else:
+ assocs = Association.objects.filter(
+ server_url = server_url
+ )
+ if not assocs:
+ return None
+ associations = []
+ for assoc in assocs:
+ association = OIDAssociation(
+ assoc.handle, base64.decodestring(assoc.secret), assoc.issued,
+ assoc.lifetime, assoc.assoc_type
+ )
+ if association.getExpiresIn() == 0:
+ self.removeAssociation(server_url, assoc.handle)
+ else:
+ associations.append((association.issued, association))
+ if not associations:
+ return None
+ return associations[-1][1]
+
+ def removeAssociation(self, server_url, handle):
+ assocs = list(Association.objects.filter(
+ server_url = server_url, handle = handle
+ ))
+ assocs_exist = len(assocs) > 0
+ for assoc in assocs:
+ assoc.delete()
+ return assocs_exist
+
+ def storeNonce(self, nonce):
+ nonce, created = Nonce.objects.get_or_create(
+ nonce = nonce, defaults={'expires': int(time.time())}
+ )
+
+ def useNonce(self, server_url, timestamp, salt):
+ if abs(timestamp - time.time()) > oid_nonce.SKEW:
+ return False
+
+ try:
+ nonce = Nonce( server_url=server_url, timestamp=timestamp, salt=salt)
+ nonce.save()
+ except:
+ raise
+ else:
+ return 1
+
+ def getAuthKey(self):
+ # Use first AUTH_KEY_LEN characters of md5 hash of SECRET_KEY
+ return md5.new(settings.SECRET_KEY).hexdigest()[:self.AUTH_KEY_LEN]
diff --git a/forum_modules/openidauth/templates/openidurl.html b/forum_modules/openidauth/templates/openidurl.html
new file mode 100755
index 0000000..cd4e77d
--- /dev/null
+++ b/forum_modules/openidauth/templates/openidurl.html
@@ -0,0 +1,20 @@
+{% load i18n %}
+{% load extra_tags %}
+
+
+
diff --git a/forum_modules/pgfulltext/DISABLED b/forum_modules/pgfulltext/DISABLED
new file mode 100755
index 0000000..e69de29
diff --git a/forum_modules/pgfulltext/__init__.py b/forum_modules/pgfulltext/__init__.py
new file mode 100755
index 0000000..ec4892c
--- /dev/null
+++ b/forum_modules/pgfulltext/__init__.py
@@ -0,0 +1,9 @@
+NAME = 'Postgresql Full Text Search'
+DESCRIPTION = "Enables PostgreSql full text search functionality."
+
+try:
+ import psycopg2
+ CAN_ENABLE = True
+except:
+ CAN_ENABLE = False
+
\ No newline at end of file
diff --git a/forum_modules/pgfulltext/handlers.py b/forum_modules/pgfulltext/handlers.py
new file mode 100755
index 0000000..17fb176
--- /dev/null
+++ b/forum_modules/pgfulltext/handlers.py
@@ -0,0 +1,11 @@
+from forum.models import Question
+
+def question_search(keywords, orderby):
+ return Question.objects.filter(deleted=False).extra(
+ select={
+ 'ranking': "ts_rank_cd(tsv, plainto_tsquery(%s), 32)",
+ },
+ where=["tsv @@ plainto_tsquery(%s)"],
+ params=[keywords],
+ select_params=[keywords]
+ ).order_by(orderby, '-ranking')
\ No newline at end of file
diff --git a/forum_modules/pgfulltext/management.py b/forum_modules/pgfulltext/management.py
new file mode 100755
index 0000000..89eb139
--- /dev/null
+++ b/forum_modules/pgfulltext/management.py
@@ -0,0 +1,29 @@
+import os
+
+from django.db import connection, transaction
+from django.conf import settings
+
+import forum.models
+
+if settings.DATABASE_ENGINE in ('postgresql_psycopg2', 'postgresql', ):
+ from django.db.models.signals import post_syncdb
+
+ def setup_pgfulltext(sender, **kwargs):
+ if sender == forum.models:
+ install_pg_fts()
+
+ post_syncdb.connect(setup_pgfulltext)
+
+def install_pg_fts():
+ f = open(os.path.join(os.path.dirname(__file__), 'pg_fts_install.sql'), 'r')
+
+ try:
+ cursor = connection.cursor()
+ cursor.execute(f.read())
+ transaction.commit_unless_managed()
+ except:
+ pass
+ finally:
+ cursor.close()
+
+ f.close()
diff --git a/forum_modules/pgfulltext/pg_fts_install.sql b/forum_modules/pgfulltext/pg_fts_install.sql
new file mode 100755
index 0000000..72eca51
--- /dev/null
+++ b/forum_modules/pgfulltext/pg_fts_install.sql
@@ -0,0 +1,38 @@
+ALTER TABLE question ADD COLUMN tsv tsvector;
+
+CREATE OR REPLACE FUNCTION public.create_plpgsql_language ()
+ RETURNS TEXT
+ AS $$
+ CREATE LANGUAGE plpgsql;
+ SELECT 'language plpgsql created'::TEXT;
+ $$
+LANGUAGE 'sql';
+
+SELECT CASE WHEN
+ (SELECT true::BOOLEAN
+ FROM pg_language
+ WHERE lanname='plpgsql')
+ THEN
+ (SELECT 'language already installed'::TEXT)
+ ELSE
+ (SELECT public.create_plpgsql_language())
+ END;
+
+DROP FUNCTION public.create_plpgsql_language ();
+
+CREATE OR REPLACE FUNCTION set_question_tsv() RETURNS TRIGGER AS $$
+begin
+ new.tsv :=
+ setweight(to_tsvector('english', coalesce(new.tagnames,'')), 'A') ||
+ setweight(to_tsvector('english', coalesce(new.title,'')), 'B') ||
+ setweight(to_tsvector('english', coalesce(new.summary,'')), 'C');
+ RETURN new;
+end
+$$ LANGUAGE plpgsql;
+
+CREATE TRIGGER tsvectorupdate BEFORE INSERT OR UPDATE
+ON question FOR EACH ROW EXECUTE PROCEDURE set_question_tsv();
+
+ CREATE INDEX question_tsv ON question USING gin(tsv);
+
+UPDATE question SET title = title;
diff --git a/forum_modules/sphinxfulltext/DISABLED b/forum_modules/sphinxfulltext/DISABLED
new file mode 100755
index 0000000..e69de29
diff --git a/forum_modules/sphinxfulltext/__init__.py b/forum_modules/sphinxfulltext/__init__.py
new file mode 100755
index 0000000..e69de29
diff --git a/forum_modules/sphinxfulltext/dependencies.py b/forum_modules/sphinxfulltext/dependencies.py
new file mode 100755
index 0000000..5ddb91d
--- /dev/null
+++ b/forum_modules/sphinxfulltext/dependencies.py
@@ -0,0 +1,2 @@
+DJANGO_APPS = ('djangosphinx', )
+
diff --git a/forum_modules/sphinxfulltext/handlers.py b/forum_modules/sphinxfulltext/handlers.py
new file mode 100755
index 0000000..665c938
--- /dev/null
+++ b/forum_modules/sphinxfulltext/handlers.py
@@ -0,0 +1,4 @@
+from forum.models import Question
+
+def question_search(keywords, orderby):
+ return Question.search.query(keywords)
\ No newline at end of file
diff --git a/forum_modules/sphinxfulltext/models.py b/forum_modules/sphinxfulltext/models.py
new file mode 100755
index 0000000..66b8ddf
--- /dev/null
+++ b/forum_modules/sphinxfulltext/models.py
@@ -0,0 +1,10 @@
+from forum.models import Question
+from django.conf import settings
+from djangosphinx.manager import SphinxSearch
+
+
+Question.add_to_class('search', SphinxSearch(
+ index=' '.join(settings.SPHINX_SEARCH_INDICES),
+ mode='SPH_MATCH_ALL',
+ )
+ )
diff --git a/forum_modules/sphinxfulltext/settings.py b/forum_modules/sphinxfulltext/settings.py
new file mode 100755
index 0000000..c98de7b
--- /dev/null
+++ b/forum_modules/sphinxfulltext/settings.py
@@ -0,0 +1,5 @@
+SPHINX_API_VERSION = 0x113 #refer to djangosphinx documentation
+SPHINX_SEARCH_INDICES=('osqa',) #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/locale/en/LC_MESSAGES/django.mo b/locale/en/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..38120e4
Binary files /dev/null and b/locale/en/LC_MESSAGES/django.mo differ
diff --git a/locale/en/LC_MESSAGES/django.po b/locale/en/LC_MESSAGES/django.po
new file mode 100644
index 0000000..bfec60c
--- /dev/null
+++ b/locale/en/LC_MESSAGES/django.po
@@ -0,0 +1,3496 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR , YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2010-02-08 18:43-0500\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: LANGUAGE \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: django_authopenid/forms.py:71 django_authopenid/views.py:118
+msgid "i-names are not supported"
+msgstr ""
+
+#: django_authopenid/forms.py:134
+msgid "Account with this name already exists on the forum"
+msgstr ""
+
+#: django_authopenid/forms.py:135
+msgid "can't have two logins to the same account yet, sorry."
+msgstr ""
+
+#: django_authopenid/forms.py:157
+msgid "Please enter valid username and password (both are case-sensitive)."
+msgstr ""
+
+#: django_authopenid/forms.py:160 django_authopenid/forms.py:210
+msgid "This account is inactive."
+msgstr ""
+
+#: django_authopenid/forms.py:162
+msgid "Login failed."
+msgstr ""
+
+#: django_authopenid/forms.py:164
+msgid "Please enter username and password"
+msgstr ""
+
+#: django_authopenid/forms.py:166
+msgid "Please enter your password"
+msgstr ""
+
+#: django_authopenid/forms.py:168
+msgid "Please enter user name"
+msgstr ""
+
+#: django_authopenid/forms.py:206
+msgid ""
+"Please enter a valid username and password. Note that "
+"both fields are case-sensitive."
+msgstr ""
+
+#: django_authopenid/forms.py:229
+msgid "Current password"
+msgstr ""
+
+#: django_authopenid/forms.py:240
+msgid ""
+"Old password is incorrect. Please enter the correct "
+"password."
+msgstr ""
+
+#: django_authopenid/forms.py:305
+msgid "Your user name (required)"
+msgstr ""
+
+#: django_authopenid/forms.py:320
+msgid "Incorrect username."
+msgstr "sorry, there is no such user name"
+
+#: django_authopenid/urls.py:23 django_authopenid/urls.py:24
+#: django_authopenid/urls.py:25 django_authopenid/urls.py:27
+#: fbconnect/urls.py:12 fbconnect/urls.py:13 fbconnect/urls.py:14
+#: forum/urls.py:29
+msgid "signin/"
+msgstr ""
+
+#: django_authopenid/urls.py:24 fbconnect/urls.py:13 fbconnect/urls.py:17
+msgid "newquestion/"
+msgstr ""
+
+#: django_authopenid/urls.py:25 fbconnect/urls.py:14 fbconnect/urls.py:18
+msgid "newanswer/"
+msgstr ""
+
+#: django_authopenid/urls.py:26
+msgid "signout/"
+msgstr ""
+
+#: django_authopenid/urls.py:27
+msgid "complete/"
+msgstr ""
+
+#: django_authopenid/urls.py:29 fbconnect/urls.py:16 fbconnect/urls.py:17
+#: fbconnect/urls.py:18
+msgid "register/"
+msgstr ""
+
+#: django_authopenid/urls.py:30
+msgid "signup/"
+msgstr ""
+
+#: django_authopenid/urls.py:32
+msgid "sendpw/"
+msgstr ""
+
+#: django_authopenid/urls.py:33 django_authopenid/urls.py:37
+msgid "password/"
+msgstr ""
+
+#: django_authopenid/urls.py:33
+msgid "confirm/"
+msgstr ""
+
+#: django_authopenid/urls.py:36
+msgid "account_settings"
+msgstr ""
+
+#: django_authopenid/urls.py:38 django_authopenid/urls.py:39
+#: django_authopenid/urls.py:40 django_authopenid/urls.py:41
+msgid "email/"
+msgstr ""
+
+#: django_authopenid/urls.py:38
+msgid "validate/"
+msgstr ""
+
+#: django_authopenid/urls.py:39
+msgid "change/"
+msgstr ""
+
+#: django_authopenid/urls.py:40
+msgid "sendkey/"
+msgstr ""
+
+#: django_authopenid/urls.py:41
+msgid "verify/"
+msgstr ""
+
+#: django_authopenid/urls.py:42
+msgid "openid/"
+msgstr ""
+
+#: django_authopenid/urls.py:43 forum/urls.py:49 forum/urls.py:53
+msgid "delete/"
+msgstr ""
+
+#: django_authopenid/urls.py:51
+msgid "external-login/forgot-password/"
+msgstr ""
+
+#: django_authopenid/urls.py:54
+msgid "external-login/signup/"
+msgstr ""
+
+#: django_authopenid/views.py:125
+#, python-format
+msgid "OpenID %(openid_url)s is invalid"
+msgstr ""
+
+#: django_authopenid/views.py:593
+msgid "Welcome email subject line"
+msgstr "Welcome to the Q&A forum"
+
+#: django_authopenid/views.py:699
+msgid "Password changed."
+msgstr ""
+
+#: django_authopenid/views.py:711 django_authopenid/views.py:717
+#, python-format
+msgid "your email needs to be validated see %(details_url)s"
+msgstr ""
+"Your email needs to be validated. Please see details here."
+
+#: django_authopenid/views.py:738
+msgid "Email verification subject line"
+msgstr "Verification Email from Q&A forum"
+
+#: django_authopenid/views.py:829
+msgid "your email was not changed"
+msgstr ""
+
+#: django_authopenid/views.py:877 django_authopenid/views.py:1035
+#, python-format
+msgid "No OpenID %s found associated in our database"
+msgstr ""
+
+#: django_authopenid/views.py:881 django_authopenid/views.py:1042
+#, python-format
+msgid "The OpenID %s isn't associated to current user logged in"
+msgstr ""
+
+#: django_authopenid/views.py:889
+msgid "Email Changed."
+msgstr ""
+
+#: django_authopenid/views.py:967
+msgid "This OpenID is already associated with another account."
+msgstr ""
+
+#: django_authopenid/views.py:972
+#, python-format
+msgid "OpenID %s is now associated with your account."
+msgstr ""
+
+#: django_authopenid/views.py:1045
+msgid "Account deleted."
+msgstr ""
+
+#: django_authopenid/views.py:1097
+msgid "Request for new password"
+msgstr ""
+
+#: django_authopenid/views.py:1111
+msgid "A new password and the activation link were sent to your email address."
+msgstr ""
+
+#: django_authopenid/views.py:1143
+#, python-format
+msgid ""
+"Could not change password. Confirmation key '%s' is not "
+"registered."
+msgstr ""
+
+#: django_authopenid/views.py:1153
+msgid ""
+"Can not change password. User don't exist anymore in our "
+"database."
+msgstr ""
+
+#: django_authopenid/views.py:1163
+#, python-format
+msgid "Password changed for %s. You may now sign in."
+msgstr ""
+
+#: forum/auth.py:484
+msgid "Your question and all of it's answers have been deleted"
+msgstr ""
+
+#: forum/auth.py:486
+msgid "Your question has been deleted"
+msgstr ""
+
+#: forum/auth.py:489
+msgid "The question and all of it's answers have been deleted"
+msgstr ""
+
+#: forum/auth.py:491
+msgid "The question has been deleted"
+msgstr ""
+
+#: forum/const.py:8
+msgid "duplicate question"
+msgstr ""
+
+#: forum/const.py:9
+msgid "question is off-topic or not relevant"
+msgstr ""
+
+#: forum/const.py:10
+msgid "too subjective and argumentative"
+msgstr ""
+
+#: forum/const.py:11
+msgid "is not an answer to the question"
+msgstr ""
+
+#: forum/const.py:12
+msgid "the question is answered, right answer was accepted"
+msgstr ""
+
+#: forum/const.py:13
+msgid "problem is not reproducible or outdated"
+msgstr ""
+
+#: forum/const.py:15
+msgid "question contains offensive inappropriate, or malicious remarks"
+msgstr ""
+
+#: forum/const.py:16
+msgid "spam or advertising"
+msgstr ""
+
+#: forum/const.py:57
+msgid "question"
+msgstr ""
+
+#: forum/const.py:58 templates/book.html:110
+msgid "answer"
+msgstr ""
+
+#: forum/const.py:59
+msgid "commented question"
+msgstr ""
+
+#: forum/const.py:60
+msgid "commented answer"
+msgstr ""
+
+#: forum/const.py:61
+msgid "edited question"
+msgstr ""
+
+#: forum/const.py:62
+msgid "edited answer"
+msgstr ""
+
+#: forum/const.py:63
+msgid "received award"
+msgstr "received badge"
+
+#: forum/const.py:64
+msgid "marked best answer"
+msgstr ""
+
+#: forum/const.py:65
+msgid "upvoted"
+msgstr ""
+
+#: forum/const.py:66
+msgid "downvoted"
+msgstr ""
+
+#: forum/const.py:67
+msgid "canceled vote"
+msgstr ""
+
+#: forum/const.py:68
+msgid "deleted question"
+msgstr ""
+
+#: forum/const.py:69
+msgid "deleted answer"
+msgstr ""
+
+#: forum/const.py:70
+msgid "marked offensive"
+msgstr ""
+
+#: forum/const.py:71
+msgid "updated tags"
+msgstr ""
+
+#: forum/const.py:72
+msgid "selected favorite"
+msgstr ""
+
+#: forum/const.py:73
+msgid "completed user profile"
+msgstr ""
+
+#: forum/const.py:74
+msgid "email update sent to user"
+msgstr ""
+
+#: forum/const.py:85
+msgid "[closed]"
+msgstr ""
+
+#: forum/const.py:86
+msgid "[deleted]"
+msgstr ""
+
+#: forum/const.py:87 forum/views.py:796 forum/views.py:815
+msgid "initial version"
+msgstr ""
+
+#: forum/const.py:88
+msgid "retagged"
+msgstr ""
+
+#: forum/const.py:92
+msgid "exclude ignored tags"
+msgstr ""
+
+#: forum/const.py:92
+msgid "allow only selected tags"
+msgstr ""
+
+#: forum/feed.py:18
+msgid " - "
+msgstr ""
+
+#: forum/feed.py:18
+msgid "latest questions"
+msgstr ""
+
+#: forum/forms.py:18 templates/answer_edit_tips.html:35
+#: templates/answer_edit_tips.html.py:39 templates/question_edit_tips.html:32
+#: templates/question_edit_tips.html:37
+msgid "title"
+msgstr ""
+
+#: forum/forms.py:19
+msgid "please enter a descriptive title for your question"
+msgstr ""
+
+#: forum/forms.py:24
+msgid "title must be > 10 characters"
+msgstr ""
+
+#: forum/forms.py:33
+msgid "content"
+msgstr ""
+
+#: forum/forms.py:39
+msgid "question content must be > 10 characters"
+msgstr ""
+
+#: forum/forms.py:49 templates/header.html:28 templates/header.html.py:56
+msgid "tags"
+msgstr ""
+
+#: forum/forms.py:51
+msgid ""
+"Tags are short keywords, with no spaces within. Up to five tags can be used."
+msgstr ""
+
+#: forum/forms.py:58 templates/question_retag.html:39
+msgid "tags are required"
+msgstr ""
+
+#: forum/forms.py:64
+msgid "please use 5 tags or less"
+msgstr ""
+
+#: forum/forms.py:67
+msgid "tags must be shorter than 20 characters"
+msgstr ""
+
+#: forum/forms.py:71
+msgid ""
+"please use following characters in tags: letters 'a-z', numbers, and "
+"characters '.-_#'"
+msgstr ""
+
+#: forum/forms.py:81 templates/index.html:61 templates/index.html.py:73
+#: templates/post_contributor_info.html:7
+#: templates/question_summary_list_roll.html:26
+#: templates/question_summary_list_roll.html:38 templates/questions.html:92
+#: templates/questions.html.py:104
+msgid "community wiki"
+msgstr ""
+
+#: forum/forms.py:82
+msgid ""
+"if you choose community wiki option, the question and answer do not generate "
+"points and name of author will not be shown"
+msgstr ""
+
+#: forum/forms.py:98
+msgid "update summary:"
+msgstr ""
+
+#: forum/forms.py:99
+msgid ""
+"enter a brief summary of your revision (e.g. fixed spelling, grammar, "
+"improved style, this field is optional)"
+msgstr ""
+
+#: forum/forms.py:102
+msgid "Automatically accept user's contributions for the email updates"
+msgstr ""
+
+#: forum/forms.py:118
+msgid "Your name:"
+msgstr ""
+
+#: forum/forms.py:119
+msgid "Email (not shared with anyone):"
+msgstr ""
+
+#: forum/forms.py:120
+msgid "Your message:"
+msgstr ""
+
+#: forum/forms.py:202
+msgid "this email does not have to be linked to gravatar"
+msgstr ""
+
+#: forum/forms.py:204
+msgid "Screen name"
+msgstr ""
+
+#: forum/forms.py:205
+msgid "Real name"
+msgstr ""
+
+#: forum/forms.py:206
+msgid "Website"
+msgstr ""
+
+#: forum/forms.py:207
+msgid "Location"
+msgstr ""
+
+#: forum/forms.py:208
+msgid "Date of birth"
+msgstr ""
+
+#: forum/forms.py:208
+msgid "will not be shown, used to calculate age, format: YYYY-MM-DD"
+msgstr ""
+
+#: forum/forms.py:209 templates/authopenid/settings.html:21
+msgid "Profile"
+msgstr ""
+
+#: forum/forms.py:240 forum/forms.py:241
+msgid "this email has already been registered, please use another one"
+msgstr ""
+
+#: forum/forms.py:247
+msgid "Choose email tag filter"
+msgstr ""
+
+#: forum/forms.py:262 forum/forms.py:263
+msgid "weekly"
+msgstr ""
+
+#: forum/forms.py:262 forum/forms.py:263
+msgid "no email"
+msgstr ""
+
+#: forum/forms.py:263
+msgid "daily"
+msgstr ""
+
+#: forum/forms.py:278
+msgid "Asked by me"
+msgstr ""
+
+#: forum/forms.py:281
+msgid "Answered by me"
+msgstr ""
+
+#: forum/forms.py:284
+msgid "Individually selected"
+msgstr ""
+
+#: forum/forms.py:287
+msgid "Entire forum (tag filtered)"
+msgstr ""
+
+#: forum/forms.py:341
+msgid "okay, let's try!"
+msgstr ""
+
+#: forum/forms.py:342
+msgid "no OSQA community email please, thanks"
+msgstr ""
+
+#: forum/forms.py:345
+msgid "please choose one of the options above"
+msgstr ""
+
+#: forum/models.py:52
+msgid "Entire forum"
+msgstr ""
+
+#: forum/models.py:53
+msgid "Questions that I asked"
+msgstr ""
+
+#: forum/models.py:54
+msgid "Questions that I answered"
+msgstr ""
+
+#: forum/models.py:55
+msgid "Individually selected questions"
+msgstr ""
+
+#: forum/models.py:58
+msgid "Weekly"
+msgstr ""
+
+#: forum/models.py:59
+msgid "Daily"
+msgstr ""
+
+#: forum/models.py:60
+msgid "No email"
+msgstr ""
+
+#: forum/models.py:321
+#, python-format
+msgid "%(author)s modified the question"
+msgstr ""
+
+#: forum/models.py:325
+#, python-format
+msgid "%(people)s posted %(new_answer_count)s new answers"
+msgstr ""
+
+#: forum/models.py:330
+#, python-format
+msgid "%(people)s commented the question"
+msgstr ""
+
+#: forum/models.py:335
+#, python-format
+msgid "%(people)s commented answers"
+msgstr ""
+
+#: forum/models.py:337
+#, python-format
+msgid "%(people)s commented an answer"
+msgstr ""
+
+#: forum/models.py:368
+msgid "interesting"
+msgstr ""
+
+#: forum/models.py:368
+msgid "ignored"
+msgstr ""
+
+#: forum/models.py:541 templates/badges.html:53
+msgid "gold"
+msgstr ""
+
+#: forum/models.py:542 templates/badges.html:61
+msgid "silver"
+msgstr ""
+
+#: forum/models.py:543 templates/badges.html:68
+msgid "bronze"
+msgstr ""
+
+#: forum/urls.py:26
+msgid "upfiles/"
+msgstr ""
+
+#: forum/urls.py:30
+msgid "about/"
+msgstr ""
+
+#: forum/urls.py:31
+msgid "faq/"
+msgstr ""
+
+#: forum/urls.py:32
+msgid "privacy/"
+msgstr ""
+
+#: forum/urls.py:33
+msgid "logout/"
+msgstr ""
+
+#: forum/urls.py:34 forum/urls.py:35 forum/urls.py:36 forum/urls.py:53
+msgid "answers/"
+msgstr ""
+
+#: forum/urls.py:34 forum/urls.py:46 forum/urls.py:49 forum/urls.py:53
+msgid "comments/"
+msgstr ""
+
+#: forum/urls.py:35 forum/urls.py:40 forum/urls.py:75
+#: templates/user_info.html:45
+msgid "edit/"
+msgstr ""
+
+#: forum/urls.py:36 forum/urls.py:45
+msgid "revisions/"
+msgstr ""
+
+#: forum/urls.py:37 forum/urls.py:38 forum/urls.py:39 forum/urls.py:40
+#: forum/urls.py:41 forum/urls.py:42 forum/urls.py:43 forum/urls.py:44
+#: forum/urls.py:45 forum/urls.py:46 forum/urls.py:49
+msgid "questions/"
+msgstr ""
+
+#: forum/urls.py:38 forum/urls.py:85
+msgid "ask/"
+msgstr ""
+
+#: forum/urls.py:39
+msgid "unanswered/"
+msgstr ""
+
+#: forum/urls.py:41
+msgid "close/"
+msgstr ""
+
+#: forum/urls.py:42
+msgid "reopen/"
+msgstr ""
+
+#: forum/urls.py:43
+msgid "answer/"
+msgstr ""
+
+#: forum/urls.py:44
+msgid "vote/"
+msgstr ""
+
+#: forum/urls.py:47
+msgid "command/"
+msgstr ""
+
+#: forum/urls.py:57 forum/views.py:440
+msgid "question/"
+msgstr ""
+
+#: forum/urls.py:58 forum/urls.py:59
+msgid "tags/"
+msgstr ""
+
+#: forum/urls.py:61 forum/urls.py:65
+msgid "mark-tag/"
+msgstr ""
+
+#: forum/urls.py:61
+msgid "interesting/"
+msgstr ""
+
+#: forum/urls.py:65
+msgid "ignored/"
+msgstr ""
+
+#: forum/urls.py:69
+msgid "unmark-tag/"
+msgstr ""
+
+#: forum/urls.py:73 forum/urls.py:75 forum/urls.py:76
+msgid "users/"
+msgstr ""
+
+#: forum/urls.py:74
+msgid "moderate-user/"
+msgstr ""
+
+#: forum/urls.py:77 forum/urls.py:78
+msgid "badges/"
+msgstr ""
+
+#: forum/urls.py:79
+msgid "messages/"
+msgstr ""
+
+#: forum/urls.py:79
+msgid "markread/"
+msgstr ""
+
+#: forum/urls.py:81
+msgid "nimda/"
+msgstr ""
+
+#: forum/urls.py:83
+msgid "upload/"
+msgstr ""
+
+#: forum/urls.py:84 forum/urls.py:85 forum/urls.py:86
+msgid "books/"
+msgstr ""
+
+#: forum/urls.py:87
+msgid "search/"
+msgstr ""
+
+#: forum/urls.py:88
+msgid "feedback/"
+msgstr ""
+
+#: forum/urls.py:89 forum/urls.py:90
+msgid "account/"
+msgstr ""
+
+#: forum/user.py:16 templates/user_tabs.html:7
+msgid "overview"
+msgstr ""
+
+#: forum/user.py:17
+msgid "user profile"
+msgstr ""
+
+#: forum/user.py:18
+msgid "user profile overview"
+msgstr ""
+
+#: forum/user.py:24 templates/user_tabs.html:9
+msgid "recent activity"
+msgstr ""
+
+#: forum/user.py:25
+msgid "recent user activity"
+msgstr ""
+
+#: forum/user.py:26
+msgid "profile - recent activity"
+msgstr ""
+
+#: forum/user.py:33 templates/user_tabs.html:13
+msgid "responses"
+msgstr ""
+
+#: forum/user.py:34 templates/user_tabs.html:12
+msgid "comments and answers to others questions"
+msgstr ""
+
+#: forum/user.py:35
+msgid "profile - responses"
+msgstr ""
+
+#: forum/user.py:42 templates/user_info.html:22 templates/users.html:26
+msgid "reputation"
+msgstr "karma"
+
+#: forum/user.py:43
+msgid "user reputation in the community"
+msgstr "user karma"
+
+#: forum/user.py:44
+msgid "profile - user reputation"
+msgstr "Profile - User's Karma"
+
+#: forum/user.py:50
+msgid "favorite questions"
+msgstr ""
+
+#: forum/user.py:51
+msgid "users favorite questions"
+msgstr ""
+
+#: forum/user.py:52
+msgid "profile - favorite questions"
+msgstr ""
+
+#: forum/user.py:59 templates/user_tabs.html:20
+msgid "casted votes"
+msgstr "votes"
+
+#: forum/user.py:60 templates/user_tabs.html:20
+msgid "user vote record"
+msgstr ""
+
+#: forum/user.py:61
+msgid "profile - votes"
+msgstr ""
+
+#: forum/user.py:68 templates/user_tabs.html:28
+msgid "email subscriptions"
+msgstr ""
+
+#: forum/user.py:69 templates/user_tabs.html:27
+msgid "email subscription settings"
+msgstr ""
+
+#: forum/user.py:70
+msgid "profile - email subscriptions"
+msgstr ""
+
+#: forum/views.py:141
+msgid "Q&A forum feedback"
+msgstr ""
+
+#: forum/views.py:142
+msgid "Thanks for the feedback!"
+msgstr ""
+
+#: forum/views.py:150
+msgid "We look forward to hearing your feedback! Please, give it next time :)"
+msgstr ""
+
+#: forum/views.py:1098
+#, 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 more details here"
+
+#: forum/views.py:1106
+msgid "email update frequency has been set to daily"
+msgstr ""
+
+#: forum/views.py:1982 forum/views.py:1986
+msgid "changes saved"
+msgstr ""
+
+#: forum/views.py:1992
+msgid "email updates canceled"
+msgstr ""
+
+#: forum/views.py:2159
+msgid "uploading images is limited to users with >60 reputation points"
+msgstr "sorry, file uploading requires karma >60"
+
+#: forum/views.py:2161
+msgid "allowed file types are 'jpg', 'jpeg', 'gif', 'bmp', 'png', 'tiff'"
+msgstr ""
+
+#: forum/views.py:2163
+#, python-format
+msgid "maximum upload file size is %sK"
+msgstr ""
+
+#: forum/views.py:2165
+#, python-format
+msgid ""
+"Error uploading file. Please contact the site administrator. Thank you. %s"
+msgstr ""
+
+#: forum/management/commands/send_email_alerts.py:156
+msgid "email update message subject"
+msgstr "news from Q&A forum"
+
+#: forum/management/commands/send_email_alerts.py:158
+#, python-format
+msgid "%(name)s, this is an update message header for a question"
+msgid_plural "%(name)s, this is an update message header for %(num)d questions"
+msgstr[0] ""
+"
Dear %(name)s,
The following question has been updated on the Q&A "
+"forum:"
+msgstr[1] ""
+"
Dear %(name)s,
The following %(num)d questions have been updated on "
+"the Q&A forum:
"
+
+#: forum/management/commands/send_email_alerts.py:169
+msgid "new question"
+msgstr ""
+
+#: forum/management/commands/send_email_alerts.py:179
+#, 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:184
+msgid ""
+"Perhaps you could look up previously sent forum reminders in your mailbox."
+msgstr ""
+
+#: forum/management/commands/send_email_alerts.py:188
+#, python-format
+msgid ""
+"go to %(link)s to change frequency of email updates or %(email)s "
+"administrator"
+msgstr ""
+"
Please remember that you can always adjust "
+"frequency of the email updates or turn them off entirely. If you believe "
+"that this message was sent in an error, please email about it the forum "
+"administrator at %(email)s.
Sincerely,
Your friendly Q&A forum "
+"server.
"
+
+#: forum/templatetags/extra_tags.py:164 forum/templatetags/extra_tags.py:193
+#: templates/header.html:33
+msgid "badges"
+msgstr ""
+
+#: forum/templatetags/extra_tags.py:165 forum/templatetags/extra_tags.py:192
+msgid "reputation points"
+msgstr "karma"
+
+#: forum/templatetags/extra_tags.py:252
+msgid "2 days ago"
+msgstr ""
+
+#: forum/templatetags/extra_tags.py:254
+msgid "yesterday"
+msgstr ""
+
+#: forum/templatetags/extra_tags.py:256
+#, python-format
+msgid "%(hr)d hour ago"
+msgid_plural "%(hr)d hours ago"
+msgstr[0] ""
+msgstr[1] ""
+
+#: forum/templatetags/extra_tags.py:258
+#, 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 ""
+
+#: templates/404.html:26
+msgid "This might have happened for the following reasons:"
+msgstr ""
+
+#: templates/404.html:28
+msgid "this question or answer has been deleted;"
+msgstr ""
+
+#: templates/404.html:29
+msgid "url has error - please check it;"
+msgstr ""
+
+#: templates/404.html:30
+msgid ""
+"the page you tried to visit is protected or you don't have sufficient "
+"points, see"
+msgstr ""
+
+#: templates/404.html:31
+msgid "if you believe this error 404 should not have occured, please"
+msgstr ""
+
+#: templates/404.html:32
+msgid "report this problem"
+msgstr ""
+
+#: templates/404.html:41 templates/500.html:27
+msgid "back to previous page"
+msgstr ""
+
+#: templates/404.html:42
+msgid "see all questions"
+msgstr ""
+
+#: templates/404.html:43
+msgid "see all tags"
+msgstr ""
+
+#: templates/500.html:22
+msgid "sorry, system error"
+msgstr ""
+
+#: templates/500.html:24
+msgid "system error log is recorded, error will be fixed as soon as possible"
+msgstr ""
+
+#: templates/500.html:25
+msgid "please report the error to the site administrators if you wish"
+msgstr ""
+
+#: templates/500.html:28
+msgid "see latest questions"
+msgstr ""
+
+#: templates/500.html:29
+msgid "see tags"
+msgstr ""
+
+#: templates/about.html:6 templates/about.html.py:11
+msgid "About"
+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:25
+#: templates/question_edit.html.py:28
+msgid "hide preview"
+msgstr ""
+
+#: templates/answer_edit.html:28 templates/ask.html:29
+#: templates/question.html:48 templates/question_edit.html:28
+msgid "show preview"
+msgstr ""
+
+#: templates/answer_edit.html:48 templates/question_edit.html:66
+#: templates/question_retag.html:53 templates/revisions_answer.html:38
+#: templates/revisions_question.html:38
+msgid "back"
+msgstr ""
+
+#: templates/answer_edit.html:53 templates/question_edit.html:71
+#: templates/revisions_answer.html:52 templates/revisions_question.html:52
+msgid "revision"
+msgstr ""
+
+#: templates/answer_edit.html:56 templates/question_edit.html:75
+msgid "select revision"
+msgstr ""
+
+#: templates/answer_edit.html:63 templates/ask.html:97
+#: templates/question.html:434 templates/question_edit.html:92
+msgid "Toggle the real time Markdown editor preview"
+msgstr ""
+
+#: templates/answer_edit.html:63 templates/ask.html:97
+#: templates/question.html:435 templates/question_edit.html:92
+msgid "toggle preview"
+msgstr ""
+
+#: templates/answer_edit.html:72 templates/question_edit.html:118
+#: templates/question_retag.html:74
+msgid "Save edit"
+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:87 templates/authopenid/changeemail.html:40
+msgid "Cancel"
+msgstr ""
+
+#: templates/answer_edit_tips.html:4
+msgid "answer tips"
+msgstr "Tips"
+
+#: templates/answer_edit_tips.html:7
+msgid "please make your answer relevant to this community"
+msgstr ""
+
+#: templates/answer_edit_tips.html:10
+msgid "try to give an answer, rather than engage into a discussion"
+msgstr ""
+
+#: templates/answer_edit_tips.html:13
+msgid "please try to provide details"
+msgstr ""
+
+#: templates/answer_edit_tips.html:16 templates/question_edit_tips.html:13
+msgid "be clear and concise"
+msgstr ""
+
+#: templates/answer_edit_tips.html:20 templates/question_edit_tips.html:17
+msgid "see frequently asked questions"
+msgstr ""
+
+#: templates/answer_edit_tips.html:26 templates/question_edit_tips.html:23
+msgid "Markdown tips"
+msgstr "Markdown basics"
+
+#: templates/answer_edit_tips.html:29 templates/question_edit_tips.html:26
+msgid "*italic* or __italic__"
+msgstr ""
+
+#: templates/answer_edit_tips.html:32 templates/question_edit_tips.html:29
+msgid "**bold** or __bold__"
+msgstr ""
+
+#: templates/answer_edit_tips.html:35 templates/question_edit_tips.html:32
+msgid "link"
+msgstr ""
+
+#: templates/answer_edit_tips.html:35 templates/answer_edit_tips.html.py:39
+#: templates/question_edit_tips.html:32 templates/question_edit_tips.html:37
+msgid "text"
+msgstr ""
+
+#: templates/answer_edit_tips.html:39 templates/question_edit_tips.html:37
+msgid "image"
+msgstr ""
+
+#: templates/answer_edit_tips.html:43 templates/question_edit_tips.html:41
+msgid "numbered list:"
+msgstr ""
+
+#: templates/answer_edit_tips.html:48 templates/question_edit_tips.html:46
+msgid "basic HTML tags are also supported"
+msgstr ""
+
+#: templates/answer_edit_tips.html:52 templates/question_edit_tips.html:50
+msgid "learn more about Markdown"
+msgstr ""
+
+#: templates/ask.html:5 templates/ask.html.py:61
+msgid "Ask a question"
+msgstr ""
+
+#: templates/ask.html:68
+msgid "login to post question info"
+msgstr ""
+"You are welcome to start submitting your question "
+"anonymously. When you submit the post, you will be redirected to the "
+"login/signup page. Your question will be saved in the current session and "
+"will be published after you log in. Login/signup process is very simple. "
+"Login takes about 30 seconds, initial signup takes a minute or less."
+
+#: templates/ask.html:74
+#, python-format
+msgid ""
+"must have valid %(email)s to post, \n"
+" see %(email_validation_faq_url)s\n"
+" "
+msgstr ""
+"Looks like your email address, %(email)s has not "
+"yet been validated. To post messages you must verify your email, "
+"please see more details here."
+" You can submit your question now and validate email after that. Your "
+"question will saved as pending meanwhile. "
+
+#: templates/ask.html:112
+msgid "(required)"
+msgstr ""
+
+#: templates/ask.html:119
+msgid "Login/signup to post your question"
+msgstr "Login/Signup to Post"
+
+#: templates/ask.html:121
+msgid "Ask your question"
+msgstr "Ask Your Question"
+
+#: templates/badge.html:6 templates/badge.html.py:17
+msgid "Badge"
+msgstr ""
+
+#: templates/badge.html:26
+msgid "The users have been awarded with badges:"
+msgstr ""
+
+#: templates/badges.html:6
+msgid "Badges summary"
+msgstr ""
+
+#: templates/badges.html:17
+msgid "Badges"
+msgstr ""
+
+#: templates/badges.html:21
+msgid "Community gives you awards for your questions, answers and votes."
+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/badges.html:22
+#, python-format
+msgid ""
+"Below is the list of available badges and number \n"
+" of times each type of badge has been awarded. Give us feedback at %"
+"(feedback_faq_url)s.\n"
+" "
+msgstr ""
+"Currently badges differ only by their level: gold, "
+"silver and bronze (their meanings are "
+"described on the right). In the future there will be many types of badges at "
+"each level. Please give us your feedback - what kinds of badges would you like to see and "
+"suggest the activity for which those badges might be awarded."
+
+#: templates/badges.html:50
+msgid "Community badges"
+msgstr "Badge levels"
+
+#: templates/badges.html:56
+msgid "gold badge description"
+msgstr ""
+"Gold badge is the highest award in this community. To obtain it have to show "
+"profound knowledge and ability in addition to your active participation."
+
+#: templates/badges.html:64
+msgid "silver badge description"
+msgstr ""
+"Obtaining silver badge requires significant patience. If you have received "
+"one, that means you have greatly contributed to this community."
+
+#: templates/badges.html:67
+msgid "bronze badge: often given as a special honor"
+msgstr ""
+
+#: templates/badges.html:71
+msgid "bronze badge description"
+msgstr ""
+"If you are an active participant in this community, you will be recognized "
+"with this badge."
+
+#: templates/book.html:7
+msgid "reading channel"
+msgstr ""
+
+#: templates/book.html:26
+msgid "[author]"
+msgstr ""
+
+#: templates/book.html:30
+msgid "[publisher]"
+msgstr ""
+
+#: templates/book.html:34
+msgid "[publication date]"
+msgstr ""
+
+#: templates/book.html:38
+msgid "[price]"
+msgstr ""
+
+#: templates/book.html:39
+msgid "currency unit"
+msgstr ""
+
+#: templates/book.html:42
+msgid "[pages]"
+msgstr ""
+
+#: templates/book.html:43
+msgid "pages abbreviation"
+msgstr ""
+
+#: templates/book.html:46
+msgid "[tags]"
+msgstr ""
+
+#: templates/book.html:56
+msgid "author blog"
+msgstr ""
+
+#: templates/book.html:62
+msgid "book directory"
+msgstr ""
+
+#: templates/book.html:66
+msgid "buy online"
+msgstr ""
+
+#: templates/book.html:79
+msgid "reader questions"
+msgstr ""
+
+#: templates/book.html:82
+msgid "ask the author"
+msgstr ""
+
+#: templates/book.html:88 templates/book.html.py:93
+#: templates/users_questions.html:18
+msgid "this question was selected as favorite"
+msgstr ""
+
+#: templates/book.html:88 templates/book.html.py:93
+#: templates/users_questions.html:11 templates/users_questions.html.py:18
+msgid "number of times"
+msgstr ""
+
+#: templates/book.html:105 templates/index.html:49
+#: templates/question_summary_list_roll.html:14 templates/questions.html:80
+#: templates/users_questions.html:32
+msgid "votes"
+msgstr ""
+
+#: templates/book.html:108
+msgid "the answer has been accepted to be correct"
+msgstr ""
+
+#: templates/book.html:115 templates/index.html:50
+#: templates/question_summary_list_roll.html:15 templates/questions.html:81
+#: templates/users_questions.html:40
+msgid "views"
+msgstr ""
+
+#: templates/book.html:125 templates/index.html:105
+#: templates/question.html:480 templates/question_summary_list_roll.html:52
+#: templates/questions.html:136 templates/tags.html:49
+#: templates/users_questions.html:52
+msgid "using tags"
+msgstr ""
+
+#: templates/book.html:147
+msgid "subscribe to book RSS feed"
+msgstr ""
+
+#: templates/book.html:147 templates/index.html:156
+msgid "subscribe to the questions feed"
+msgstr ""
+
+#: templates/close.html:6 templates/close.html.py:16
+msgid "Close question"
+msgstr ""
+
+#: templates/close.html:19
+msgid "Close the question"
+msgstr ""
+
+#: templates/close.html:25
+msgid "Reasons"
+msgstr ""
+
+#: templates/close.html:28
+msgid "OK to close"
+msgstr ""
+
+#: templates/faq.html:11
+msgid "Frequently Asked Questions "
+msgstr ""
+
+#: templates/faq.html:16
+msgid "What kinds of questions can I ask here?"
+msgstr ""
+
+#: templates/faq.html:17
+msgid ""
+"Most importanly - questions should be relevant to this "
+"community."
+msgstr ""
+
+#: templates/faq.html:18
+msgid ""
+"Before asking the question - please make sure to use search to see whether "
+"your question has alredy been answered."
+msgstr ""
+"Before you ask - please make sure to search for a similar question. You can "
+"search questions by their title or tags."
+
+#: templates/faq.html:21
+msgid "What questions should I avoid asking?"
+msgstr "What kinds of questions should be avoided?"
+
+#: templates/faq.html:22
+msgid ""
+"Please avoid asking questions that are not relevant to this community, too "
+"subjective and argumentative."
+msgstr ""
+
+#: templates/faq.html:27
+msgid "What should I avoid in my answers?"
+msgstr ""
+
+#: templates/faq.html:28
+msgid ""
+"is a Q&A site, not a discussion group. Therefore - please avoid having "
+"discussions in your answers, comment facility allows some space for brief "
+"discussions."
+msgstr ""
+"is a question and answer site - it is not a "
+"discussion group. Please avoid holding debates in your answers as "
+"they tend to dilute the essense of questions and answers. For the brief "
+"discussions please use commenting facility."
+
+#: templates/faq.html:32
+msgid "Who moderates this community?"
+msgstr ""
+
+#: templates/faq.html:33
+msgid "The short answer is: you."
+msgstr ""
+
+#: templates/faq.html:34
+msgid "This website is moderated by the users."
+msgstr ""
+
+#: templates/faq.html:35
+msgid ""
+"The reputation system allows users earn the authorization to perform a "
+"variety of moderation tasks."
+msgstr ""
+"Karma system allows users to earn rights to perform a variety of moderation "
+"tasks"
+
+#: templates/faq.html:40
+msgid "How does reputation system work?"
+msgstr "How does karma system work?"
+
+#: templates/faq.html:41
+msgid "Rep system summary"
+msgstr ""
+"When a question or answer is upvoted, the user who posted them will gain "
+"some points, which are called \"karma points\". These points serve as a "
+"rough measure of the community trust to him/her. Various moderation tasks "
+"are gradually assigned to the users based on those points."
+
+#: templates/faq.html:42
+msgid ""
+"For example, if you ask an interesting question or give a helpful answer, "
+"your input will be upvoted. On the other hand if the answer is misleading - "
+"it will be downvoted. Each vote in favor will generate 10 "
+"points, each vote against will subtract 2 points. There is "
+"a limit of 200 points that can be accumulated per question "
+"or answer. The table below explains reputation point requirements for each "
+"type of moderation task."
+msgstr ""
+
+#: templates/faq.html:53 templates/user_votes.html:15
+msgid "upvote"
+msgstr ""
+
+#: templates/faq.html:57
+msgid "use tags"
+msgstr ""
+
+#: templates/faq.html:62
+msgid "add comments"
+msgstr ""
+
+#: templates/faq.html:66 templates/user_votes.html:17
+msgid "downvote"
+msgstr ""
+
+#: templates/faq.html:69
+msgid "open and close own questions"
+msgstr ""
+
+#: templates/faq.html:73
+msgid "retag questions"
+msgstr ""
+
+#: templates/faq.html:78
+msgid "edit community wiki questions"
+msgstr ""
+
+#: templates/faq.html:83
+msgid "edit any answer"
+msgstr ""
+
+#: templates/faq.html:87
+msgid "open any closed question"
+msgstr ""
+
+#: templates/faq.html:91
+msgid "delete any comment"
+msgstr ""
+
+#: templates/faq.html:95
+msgid "delete any questions and answers and perform other moderation tasks"
+msgstr ""
+
+#: templates/faq.html:102
+msgid "how to validate email title"
+msgstr "How to validate email and why?"
+
+#: templates/faq.html:104
+#, python-format
+msgid ""
+"how to validate email info with %(send_email_key_url)s %(gravatar_faq_url)s"
+msgstr ""
+"Why? Email validation is required to make sure that "
+"only you can post messages on your behalf and to "
+"minimize spam posts. With email you can "
+"subscribe for updates on the most interesting questions. "
+"Also, when you sign up for the first time - create a unique gravatar personal image."
+
+#: templates/faq.html:108
+msgid "what is gravatar"
+msgstr "What is gravatar?"
+
+#: templates/faq.html:109
+msgid "gravatar faq info"
+msgstr ""
+"Gravatar means globally r"
+"strong>ecognized avatar - your unique avatar image "
+"associated with your email address. It's simply a picture that shows next to "
+"your posts on the websites that support gravatar protocol. By default gravar "
+"appears as a square filled with a snowflake-like figure. You can set "
+"your image at gravatar.com"
+"strong>"
+
+#: templates/faq.html:112
+msgid "To register, do I need to create new password?"
+msgstr ""
+
+#: templates/faq.html:113
+msgid ""
+"No, you don't have to. You can login through any service that supports "
+"OpenID, e.g. Google, Yahoo, AOL, etc."
+msgstr ""
+
+#: templates/faq.html:114
+msgid "Login now!"
+msgstr ""
+
+#: templates/faq.html:119
+msgid "Why other people can edit my questions/answers?"
+msgstr ""
+
+#: templates/faq.html:120
+msgid "Goal of this site is..."
+msgstr ""
+
+#: templates/faq.html:120
+msgid ""
+"So questions and answers can be edited like wiki pages by experienced users "
+"of this site and this improves the overall quality of the knowledge base "
+"content."
+msgstr ""
+
+#: templates/faq.html:121
+msgid "If this approach is not for you, we respect your choice."
+msgstr ""
+
+#: templates/faq.html:125
+msgid "Still have questions?"
+msgstr ""
+
+#: templates/faq.html:126
+#, python-format
+msgid ""
+"Please ask your question at %(ask_question_url)s, help make our community "
+"better!"
+msgstr ""
+"Please ask your question, help make our "
+"community better!"
+
+#: templates/faq.html:128 templates/header.html:27 templates/header.html.py:55
+msgid "questions"
+msgstr ""
+
+#: templates/faq.html:128 templates/index.html:161
+msgid "."
+msgstr ""
+
+#: templates/feedback.html:6
+msgid "Feedback"
+msgstr ""
+
+#: templates/feedback.html:11
+msgid "Give us your feedback!"
+msgstr ""
+
+#: templates/feedback.html:17
+#, python-format
+msgid ""
+"\n"
+" Dear %(user_name)s, we look "
+"forward to hearing your feedback. \n"
+" Please type and send us your message below.\n"
+" "
+msgstr ""
+
+#: templates/feedback.html:24
+msgid ""
+"\n"
+" Dear visitor, we look forward to "
+"hearing your feedback.\n"
+" Please type and send us your message below.\n"
+" "
+msgstr ""
+
+#: templates/feedback.html:41
+msgid "(this field is required)"
+msgstr ""
+
+#: templates/feedback.html:49
+msgid "Send Feedback"
+msgstr ""
+
+#: templates/feedback_email.txt:3
+#, python-format
+msgid ""
+"\n"
+"Hello, this is a %(site_title)s forum feedback message\n"
+msgstr ""
+
+#: templates/feedback_email.txt:9
+msgid "Sender is"
+msgstr ""
+
+#: templates/feedback_email.txt:11 templates/feedback_email.txt.py:14
+msgid "email"
+msgstr ""
+
+#: templates/feedback_email.txt:13
+msgid "anonymous"
+msgstr ""
+
+#: templates/feedback_email.txt:19
+msgid "Message body:"
+msgstr ""
+
+#: templates/footer.html:8 templates/header.html:13 templates/index.html:119
+msgid "about"
+msgstr ""
+
+#: 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 "privacy policy"
+msgstr ""
+
+#: templates/footer.html:19
+msgid "give feedback"
+msgstr ""
+
+#: templates/header.html:9
+msgid "logout"
+msgstr ""
+
+#: templates/header.html:11
+msgid "login"
+msgstr ""
+
+#: templates/header.html:21
+msgid "back to home page"
+msgstr ""
+
+#: templates/header.html:29 templates/header.html.py:57
+msgid "users"
+msgstr ""
+
+#: templates/header.html:31
+msgid "books"
+msgstr ""
+
+#: templates/header.html:34
+msgid "unanswered questions"
+msgstr "unanswered"
+
+#: templates/header.html:36
+msgid "ask a question"
+msgstr ""
+
+#: templates/header.html:51
+msgid "search"
+msgstr ""
+
+#: templates/index.html:8
+msgid "Home"
+msgstr ""
+
+#: templates/index.html:25 templates/questions.html:8
+msgid "Questions"
+msgstr ""
+
+#: templates/index.html:27
+msgid "last updated questions"
+msgstr ""
+
+#: templates/index.html:27 templates/questions.html:47
+msgid "newest"
+msgstr ""
+
+#: templates/index.html:28 templates/questions.html:49
+msgid "hottest questions"
+msgstr ""
+
+#: templates/index.html:28 templates/questions.html:49
+msgid "hottest"
+msgstr ""
+
+#: templates/index.html:29 templates/questions.html:50
+msgid "most voted questions"
+msgstr ""
+
+#: templates/index.html:29 templates/questions.html:50
+msgid "most voted"
+msgstr ""
+
+#: templates/index.html:30
+msgid "all questions"
+msgstr ""
+
+#: templates/index.html:48 templates/question_summary_list_roll.html:13
+#: templates/questions.html:79 templates/users_questions.html:36
+msgid "answers"
+msgstr ""
+
+#: templates/index.html:80 templates/index.html.py:94
+#: templates/questions.html:111 templates/questions.html.py:125
+msgid "Posted:"
+msgstr ""
+
+#: templates/index.html:83 templates/index.html.py:88
+#: templates/questions.html:114 templates/questions.html.py:119
+msgid "Updated:"
+msgstr ""
+
+#: templates/index.html:105 templates/question.html:480
+#: templates/question_summary_list_roll.html:52 templates/questions.html:136
+#: templates/tags.html:49 templates/users_questions.html:52
+msgid "see questions tagged"
+msgstr ""
+
+#: templates/index.html:116
+msgid "welcome to website"
+msgstr "Welcome to Q&A forum"
+
+#: templates/index.html:127
+msgid "Recent tags"
+msgstr ""
+
+#: templates/index.html:132 templates/question.html:135
+#, python-format
+msgid "see questions tagged '%(tagname)s'"
+msgstr ""
+
+#: templates/index.html:135 templates/index.html.py:161
+msgid "popular tags"
+msgstr "tags"
+
+#: templates/index.html:140
+msgid "Recent awards"
+msgstr "Recent badges"
+
+#: templates/index.html:146
+msgid "given to"
+msgstr ""
+
+#: templates/index.html:151
+msgid "all awards"
+msgstr "all badges"
+
+#: templates/index.html:156
+msgid "subscribe to last 30 questions by RSS"
+msgstr ""
+
+#: templates/index.html:161
+msgid "Still looking for more? See"
+msgstr ""
+
+#: templates/index.html:161
+msgid "complete list of questions"
+msgstr "list of all questions"
+
+#: templates/index.html:161 templates/authopenid/signup.html:26
+msgid "or"
+msgstr ""
+
+#: templates/index.html:161
+msgid "Please help us answer"
+msgstr ""
+
+#: templates/index.html:161
+msgid "list of unanswered questions"
+msgstr "unanswered questions"
+
+#: templates/logout.html:6 templates/logout.html.py:16
+msgid "Logout"
+msgstr ""
+
+#: templates/logout.html:19
+msgid ""
+"As a registered user you can login with your OpenID, log out of the site or "
+"permanently remove your account."
+msgstr ""
+"Clicking Logout will log you out from the forumbut will not "
+"sign you off from your OpenID provider.
If you wish to sign off "
+"completely - please make sure to log out from your OpenID provider as well."
+
+#: templates/logout.html:20
+msgid "Logout now"
+msgstr "Logout Now"
+
+#: templates/notarobot.html:3
+msgid "Please prove that you are a Human Being"
+msgstr ""
+
+#: templates/notarobot.html:10
+msgid "I am a Human Being"
+msgstr ""
+
+#: templates/pagesize.html:6
+msgid "posts per page"
+msgstr ""
+
+#: templates/paginator.html:6 templates/paginator.html.py:7
+msgid "previous"
+msgstr ""
+
+#: templates/paginator.html:19
+msgid "current page"
+msgstr ""
+
+#: templates/paginator.html:22 templates/paginator.html.py:29
+msgid "page number "
+msgstr ""
+
+#: templates/paginator.html:22 templates/paginator.html.py:29
+msgid "number - make blank in english"
+msgstr ""
+
+#: templates/paginator.html:33
+msgid "next page"
+msgstr ""
+
+#: templates/post_contributor_info.html:9
+#, python-format
+msgid ""
+"\n"
+" one revision\n"
+" "
+msgid_plural ""
+"\n"
+" %(rev_count)s revisions\n"
+" "
+msgstr[0] ""
+msgstr[1] ""
+
+#: templates/post_contributor_info.html:19
+msgid "asked"
+msgstr ""
+
+#: templates/post_contributor_info.html:22
+msgid "answered"
+msgstr ""
+
+#: templates/post_contributor_info.html:24
+msgid "posted"
+msgstr ""
+
+#: templates/post_contributor_info.html:45
+msgid "updated"
+msgstr ""
+
+#: templates/privacy.html:6 templates/privacy.html.py:11
+msgid "Privacy policy"
+msgstr ""
+
+#: templates/privacy.html:15
+msgid "general message about privacy"
+msgstr ""
+"Respecting users privacy is an important core principle of this Q&A "
+"forum. Information on this page details how this forum protects your "
+"privacy, and what type of information is collected."
+
+#: templates/privacy.html:18
+msgid "Site Visitors"
+msgstr ""
+
+#: templates/privacy.html:20
+msgid "what technical information is collected about visitors"
+msgstr ""
+"Information on question views, revisions of questions and answers - both "
+"times and content are recorded for each user in order to correctly count "
+"number of views, maintain data integrity and report relevant updates."
+
+#: templates/privacy.html:23
+msgid "Personal Information"
+msgstr ""
+
+#: templates/privacy.html:25
+msgid "details on personal information policies"
+msgstr ""
+"Members of this community may choose to display personally identifiable "
+"information in their profiles. Forum will never display such information "
+"without a request from the user."
+
+#: templates/privacy.html:28
+msgid "Other Services"
+msgstr ""
+
+#: templates/privacy.html:30
+msgid "details on sharing data with third parties"
+msgstr ""
+"None of the data that is not openly shown on the forum by the choice of the "
+"user is shared with any third party."
+
+#: templates/privacy.html:35
+msgid "cookie policy details"
+msgstr ""
+"Forum software relies on the internet cookie technology to keep track of "
+"user sessions. Cookies must be enabled in your browser so that forum can "
+"work for you."
+
+#: templates/privacy.html:37
+msgid "Policy Changes"
+msgstr ""
+
+#: templates/privacy.html:38
+msgid "how privacy policies can be changed"
+msgstr ""
+"These policies may be adjusted to improve protection of user's privacy. "
+"Whenever such changes occur, users will be notified via the internal "
+"messaging system. "
+
+#: templates/question.html:77 templates/question.html.py:78
+#: templates/question.html:94 templates/question.html.py:96
+msgid "i like this post (click again to cancel)"
+msgstr ""
+
+#: templates/question.html:80 templates/question.html.py:98
+#: templates/question.html:257
+msgid "current number of votes"
+msgstr ""
+
+#: templates/question.html:89 templates/question.html.py:90
+#: templates/question.html:103 templates/question.html.py:104
+msgid "i dont like this post (click again to cancel)"
+msgstr ""
+
+#: templates/question.html:109 templates/question.html.py:110
+msgid "mark this question as favorite (click again to cancel)"
+msgstr ""
+
+#: templates/question.html:116 templates/question.html.py:117
+msgid "remove favorite mark from this question (click again to restore mark)"
+msgstr ""
+
+#: templates/question.html:140 templates/question.html.py:294
+#: templates/revisions_answer.html:58 templates/revisions_question.html:58
+msgid "edit"
+msgstr ""
+
+#: templates/question.html:145
+msgid "reopen"
+msgstr ""
+
+#: templates/question.html:149
+msgid "close"
+msgstr ""
+
+#: 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: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 ""
+
+#: templates/question.html:193 templates/question.html.py:342
+msgid "add comment"
+msgstr "post a comment"
+
+#: templates/question.html:197
+#, python-format
+msgid ""
+"\n"
+" see one more \n"
+" "
+msgid_plural ""
+"\n"
+" see %(counter)s "
+"more\n"
+" "
+msgstr[0] ""
+msgstr[1] ""
+
+#: templates/question.html:203
+#, python-format
+msgid ""
+"\n"
+" see one more "
+"comment\n"
+" "
+msgid_plural ""
+"\n"
+" see %(counter)s "
+"more comments\n"
+" "
+msgstr[0] ""
+msgstr[1] ""
+
+#: templates/question.html:219
+#, python-format
+msgid ""
+"The question has been closed for the following reason \"%(close_reason)s\" by"
+msgstr ""
+
+#: templates/question.html:221
+#, python-format
+msgid "close date %(closed_at)s"
+msgstr ""
+
+#: templates/question.html:229
+#, python-format
+msgid ""
+"\n"
+" One Answer:\n"
+" "
+msgid_plural ""
+"\n"
+" %(counter)s Answers:\n"
+" "
+msgstr[0] ""
+msgstr[1] ""
+
+#: templates/question.html:237
+msgid "oldest answers will be shown first"
+msgstr ""
+
+#: templates/question.html:237
+msgid "oldest answers"
+msgstr "oldest"
+
+#: templates/question.html:239
+msgid "newest answers will be shown first"
+msgstr ""
+
+#: templates/question.html:239
+msgid "newest answers"
+msgstr "newest"
+
+#: templates/question.html:241
+msgid "most voted answers will be shown first"
+msgstr ""
+
+#: templates/question.html:241
+msgid "popular answers"
+msgstr "most voted"
+
+#: templates/question.html:255 templates/question.html.py:256
+msgid "i like this answer (click again to cancel)"
+msgstr ""
+
+#: templates/question.html:262 templates/question.html.py:263
+msgid "i dont like this answer (click again to cancel)"
+msgstr ""
+
+#: templates/question.html:268 templates/question.html.py:269
+msgid "mark this answer as favorite (click again to undo)"
+msgstr ""
+
+#: templates/question.html:274 templates/question.html.py:275
+msgid "the author of the question has selected this answer as correct"
+msgstr ""
+
+#: templates/question.html:288
+msgid "answer permanent link"
+msgstr ""
+
+#: templates/question.html:289
+msgid "permanent link"
+msgstr "link"
+
+#: templates/question.html:311
+msgid "undelete"
+msgstr ""
+
+#: templates/question.html:346
+#, python-format
+msgid ""
+"\n"
+" see one"
+"strong> more \n"
+" "
+msgid_plural ""
+"\n"
+" see %"
+"(counter)s more\n"
+" "
+msgstr[0] ""
+msgstr[1] ""
+
+#: templates/question.html:352
+#, python-format
+msgid ""
+"\n"
+" see one"
+"strong> more comment\n"
+" "
+msgid_plural ""
+"\n"
+" see %"
+"(counter)s more comments\n"
+" "
+msgstr[0] ""
+msgstr[1] ""
+
+#: templates/question.html:378 templates/question.html.py:381
+msgid "Notify me once a day when there are any new answers"
+msgstr ""
+"Notify me once a day by email when there are any new "
+"answers or updates"
+
+#: templates/question.html:384
+msgid "Notify me weekly when there are any new answers"
+msgstr ""
+"Notify me weekly when there are any new answers or updates"
+
+#: templates/question.html:389
+#, python-format
+msgid ""
+"\n"
+" You can always adjust frequency of email updates from your %"
+"(profile_url)s\n"
+" "
+msgstr ""
+"\n"
+"(note: you can always adjust frequency of email updates)"
+
+#: templates/question.html:396
+msgid "once you sign in you will be able to subscribe for any updates here"
+msgstr ""
+"Here (once you log in) you will be able to sign "
+"up for the periodic email updates about this question."
+
+#: templates/question.html:407
+msgid "Your answer"
+msgstr ""
+
+#: templates/question.html:409
+msgid "Be the first one to answer this question!"
+msgstr ""
+
+#: templates/question.html:415
+msgid "you can answer anonymously and then login"
+msgstr ""
+"Please start posting your answer anonymously "
+"- your answer will be saved within the current session and published after "
+"you log in or create a new account. Please try to give a substantial "
+"answer, for discussions, please use comments and "
+"please do remember to vote (after you log in)!"
+
+#: templates/question.html:419
+msgid "answer your own question only to give an answer"
+msgstr ""
+"You are welcome to answer your own question, "
+"but please make sure to give an answer. Remember that you "
+"can always revise your original question. Please "
+"use comments for discussions and please don't "
+"forget to vote :) for the answers that you liked (or perhaps did "
+"not like)! "
+
+#: templates/question.html:421
+msgid "please only give an answer, no discussions"
+msgstr ""
+"Please try to give a substantial answer. If "
+"you wanted to comment on the question or answer, just use the "
+"commenting tool. Please remember that you can always revise "
+"your answers - no need to answer the same question twice. Also, "
+"please don't forget to vote - it really helps to select the "
+"best questions and answers!"
+
+#: templates/question.html:457
+msgid "Login/Signup to Post Your Answer"
+msgstr ""
+
+#: templates/question.html:460
+msgid "Answer Your Own Question"
+msgstr ""
+
+#: templates/question.html:462
+msgid "Answer the question"
+msgstr "Post Your Answer"
+
+#: templates/question.html:475
+msgid "Question tags"
+msgstr "Tags"
+
+#: templates/question.html:485
+msgid "question asked"
+msgstr "Asked"
+
+#: templates/question.html:488
+msgid "question was seen"
+msgstr "Seen"
+
+#: templates/question.html:488
+msgid "times"
+msgstr ""
+
+#: templates/question.html:491
+msgid "last updated"
+msgstr "Last updated"
+
+#: templates/question.html:496
+msgid "Related questions"
+msgstr ""
+
+#: templates/question_edit.html:5 templates/question_edit.html.py:66
+msgid "Edit question"
+msgstr ""
+
+#: templates/question_edit_tips.html:4
+msgid "question tips"
+msgstr "Tips"
+
+#: templates/question_edit_tips.html:7
+msgid "please ask a relevant question"
+msgstr "ask a question relevant to the CNPROG community"
+
+#: templates/question_edit_tips.html:10
+msgid "please try provide enough details"
+msgstr "provide enough details"
+
+#: templates/question_retag.html:4 templates/question_retag.html.py:53
+msgid "Change tags"
+msgstr ""
+
+#: templates/question_retag.html:40
+msgid "up to 5 tags, less than 20 characters each"
+msgstr ""
+
+#: templates/question_retag.html:83
+msgid "Why use and modify tags?"
+msgstr ""
+
+#: templates/question_retag.html:86
+msgid "tags help us keep Questions organized"
+msgstr ""
+
+#: templates/question_retag.html:94
+msgid "tag editors receive special awards from the community"
+msgstr ""
+
+#: templates/questions.html:29
+msgid "Found by tags"
+msgstr "Tagged questions"
+
+#: templates/questions.html:33
+msgid "Search results"
+msgstr ""
+
+#: templates/questions.html:35
+msgid "Found by title"
+msgstr ""
+
+#: templates/questions.html:39
+msgid "Unanswered questions"
+msgstr ""
+
+#: templates/questions.html:41
+msgid "All questions"
+msgstr ""
+
+#: templates/questions.html:47
+msgid "most recently asked questions"
+msgstr ""
+
+#: templates/questions.html:48
+msgid "most recently updated questions"
+msgstr ""
+
+#: templates/questions.html:48
+msgid "active"
+msgstr ""
+
+#: 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"
+" have total %(q_num)s questions tagged %(tagname)s\n"
+" "
+msgid_plural ""
+"\n"
+" have total %(q_num)s questions tagged %(tagname)s\n"
+" "
+msgstr[0] ""
+"\n"
+"
%(q_num)s
question tagged
%(tagname)s
"
+msgstr[1] ""
+"\n"
+"
%(q_num)s
questions tagged
%(tagname)s
"
+
+#: 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"
+"
%(q_num)s
question containing "
+"%(searchtitle)s
"
+msgstr[1] ""
+"\n"
+"
%(q_num)s
questions containing "
+"%(searchtitle)s
"
+
+#: templates/questions.html:177
+#, python-format
+msgid ""
+"\n"
+" have total %(q_num)s questions containing %(searchtitle)"
+"s\n"
+" "
+msgid_plural ""
+"\n"
+" have total %(q_num)s questions containing %(searchtitle)"
+"s\n"
+" "
+msgstr[0] ""
+"\n"
+"
%(q_num)s
question with title "
+"containing %(searchtitle)s"
+"p>"
+msgstr[1] ""
+"\n"
+"
%(q_num)s
questions with title "
+"containing %(searchtitle)s"
+"p>"
+
+#: templates/questions.html:185
+#, python-format
+msgid ""
+"\n"
+" have total %(q_num)s unanswered questions\n"
+" "
+msgid_plural ""
+"\n"
+" have total %(q_num)s unanswered questions\n"
+" "
+msgstr[0] ""
+"\n"
+"
%(q_num)s
question without an "
+"accepted answer
"
+msgstr[1] ""
+"\n"
+"
%(q_num)s
questions without an "
+"accepted answer
"
+
+#: 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"
+"
%(q_num)s
question
"
+msgstr[1] ""
+"\n"
+"
%(q_num)s
questions
"
+
+#: templates/questions.html:201
+msgid "latest questions info"
+msgstr "Newest questions are shown first."
+
+#: templates/questions.html:205
+msgid "Questions are sorted by the time of last update."
+msgstr ""
+
+#: templates/questions.html:206
+msgid "Most recently answered ones are shown first."
+msgstr "Most recently answered questions are shown first."
+
+#: templates/questions.html:210
+msgid "Questions sorted by number of responses."
+msgstr "Questions sorted by the number of answers."
+
+#: templates/questions.html:211
+msgid "Most answered questions are shown first."
+msgstr " "
+
+#: templates/questions.html:215
+msgid "Questions are sorted by the number of votes."
+msgstr ""
+
+#: templates/questions.html:216
+msgid "Most voted questions are shown first."
+msgstr ""
+
+#: templates/questions.html:224
+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 ""
+
+#: templates/reopen.html:19
+msgid "Open the previously closed question"
+msgstr ""
+
+#: templates/reopen.html:22
+msgid "The question was closed for the following reason "
+msgstr ""
+
+#: templates/reopen.html:22
+msgid "reason - leave blank in english"
+msgstr ""
+
+#: templates/reopen.html:22
+msgid "on "
+msgstr ""
+
+#: templates/reopen.html:22
+msgid "date closed"
+msgstr ""
+
+#: templates/reopen.html:29
+msgid "Reopen this question"
+msgstr ""
+
+#: templates/revisions_answer.html:7 templates/revisions_answer.html.py:38
+#: templates/revisions_question.html:8 templates/revisions_question.html:38
+msgid "Revision history"
+msgstr ""
+
+#: templates/revisions_answer.html:50 templates/revisions_question.html:50
+msgid "click to hide/show revision"
+msgstr ""
+
+#: templates/tag_selector.html:4
+msgid "Interesting tags"
+msgstr ""
+
+#: templates/tag_selector.html:14
+#, python-format
+msgid "remove '%(tag_name)s' from the list of interesting tags"
+msgstr ""
+
+#: templates/tag_selector.html:20 templates/tag_selector.html.py:37
+msgid "Add"
+msgstr ""
+
+#: templates/tag_selector.html:21
+msgid "Ignored tags"
+msgstr ""
+
+#: templates/tag_selector.html:31
+#, python-format
+msgid "remove '%(tag_name)s' from the list of ignored tags"
+msgstr ""
+
+#: templates/tag_selector.html:40
+msgid "keep ingored questions hidden"
+msgstr ""
+
+#: templates/tags.html:6 templates/tags.html.py:30
+msgid "Tag list"
+msgstr ""
+
+#: templates/tags.html:32
+msgid "sorted alphabetically"
+msgstr ""
+
+#: templates/tags.html:32
+msgid "by name"
+msgstr ""
+
+#: templates/tags.html:33
+msgid "sorted by frequency of tag use"
+msgstr ""
+
+#: templates/tags.html:33
+msgid "by popularity"
+msgstr ""
+
+#: templates/tags.html:39
+msgid "All tags matching query"
+msgstr ""
+
+#: templates/tags.html:39
+msgid "all tags - make this empty in english"
+msgstr ""
+
+#: templates/tags.html:42
+msgid "Nothing found"
+msgstr ""
+
+#: templates/user_edit.html:6
+msgid "Edit user profile"
+msgstr ""
+
+#: templates/user_edit.html:19
+msgid "edit profile"
+msgstr ""
+
+#: templates/user_edit.html:31
+msgid "image associated with your email address"
+msgstr ""
+
+#: templates/user_edit.html:31
+#, python-format
+msgid "avatar, see %(gravatar_faq_url)s"
+msgstr "gravatar"
+
+#: templates/user_edit.html:36 templates/user_info.html:56
+msgid "Registered user"
+msgstr ""
+
+#: templates/user_edit.html:43
+msgid "Screen Name"
+msgstr ""
+
+#: templates/user_edit.html:86 templates/user_email_subscriptions.html:20
+msgid "Update"
+msgstr ""
+
+#: templates/user_email_subscriptions.html:8
+msgid "Email subscription settings"
+msgstr ""
+
+#: templates/user_email_subscriptions.html:9
+msgid "email subscription settings info"
+msgstr ""
+"Adjust frequency of email updates. Receive "
+"updates on interesting questions by email, help the community"
+"strong> by answering questions of your colleagues. If you do not wish to "
+"receive emails - select 'no email' on all items below. Updates are only "
+"sent when there is any new activity on selected items."
+
+#: templates/user_email_subscriptions.html:21
+msgid "Stop sending email"
+msgstr "Stop Email"
+
+#: templates/user_info.html:32
+msgid "Moderate this user"
+msgstr ""
+
+#: templates/user_info.html:45
+msgid "update profile"
+msgstr ""
+
+#: templates/user_info.html:60
+msgid "real name"
+msgstr ""
+
+#: templates/user_info.html:65
+msgid "member for"
+msgstr "member since"
+
+#: templates/user_info.html:70
+msgid "last seen"
+msgstr ""
+
+#: templates/user_info.html:76
+msgid "user website"
+msgstr ""
+
+#: templates/user_info.html:82
+msgid "location"
+msgstr ""
+
+#: templates/user_info.html:89
+msgid "age"
+msgstr ""
+
+#: templates/user_info.html:90
+msgid "age unit"
+msgstr "years old"
+
+#: templates/user_info.html:96
+msgid "todays unused votes"
+msgstr ""
+
+#: templates/user_info.html:97
+msgid "votes left"
+msgstr ""
+
+#: templates/user_stats.html:12
+#, python-format
+msgid ""
+"\n"
+" 1 Question\n"
+" "
+msgid_plural ""
+"\n"
+" %(counter)s Questions\n"
+" "
+msgstr[0] ""
+msgstr[1] ""
+
+#: templates/user_stats.html:23
+#, python-format
+msgid ""
+"\n"
+" 1 Answer\n"
+" "
+msgid_plural ""
+"\n"
+" %(counter)s Answers\n"
+" "
+msgstr[0] ""
+msgstr[1] ""
+
+#: templates/user_stats.html:36
+#, python-format
+msgid "the answer has been voted for %(vote_count)s times"
+msgstr ""
+
+#: templates/user_stats.html:36
+msgid "this answer has been selected as correct"
+msgstr ""
+
+#: templates/user_stats.html:46
+#, python-format
+msgid ""
+"\n"
+" (one comment)\n"
+" "
+msgid_plural ""
+"\n"
+" the answer has been commented %(comment_count)s times\n"
+" "
+msgstr[0] ""
+"\n"
+"(one comment)"
+msgstr[1] ""
+"\n"
+"(%(comment_count)s comments)"
+
+#: templates/user_stats.html:61
+#, python-format
+msgid ""
+"\n"
+" 1 Vote\n"
+" "
+msgid_plural ""
+"\n"
+" %(cnt)s Votes\n"
+" "
+msgstr[0] ""
+msgstr[1] ""
+
+#: templates/user_stats.html:72
+msgid "thumb up"
+msgstr ""
+
+#: templates/user_stats.html:73
+msgid "user has voted up this many times"
+msgstr ""
+
+#: templates/user_stats.html:77
+msgid "thumb down"
+msgstr ""
+
+#: templates/user_stats.html:78
+msgid "user voted down this many times"
+msgstr ""
+
+#: templates/user_stats.html:87
+#, python-format
+msgid ""
+"\n"
+" 1 Tag\n"
+" "
+msgid_plural ""
+"\n"
+" %(counter)s Tags\n"
+" "
+msgstr[0] ""
+msgstr[1] ""
+
+#: templates/user_stats.html:100
+#, python-format
+msgid ""
+"see other questions with %(view_user)s's contributions tagged '%(tag_name)s' "
+msgstr ""
+
+#: templates/user_stats.html:115
+#, python-format
+msgid ""
+"\n"
+" 1 Badge\n"
+" "
+msgid_plural ""
+"\n"
+" %(counter)s Badges\n"
+" "
+msgstr[0] ""
+msgstr[1] ""
+
+#: templates/user_tabs.html:7
+msgid "User profile"
+msgstr ""
+
+#: templates/user_tabs.html:16
+msgid "graph of user reputation"
+msgstr "Graph of user karma"
+
+#: templates/user_tabs.html:17
+msgid "reputation history"
+msgstr "karma history"
+
+#: templates/user_tabs.html:23
+msgid "questions that user selected as his/her favorite"
+msgstr ""
+
+#: templates/user_tabs.html:24
+msgid "favorites"
+msgstr ""
+
+#: templates/users.html:6 templates/users.html.py:24
+msgid "Users"
+msgstr ""
+
+#: templates/users.html:27
+msgid "recent"
+msgstr ""
+
+#: templates/users.html:28
+msgid "oldest"
+msgstr ""
+
+#: templates/users.html:29
+msgid "by username"
+msgstr ""
+
+#: templates/users.html:35
+#, python-format
+msgid "users matching query %(suser)s:"
+msgstr ""
+
+#: templates/users.html:39
+msgid "Nothing found."
+msgstr ""
+
+#: templates/users_questions.html:11
+msgid "this questions was selected as favorite"
+msgstr ""
+
+#: templates/users_questions.html:12
+msgid "thumb-up on"
+msgstr ""
+
+#: templates/users_questions.html:19
+msgid "thumb-up off"
+msgstr ""
+
+#: templates/users_questions.html:34
+msgid "this answer has been accepted to be correct"
+msgstr ""
+
+#: templates/authopenid/changeemail.html:3
+#: templates/authopenid/changeemail.html:9
+#: templates/authopenid/changeemail.html:38
+msgid "Change email"
+msgstr "Change Email"
+
+#: templates/authopenid/changeemail.html:11
+msgid "Save your email address"
+msgstr ""
+
+#: templates/authopenid/changeemail.html:16
+#, python-format
+msgid "change %(email)s info"
+msgstr ""
+"Enter your new email into the box below if "
+"you'd like to use another email for update subscriptions."
+" Currently you are using %(email)s"
+
+#: templates/authopenid/changeemail.html:18
+#, python-format
+msgid "here is why email is required, see %(gravatar_faq_url)s"
+msgstr ""
+"Please enter your email address in the box below."
+"span> Valid email address is required on this Q&A forum. If you like, "
+"you can receive updates on interesting questions or entire "
+"forum via email. Also, your email is used to create a unique gravatar image for your account. "
+"Email addresses are never shown or otherwise shared with anybody else."
+
+#: templates/authopenid/changeemail.html:31
+msgid "Your new Email"
+msgstr ""
+"Your new Email: (will not be shown to "
+"anyone, must be valid)"
+
+#: templates/authopenid/changeemail.html:31
+msgid "Your Email"
+msgstr ""
+"Your Email (must be valid, never shown to others)"
+
+#: templates/authopenid/changeemail.html:38
+msgid "Save Email"
+msgstr ""
+
+#: templates/authopenid/changeemail.html:49
+msgid "Validate email"
+msgstr ""
+
+#: templates/authopenid/changeemail.html:52
+#, python-format
+msgid "validate %(email)s info or go to %(change_email_url)s"
+msgstr ""
+"An email with a validation link has been sent to %"
+"(email)s. Please follow the emailed link with your "
+"web browser. Email validation is necessary to help insure the proper use of "
+"email on Q&A. If you would like to use "
+"another email, please change it again."
+
+#: templates/authopenid/changeemail.html:57
+msgid "Email not changed"
+msgstr ""
+
+#: templates/authopenid/changeemail.html:60
+#, python-format
+msgid "old %(email)s kept, if you like go to %(change_email_url)s"
+msgstr ""
+"Your email address %(email)s has not been changed."
+" If you decide to change it later - you can always do it by editing "
+"it in your user profile or by using the previous form again."
+
+#: templates/authopenid/changeemail.html:65
+msgid "Email changed"
+msgstr ""
+
+#: templates/authopenid/changeemail.html:68
+#, python-format
+msgid "your current %(email)s can be used for this"
+msgstr ""
+"Your email address is now set to %(email)s. "
+"Updates on the questions that you like most will be sent to this address. "
+"Email notifications are sent once a day or less frequently - only when there "
+"are any news."
+
+#: templates/authopenid/changeemail.html:73
+msgid "Email verified"
+msgstr ""
+
+#: templates/authopenid/changeemail.html:76
+msgid "thanks for verifying email"
+msgstr ""
+"Thank you for verifying your email! Now "
+"you can ask and answer questions. Also if "
+"you find a very interesting question you can subscribe for the "
+"updates - then will be notified about changes once a day"
+"strong> or less frequently."
+
+#: templates/authopenid/changeemail.html:81
+msgid "email key not sent"
+msgstr "Validation email not sent"
+
+#: templates/authopenid/changeemail.html:84
+#, python-format
+msgid "email key not sent %(email)s change email here %(change_link)s"
+msgstr ""
+"Your current email address %(email)s has been "
+"validated before so the new key was not sent. You can change email used for update subscriptions if necessary."
+
+#: templates/authopenid/changeopenid.html:4
+#: templates/authopenid/changeopenid.html:30
+#: templates/authopenid/settings.html:34
+msgid "Change OpenID"
+msgstr ""
+
+#: templates/authopenid/changeopenid.html:8
+msgid "Account: change OpenID URL"
+msgstr ""
+
+#: templates/authopenid/changeopenid.html:12
+msgid ""
+"This is where you can change your OpenID URL. Make sure you remember it!"
+msgstr ""
+
+#: templates/authopenid/changeopenid.html:14
+#: templates/authopenid/delete.html:14 templates/authopenid/delete.html:24
+msgid "Please correct errors below:"
+msgstr ""
+
+#: templates/authopenid/changeopenid.html:29
+msgid "OpenID URL:"
+msgstr ""
+
+#: templates/authopenid/changepw.html:5 templates/authopenid/changepw.html:14
+#: templates/authopenid/settings.html:29
+msgid "Change password"
+msgstr ""
+
+#: templates/authopenid/changepw.html:7
+msgid "Account: change password"
+msgstr "Change your password"
+
+#: templates/authopenid/changepw.html:8
+msgid "This is where you can change your password. Make sure you remember it!"
+msgstr ""
+"To change your password please fill out and "
+"submit this form"
+
+#: templates/authopenid/complete.html:19
+msgid "Connect your OpenID with this site"
+msgstr "New user signup"
+
+#: templates/authopenid/complete.html:22
+msgid "Connect your OpenID with your account on this site"
+msgstr "New user signup"
+
+#: templates/authopenid/complete.html:27
+#, python-format
+msgid "register new %(provider)s account info, see %(gravatar_faq_url)s"
+msgstr ""
+"
You are here for the first time with your %"
+"(provider)s login. Please create your screen name "
+"and save your email address. Saved email address will let "
+"you subscribe for the updates on the most interesting "
+"questions and will be used to create and retrieve your unique avatar image - "
+"gravatar.
"
+
+#: templates/authopenid/complete.html:31
+#, python-format
+msgid ""
+"%(username)s already exists, choose another name for \n"
+" %(provider)s. Email is required too, see %"
+"(gravatar_faq_url)s\n"
+" "
+msgstr ""
+"
Oops... looks like screen name %(username)s is "
+"already used in another account.
Please choose another screen "
+"name to use with your %(provider)s login. Also, a valid email address is "
+"required on the Q&A forum. Your email is "
+"used to create a unique gravatar"
+"strong> image for your account. If you like, you can receive "
+"updates on the interesting questions or entire forum by email. "
+"Email addresses are never shown or otherwise shared with anybody else.
"
+
+#: templates/authopenid/complete.html:35
+#, python-format
+msgid ""
+"register new external %(provider)s account info, see %(gravatar_faq_url)s"
+msgstr ""
+"
You are here for the first time with your %"
+"(provider)s login.
You can either keep your screen "
+"name the same as your %(provider)s login name or choose some other "
+"nickname.
Also, please save a valid email address. "
+"With the email you can subscribe for the updates on the "
+"most interesting questions. Email address is also used to create and "
+"retrieve your unique avatar image - gravatar.
"
+
+#: templates/authopenid/complete.html:38
+#, python-format
+msgid "register new Facebook connect account info, see %(gravatar_faq_url)s"
+msgstr ""
+"
You are here for the first time with your "
+"Facebook login. Please create your screen name "
+"and save your email address. Saved email address will let "
+"you subscribe for the updates on the most interesting "
+"questions and will be used to create and retrieve your unique avatar image - "
+"gravatar.
"
+
+#: templates/authopenid/complete.html:42
+msgid "This account already exists, please use another."
+msgstr ""
+
+#: templates/authopenid/complete.html:57
+msgid "Sorry, looks like we have some errors:"
+msgstr ""
+
+#: templates/authopenid/complete.html:82
+msgid "Screen name label"
+msgstr "Screen Name (will be shown to others)"
+
+#: templates/authopenid/complete.html:89
+msgid "Email address label"
+msgstr ""
+"Email Address (will not be shared with "
+"anyone, must be valid)"
+
+#: templates/authopenid/complete.html:95 templates/authopenid/signup.html:18
+msgid "receive updates motivational blurb"
+msgstr ""
+"Receive forum updates by email - this will help our "
+"community grow and become more useful. By default Q&A forum sends up to one email digest per "
+"week - only when there is anything new. If you like, please "
+"adjust this now or any time later from your user account."
+
+#: templates/authopenid/complete.html:99
+msgid "Tag filter tool will be your right panel, once you log in."
+msgstr ""
+
+#: templates/authopenid/complete.html:100
+msgid "create account"
+msgstr "Signup"
+
+#: templates/authopenid/complete.html:109
+msgid "Existing account"
+msgstr ""
+
+#: templates/authopenid/complete.html:110
+msgid "user name"
+msgstr ""
+
+#: templates/authopenid/complete.html:111
+msgid "password"
+msgstr ""
+
+#: templates/authopenid/complete.html:118
+msgid "Register"
+msgstr ""
+
+#: templates/authopenid/complete.html:119 templates/authopenid/signin.html:151
+msgid "Forgot your password?"
+msgstr ""
+
+#: templates/authopenid/confirm_email.txt:2
+msgid "Thank you for registering at our Q&A forum!"
+msgstr ""
+
+#: templates/authopenid/confirm_email.txt:4
+msgid "Your account details are:"
+msgstr ""
+
+#: templates/authopenid/confirm_email.txt:6
+msgid "Username:"
+msgstr ""
+
+#: templates/authopenid/confirm_email.txt:7
+#: templates/authopenid/delete.html:19
+msgid "Password:"
+msgstr ""
+
+#: templates/authopenid/confirm_email.txt:9
+msgid "Please sign in here:"
+msgstr ""
+
+#: templates/authopenid/confirm_email.txt:12
+#: templates/authopenid/email_validation.txt:14
+#: templates/authopenid/sendpw_email.txt:8
+msgid ""
+"Sincerely,\n"
+"Forum Administrator"
+msgstr ""
+"Sincerely,\n"
+"Q&A Forum Administrator"
+
+#: templates/authopenid/delete.html:4 templates/authopenid/settings.html:38
+msgid "Delete account"
+msgstr ""
+
+#: templates/authopenid/delete.html:8
+msgid "Account: delete account"
+msgstr ""
+
+#: templates/authopenid/delete.html:12
+msgid ""
+"Note: After deleting your account, anyone will be able to register this "
+"username."
+msgstr ""
+
+#: templates/authopenid/delete.html:16
+msgid "Check confirm box, if you want delete your account."
+msgstr ""
+
+#: templates/authopenid/delete.html:31
+msgid "I am sure I want to delete my account."
+msgstr ""
+
+#: templates/authopenid/delete.html:32
+msgid "Password/OpenID URL"
+msgstr ""
+
+#: templates/authopenid/delete.html:32
+msgid "(required for your security)"
+msgstr ""
+
+#: templates/authopenid/delete.html:34
+msgid "Delete account permanently"
+msgstr ""
+
+#: templates/authopenid/email_validation.txt:2
+msgid "Greetings from the Q&A forum"
+msgstr ""
+
+#: templates/authopenid/email_validation.txt:4
+msgid "To make use of the Forum, please follow the link below:"
+msgstr ""
+
+#: templates/authopenid/email_validation.txt:8
+msgid "Following the link above will help us verify your email address."
+msgstr ""
+
+#: templates/authopenid/email_validation.txt:10
+msgid ""
+"If you beleive that this message was sent in mistake - \n"
+"no further action is needed. Just ingore this email, we apologize\n"
+"for any inconvenience"
+msgstr ""
+
+#: templates/authopenid/external_legacy_login_info.html:4
+#: templates/authopenid/external_legacy_login_info.html:7
+msgid "Traditional login information"
+msgstr ""
+
+#: templates/authopenid/external_legacy_login_info.html:12
+#, python-format
+msgid ""
+"how to login with password through external login website or use %"
+"(feedback_url)s"
+msgstr ""
+
+#: templates/authopenid/sendpw.html:4 templates/authopenid/sendpw.html.py:7
+msgid "Send new password"
+msgstr "Recover password"
+
+#: templates/authopenid/sendpw.html:10
+msgid "password recovery information"
+msgstr ""
+"Forgot you password? No problems - just get a new "
+"one! Please follow the following steps: • submit your "
+"user name below and check your email • follow the "
+"activation link for the new password - sent to you by email and "
+"login with the suggested password • at this you might want to "
+"change your password to something you can remember better"
+
+#: templates/authopenid/sendpw.html:21
+msgid "Reset password"
+msgstr "Send me a new password"
+
+#: templates/authopenid/sendpw.html:22
+msgid "return to login"
+msgstr ""
+
+#: templates/authopenid/sendpw_email.txt:2
+#, python-format
+msgid ""
+"Someone has requested to reset your password on %(site_url)s.\n"
+"If it were not you, it is safe to ignore this email."
+msgstr ""
+
+#: templates/authopenid/sendpw_email.txt:5
+#, python-format
+msgid ""
+"email explanation how to use new %(password)s for %(username)s\n"
+"with the %(key_link)s"
+msgstr ""
+"To change your password, please follow these steps:\n"
+"* visit this link: %(key_link)s\n"
+"* login with user name %(username)s and password %(password)s\n"
+"* go to your user profile and set the password to something you can remember"
+
+#: templates/authopenid/settings.html:4
+msgid "Account functions"
+msgstr ""
+
+#: templates/authopenid/settings.html:30
+msgid "Give your account a new password."
+msgstr ""
+
+#: templates/authopenid/settings.html:31
+msgid "Change email "
+msgstr ""
+
+#: templates/authopenid/settings.html:32
+msgid "Add or update the email address associated with your account."
+msgstr ""
+
+#: templates/authopenid/settings.html:35
+msgid "Change openid associated to your account"
+msgstr ""
+
+#: templates/authopenid/settings.html:39
+msgid "Erase your username and all your data from website"
+msgstr ""
+
+#: templates/authopenid/signin.html:5 templates/authopenid/signin.html:21
+msgid "User login"
+msgstr "User login"
+
+#: templates/authopenid/signin.html:28
+#, python-format
+msgid ""
+"\n"
+" Your answer to %(title)s %(summary)s will be posted once you "
+"log in\n"
+" "
+msgstr ""
+"\n"
+"Your answer to \"%(title)s"
+"strong> %(summary)s...\"is saved and will be "
+"posted once you log in."
+
+#: templates/authopenid/signin.html:35
+#, python-format
+msgid ""
+"Your question \n"
+" %(title)s %(summary)s will be posted once you log in\n"
+" "
+msgstr ""
+"Your question\"%(title)s"
+"strong> %(summary)s...\"is saved and will be "
+"posted once you log in."
+
+#: templates/authopenid/signin.html:42
+msgid "Click to sign in through any of these services."
+msgstr ""
+"
Please select your favorite login method below."
+"
External login services use OpenID technology, where your password "
+"always stays confidential between you and your login provider and you don't "
+"have to remember another one. CNPROG option requires your login name and "
+"password entered here.
"
+
+#: templates/authopenid/signin.html:128
+msgid "Enter your Provider user name"
+msgstr ""
+"Enter your Provider user name (or "
+"select another login method above)"
+
+#: templates/authopenid/signin.html:135
+msgid ""
+"Enter your OpenID "
+"web address"
+msgstr ""
+"Enter your OpenID web address (or choose "
+"another login method above)"
+
+#: templates/authopenid/signin.html:137 templates/authopenid/signin.html:149
+msgid "Login"
+msgstr ""
+
+#: templates/authopenid/signin.html:140
+msgid "Enter your login name and password"
+msgstr ""
+"Enter your CNPROG login and password (or select your OpenID provider above)"
+
+#: templates/authopenid/signin.html:144
+msgid "Login name"
+msgstr ""
+
+#: templates/authopenid/signin.html:146
+msgid "Password"
+msgstr ""
+
+#: templates/authopenid/signin.html:150
+msgid "Create account"
+msgstr ""
+
+#: templates/authopenid/signin.html:160
+msgid "Why use OpenID?"
+msgstr ""
+
+#: templates/authopenid/signin.html:163
+msgid "with openid it is easier"
+msgstr "With the OpenID you don't need to create new username and password."
+
+#: templates/authopenid/signin.html:166
+msgid "reuse openid"
+msgstr "You can safely re-use the same login for all OpenID-enabled websites."
+
+#: templates/authopenid/signin.html:169
+msgid "openid is widely adopted"
+msgstr ""
+"There are > 160,000,000 OpenID account in use. Over 10,000 sites are OpenID-"
+"enabled."
+
+#: templates/authopenid/signin.html:172
+msgid "openid is supported open standard"
+msgstr "OpenID is based on an open standard, supported by many organizations."
+
+#: templates/authopenid/signin.html:177
+msgid "Find out more"
+msgstr ""
+
+#: templates/authopenid/signin.html:178
+msgid "Get OpenID"
+msgstr ""
+
+#: templates/authopenid/signup.html:4
+msgid "Signup"
+msgstr ""
+
+#: templates/authopenid/signup.html:8
+msgid "Create login name and password"
+msgstr ""
+
+#: templates/authopenid/signup.html:10
+msgid "Traditional signup info"
+msgstr ""
+"If you prefer, create your forum login name and "
+"password here. However, please keep in mind that we also support "
+"OpenID login method. With OpenID you can "
+"simply reuse your external login (e.g. Gmail or AOL) without ever sharing "
+"your login details with anyone and having to remember yet another password."
+
+#: templates/authopenid/signup.html:19
+msgid ""
+"Please select your preferred email update schedule for the following groups "
+"of questions:"
+msgstr ""
+
+#: templates/authopenid/signup.html:23
+msgid ""
+"Please read and type in the two words below to help us prevent automated "
+"account creation."
+msgstr ""
+
+#: templates/authopenid/signup.html:25
+msgid "Create Account"
+msgstr ""
+
+#: templates/authopenid/signup.html:27
+msgid "return to OpenID login"
+msgstr ""
+
+#: templates/fbconnect/xd_receiver.html:5
+#, python-format
+msgid "Connect to %(APP_SHORT_NAME)s with Facebook!"
+msgstr ""
+
+#: utils/forms.py:27
+msgid "this field is required"
+msgstr ""
+
+#: utils/forms.py:42
+msgid "choose a username"
+msgstr "Choose screen name"
+
+#: utils/forms.py:47
+msgid "user name is required"
+msgstr ""
+
+#: utils/forms.py:48
+msgid "sorry, this name is taken, please choose another"
+msgstr ""
+
+#: utils/forms.py:49
+msgid "sorry, this name is not allowed, please choose another"
+msgstr ""
+
+#: utils/forms.py:50
+msgid "sorry, there is no user with this name"
+msgstr ""
+
+#: utils/forms.py:51
+msgid "sorry, we have a serious error - user name is taken by several users"
+msgstr ""
+
+#: utils/forms.py:52
+msgid "user name can only consist of letters, empty space and underscore"
+msgstr ""
+
+#: utils/forms.py:100
+msgid "your email address"
+msgstr "Your email (never shared)"
+
+#: utils/forms.py:101
+msgid "email address is required"
+msgstr ""
+
+#: utils/forms.py:102
+msgid "please enter a valid email address"
+msgstr ""
+
+#: utils/forms.py:103
+msgid "this email is already used by someone else, please choose another"
+msgstr ""
+
+#: utils/forms.py:128
+msgid "choose password"
+msgstr "Password"
+
+#: utils/forms.py:129
+msgid "password is required"
+msgstr ""
+
+#: utils/forms.py:132
+msgid "retype password"
+msgstr "Password (please retype)"
+
+#: utils/forms.py:133
+msgid "please, retype your password"
+msgstr ""
+
+#: utils/forms.py:134
+msgid "sorry, entered passwords did not match, please try again"
+msgstr ""
+
+#~ msgid "have %(num_q)s unanswered questions"
+#~ msgstr ""
+#~ "
+ {{comment.comment}} + - {{comment.user}} + {% spaceless %} + ({% diff_date comment.added_at %}) + {% if request.user|can_delete_comment:comment %} + + {% endif %} + {% endspaceless %} +
+ {% endfor %} +