From 4e7d6e104d95115b091e6bbf29148b5b1c96aa7e Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Sun, 24 Jan 2010 14:12:22 -0500 Subject: dos2unix everything, added dos2unix.sh script --- INSTALL | 602 ++-- README | 6 + django_authopenid/urls.py | 62 +- django_authopenid/views.py | 2142 +++++++------- dos2unix.sh | 10 + fbconnect/tests.py | 46 +- forum/forms.py | 636 ++-- forum/managers.py | 482 ++-- forum/templatetags/extra_tags.py | 702 ++--- forum/templatetags/smart_if.py | 802 +++--- forum/views.py | 4774 +++++++++++++++---------------- templates/content/js/com.cnprog.post.js | 1380 ++++----- 12 files changed, 5830 insertions(+), 5814 deletions(-) create mode 100644 README create mode 100644 dos2unix.sh diff --git a/INSTALL b/INSTALL index fe41befe..612cd371 100644 --- a/INSTALL +++ b/INSTALL @@ -1,301 +1,301 @@ -CONTENTS ------------------- -A. PREREQUISITES -B. INSTALLATION - 1. Settings file - 2. Database - 3. Running CNPROG in the development server - 4. Installation under Apache/WSGI - 5. Full text search - 6. Email subscriptions - 7. Sitemap - 8. Miscellaneous -C. CONFIGURATION PARAMETERS (settings_local.py) -D. CUSTOMIZATION - - -A. PREREQUISITES ------------------------------------------------ -0. We recommend you to use python-setuptools to install pre-requirement libraries. -If you haven't installed it, please try to install it first. -e.g, sudo apt-get install python-setuptools - -1. Python2.5/2.6, MySQL, Django v1.0/1.1 -Note: email subscription sender job requires Django 1.1, everything else works with 1.0 -Make sure mysql for python provider has been installed. -sudo easy_install mysql-python - -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 - -NOTES: django_authopenid is included into CNPROG code -and is significantly modified. http://code.google.com/p/django-authopenid/ -no need to install this library - -B. INSTALLATION ------------------------------------------------ -0. Make sure you have all above python libraries installed. - - make cnprog installation server-readable on Linux command might be: - chown -R yourlogin:apache /path/to/CNPROG - - directories templates/upfiles and log must be server writable - - on Linux type chmod - chmod -R g+w /path/to/CNPROG/upfiles - chmod -R g+w /path/to/log - - above it is assumed that webserver runs under group named "apache" - -1. Settings file - -Copy settings_local.py.dist to settings_local.py and -update all your settings. Check settings.py and update -it as well if necessory. -Section C explains configuration paramaters. - -2. Database - -Prepare your database by using the same database/account -configuration from above. -e.g, -create database cnprog DEFAULT CHARACTER SET UTF8 COLLATE utf8_general_ci; -grant all on cnprog.* to 'cnprog'@'localhost'; -And then run "python manage.py syncdb" to synchronize your database. - -3. Running CNPROG on the development server - -Run "python manage.py runserver" to startup django -development environment. -(Under Linux you can use command "python manage.py runserver `hostname -i`:8000", -where you can use any other available number for the port) - -you might want to have DEBUG=True in the beginning of settings.py -when using the test server - -4. Installation under Apache/WSGI - -4.1 Prepare wsgi script - -Make a file readable by your webserver with the following content: - ---------- -import os -import sys - -sys.path.insert(0,'/one/level/above') #insert to make sure that forum will be found -sys.path.append('/one/level/above/CNPROG') #maybe this is not necessary -os.environ['DJANGO_SETTINGS_MODULE'] = 'CNPROG.settings' -import django.core.handlers.wsgi -application = django.core.handlers.wsgi.WSGIHandler() ------------ - -insert method is used for path because if the forum directory name -is by accident the same as some other python module -you wull see strange errors - forum won't be found even though -it's in the python path. for example using name "test" is -not a good idea - as there is a module with such name - - -4.2 Configure webserver -Settings below are not perfect but may be a good starting point - ---------- -WSGISocketPrefix /path/to/socket/sock #must be readable and writable by apache -WSGIPythonHome /usr/local #must be readable by apache -WSGIPythonEggs /var/python/eggs #must be readable and writable by apache - -#NOTE: all urs below will need to be adjusted if -#settings.FORUM_SCRIPT_ALIAS !='' (e.g. = 'forum/') -#this allows "rooting" forum at http://example.com/forum, if you like - - ServerAdmin forum@example.com - DocumentRoot /path/to/cnprog - ServerName example.com - - #run mod_wsgi process for django in daemon mode - #this allows avoiding confused timezone settings when - #another application runs in the same virtual host - WSGIDaemonProcess CNPROG - WSGIProcessGroup CNPROG - - #force all content to be served as static files - #otherwise django will be crunching images through itself wasting time - Alias /content/ /path/to/cnprog/templates/content/ - AliasMatch /([^/]*\.css) /path/to/cnprog/templates/content/style/$1 - - Order deny,allow - Allow from all - - - #this is your wsgi script described in the prev section - WSGIScriptAlias / /path/to/cnprog/cnprog.wsgi - - #this will force admin interface to work only - #through https (optional) - #"nimda" is the secret spelling of "admin" ;) - - RewriteEngine on - RewriteRule /nimda(.*)$ https://example.com/nimda$1 [L,R=301] - - CustomLog /var/log/httpd/CNPROG/access_log common - ErrorLog /var/log/httpd/CNPROG/error_log - -#(optional) run admin interface under https - - ServerAdmin forum@example.com - DocumentRoot /path/to/cnrpog - ServerName example.com - SSLEngine on - SSLCertificateFile /path/to/ssl-certificate/server.crt - SSLCertificateKeyFile /path/to/ssl-certificate/server.key - WSGIScriptAlias / /path/to/cnprogcnprog.wsgi - CustomLog /var/log/httpd/CNPROG/access_log common - ErrorLog /var/log/httpd/CNPROG/error_log - DirectoryIndex index.html - -------------- - -5. Full text search (using sphinx search) - Currently full text search works only with sphinx search engine - Sphinx at this time supports only MySQL and PostgreSQL databases - to enable this, install sphinx search engine and djangosphinx - - configure sphinx, sample configuration can be found in - sphinx/sphinx.conf file usually goes somewhere in /etc tree - - build cnprog index first time manually - - % indexer --config /path/to/sphinx.conf --index cnprog - - setup cron job to rebuild index periodically with command - your crontab entry may be something like - - 0 9,15,21 * * * /usr/local/bin/indexer --config /etc/sphinx/sphinx.conf --all --rotate >/dev/null 2>&1 - adjust it as necessary this one will reindex three times a day at 9am 3pm and 9pm - - if your forum grows very big ( good luck with that :) you'll - need to two search indices one diff index and one main - please refer to online sphinx search documentation for the information - on the subject http://sphinxsearch.com/docs/ - - in settings_local.py set - USE_SPHINX_SEARCH=True - adjust other settings that have SPHINX_* prefix accordingly - remember that there must be trailing comma in parentheses for - SHPINX_SEARCH_INDICES tuple - particlarly with just one item! - - 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 = 'cnprog.log' #where logging messages should go -DATABASE_NAME = 'cnprog' # Or path to database file if using sqlite3. -DATABASE_USER = '' # Not used with sqlite3. -DATABASE_PASSWORD = '' # Not used with sqlite3. -DATABASE_ENGINE = 'mysql' #mysql, etc -SERVER_EMAIL = '' -DEFAULT_FROM_EMAIL = '' -EMAIL_HOST_USER = '' -EMAIL_HOST_PASSWORD = '' #not necessary if mailserver is run on local machine -EMAIL_SUBJECT_PREFIX = '[CNPROG] ' -EMAIL_HOST='cnprog.com' -EMAIL_PORT='25' -EMAIL_USE_TLS=False -TIME_ZONE = 'America/Tijuana' -APP_TITLE = u'CNPROG Q&A Forum' #title of your forum -APP_KEYWORDS = u'CNPROG,forum,community' #keywords for search engines -APP_DESCRIPTION = u'Ask and answer questions.' #site description for searche engines -APP_INTRO = u'

Ask and answer questions, make the world better!

' #slogan that goes to front page in logged out mode -APP_COPYRIGHT = '' #copyright message - -#if you set FORUM_SCRIPT_ALIAS= 'forum/' -#then CNPROG will run at url http://example.com/forum -#FORUM_SCRIPT_ALIAS cannot have leading slash, otherwise it can be set to anything -FORUM_SCRIPT_ALIAS = '' #no leading slash, default = '' empty string - -LANGUAGE_CODE = 'en' #forum language (see language instructions on the wiki) -EMAIL_VALIDATION = 'off' #string - on|off -MIN_USERNAME_LENGTH = 1 -EMAIL_UNIQUE = False #if True, email addresses must be unique in all accounts -APP_URL = 'http://cnprog.com' #used by email notif system and RSS -GOOGLE_SITEMAP_CODE = '' #code for google site crawler (look up google webmaster tools) -GOOGLE_ANALYTICS_KEY = '' #key to enable google analytics on this site -BOOKS_ON = False #if True - books tab will be on -WIKI_ON = True #if False - community wiki feature is disabled - -#experimental - allow password login through external site -#must implement django_authopenid/external_login.py -#included prototype external_login works with Mediawiki -USE_EXTERNAL_LEGACY_LOGIN = True #if false CNPROG uses it's own login/password -EXTERNAL_LEGACY_LOGIN_HOST = 'login.cnprog.com' -EXTERNAL_LEGACY_LOGIN_PORT = 80 -EXTERNAL_LEGACY_LOGIN_PROVIDER_NAME = 'CNPROG' - -FEEDBACK_SITE_URL = None #None or url -LOGIN_URL = '/%s%s%s' % (FORUM_SCRIPT_ALIAS,'account/','signin/') - -DJANGO_VERSION = 1.1 #must be either 1.0 or 1.1 -RESOURCE_REVISION=4 #increment when you update media files - clients will be forced to load new version - -D. Customization - -Other than settings_local.py the following will most likely need customization: -* locale/*/django.po - language files that may also contain your site-specific messages - if you want to start with english messages file - look for words like "forum" and - "CNPROG" in the msgstr lines -* templates/header.html and templates/footer.html may contain extra links -* templates/about.html - a place to explain for is your forum for -* templates/faq.html - put answers to users frequent questions -* templates/content/style/style.css - modify style sheet to add disctinctive look to your forum +CONTENTS +------------------ +A. PREREQUISITES +B. INSTALLATION + 1. Settings file + 2. Database + 3. Running CNPROG in the development server + 4. Installation under Apache/WSGI + 5. Full text search + 6. Email subscriptions + 7. Sitemap + 8. Miscellaneous +C. CONFIGURATION PARAMETERS (settings_local.py) +D. CUSTOMIZATION + + +A. PREREQUISITES +----------------------------------------------- +0. We recommend you to use python-setuptools to install pre-requirement libraries. +If you haven't installed it, please try to install it first. +e.g, sudo apt-get install python-setuptools + +1. Python2.5/2.6, MySQL, Django v1.0/1.1 +Note: email subscription sender job requires Django 1.1, everything else works with 1.0 +Make sure mysql for python provider has been installed. +sudo easy_install mysql-python + +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 + +NOTES: django_authopenid is included into CNPROG code +and is significantly modified. http://code.google.com/p/django-authopenid/ +no need to install this library + +B. INSTALLATION +----------------------------------------------- +0. Make sure you have all above python libraries installed. + + make cnprog installation server-readable on Linux command might be: + chown -R yourlogin:apache /path/to/CNPROG + + directories templates/upfiles and log must be server writable + + on Linux type chmod + chmod -R g+w /path/to/CNPROG/upfiles + chmod -R g+w /path/to/log + + above it is assumed that webserver runs under group named "apache" + +1. Settings file + +Copy settings_local.py.dist to settings_local.py and +update all your settings. Check settings.py and update +it as well if necessory. +Section C explains configuration paramaters. + +2. Database + +Prepare your database by using the same database/account +configuration from above. +e.g, +create database cnprog DEFAULT CHARACTER SET UTF8 COLLATE utf8_general_ci; +grant all on cnprog.* to 'cnprog'@'localhost'; +And then run "python manage.py syncdb" to synchronize your database. + +3. Running CNPROG on the development server + +Run "python manage.py runserver" to startup django +development environment. +(Under Linux you can use command "python manage.py runserver `hostname -i`:8000", +where you can use any other available number for the port) + +you might want to have DEBUG=True in the beginning of settings.py +when using the test server + +4. Installation under Apache/WSGI + +4.1 Prepare wsgi script + +Make a file readable by your webserver with the following content: + +--------- +import os +import sys + +sys.path.insert(0,'/one/level/above') #insert to make sure that forum will be found +sys.path.append('/one/level/above/CNPROG') #maybe this is not necessary +os.environ['DJANGO_SETTINGS_MODULE'] = 'CNPROG.settings' +import django.core.handlers.wsgi +application = django.core.handlers.wsgi.WSGIHandler() +----------- + +insert method is used for path because if the forum directory name +is by accident the same as some other python module +you wull see strange errors - forum won't be found even though +it's in the python path. for example using name "test" is +not a good idea - as there is a module with such name + + +4.2 Configure webserver +Settings below are not perfect but may be a good starting point + +--------- +WSGISocketPrefix /path/to/socket/sock #must be readable and writable by apache +WSGIPythonHome /usr/local #must be readable by apache +WSGIPythonEggs /var/python/eggs #must be readable and writable by apache + +#NOTE: all urs below will need to be adjusted if +#settings.FORUM_SCRIPT_ALIAS !='' (e.g. = 'forum/') +#this allows "rooting" forum at http://example.com/forum, if you like + + ServerAdmin forum@example.com + DocumentRoot /path/to/cnprog + ServerName example.com + + #run mod_wsgi process for django in daemon mode + #this allows avoiding confused timezone settings when + #another application runs in the same virtual host + WSGIDaemonProcess CNPROG + WSGIProcessGroup CNPROG + + #force all content to be served as static files + #otherwise django will be crunching images through itself wasting time + Alias /content/ /path/to/cnprog/templates/content/ + AliasMatch /([^/]*\.css) /path/to/cnprog/templates/content/style/$1 + + Order deny,allow + Allow from all + + + #this is your wsgi script described in the prev section + WSGIScriptAlias / /path/to/cnprog/cnprog.wsgi + + #this will force admin interface to work only + #through https (optional) + #"nimda" is the secret spelling of "admin" ;) + + RewriteEngine on + RewriteRule /nimda(.*)$ https://example.com/nimda$1 [L,R=301] + + CustomLog /var/log/httpd/CNPROG/access_log common + ErrorLog /var/log/httpd/CNPROG/error_log + +#(optional) run admin interface under https + + ServerAdmin forum@example.com + DocumentRoot /path/to/cnrpog + ServerName example.com + SSLEngine on + SSLCertificateFile /path/to/ssl-certificate/server.crt + SSLCertificateKeyFile /path/to/ssl-certificate/server.key + WSGIScriptAlias / /path/to/cnprogcnprog.wsgi + CustomLog /var/log/httpd/CNPROG/access_log common + ErrorLog /var/log/httpd/CNPROG/error_log + DirectoryIndex index.html + +------------- + +5. Full text search (using sphinx search) + Currently full text search works only with sphinx search engine + Sphinx at this time supports only MySQL and PostgreSQL databases + to enable this, install sphinx search engine and djangosphinx + + configure sphinx, sample configuration can be found in + sphinx/sphinx.conf file usually goes somewhere in /etc tree + + build cnprog index first time manually + + % indexer --config /path/to/sphinx.conf --index cnprog + + setup cron job to rebuild index periodically with command + your crontab entry may be something like + + 0 9,15,21 * * * /usr/local/bin/indexer --config /etc/sphinx/sphinx.conf --all --rotate >/dev/null 2>&1 + adjust it as necessary this one will reindex three times a day at 9am 3pm and 9pm + + if your forum grows very big ( good luck with that :) you'll + need to two search indices one diff index and one main + please refer to online sphinx search documentation for the information + on the subject http://sphinxsearch.com/docs/ + + in settings_local.py set + USE_SPHINX_SEARCH=True + adjust other settings that have SPHINX_* prefix accordingly + remember that there must be trailing comma in parentheses for + SHPINX_SEARCH_INDICES tuple - particlarly with just one item! + + 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 = 'cnprog.log' #where logging messages should go +DATABASE_NAME = 'cnprog' # Or path to database file if using sqlite3. +DATABASE_USER = '' # Not used with sqlite3. +DATABASE_PASSWORD = '' # Not used with sqlite3. +DATABASE_ENGINE = 'mysql' #mysql, etc +SERVER_EMAIL = '' +DEFAULT_FROM_EMAIL = '' +EMAIL_HOST_USER = '' +EMAIL_HOST_PASSWORD = '' #not necessary if mailserver is run on local machine +EMAIL_SUBJECT_PREFIX = '[CNPROG] ' +EMAIL_HOST='cnprog.com' +EMAIL_PORT='25' +EMAIL_USE_TLS=False +TIME_ZONE = 'America/Tijuana' +APP_TITLE = u'CNPROG Q&A Forum' #title of your forum +APP_KEYWORDS = u'CNPROG,forum,community' #keywords for search engines +APP_DESCRIPTION = u'Ask and answer questions.' #site description for searche engines +APP_INTRO = u'

Ask and answer questions, make the world better!

' #slogan that goes to front page in logged out mode +APP_COPYRIGHT = '' #copyright message + +#if you set FORUM_SCRIPT_ALIAS= 'forum/' +#then CNPROG will run at url http://example.com/forum +#FORUM_SCRIPT_ALIAS cannot have leading slash, otherwise it can be set to anything +FORUM_SCRIPT_ALIAS = '' #no leading slash, default = '' empty string + +LANGUAGE_CODE = 'en' #forum language (see language instructions on the wiki) +EMAIL_VALIDATION = 'off' #string - on|off +MIN_USERNAME_LENGTH = 1 +EMAIL_UNIQUE = False #if True, email addresses must be unique in all accounts +APP_URL = 'http://cnprog.com' #used by email notif system and RSS +GOOGLE_SITEMAP_CODE = '' #code for google site crawler (look up google webmaster tools) +GOOGLE_ANALYTICS_KEY = '' #key to enable google analytics on this site +BOOKS_ON = False #if True - books tab will be on +WIKI_ON = True #if False - community wiki feature is disabled + +#experimental - allow password login through external site +#must implement django_authopenid/external_login.py +#included prototype external_login works with Mediawiki +USE_EXTERNAL_LEGACY_LOGIN = True #if false CNPROG uses it's own login/password +EXTERNAL_LEGACY_LOGIN_HOST = 'login.cnprog.com' +EXTERNAL_LEGACY_LOGIN_PORT = 80 +EXTERNAL_LEGACY_LOGIN_PROVIDER_NAME = 'CNPROG' + +FEEDBACK_SITE_URL = None #None or url +LOGIN_URL = '/%s%s%s' % (FORUM_SCRIPT_ALIAS,'account/','signin/') + +DJANGO_VERSION = 1.1 #must be either 1.0 or 1.1 +RESOURCE_REVISION=4 #increment when you update media files - clients will be forced to load new version + +D. Customization + +Other than settings_local.py the following will most likely need customization: +* locale/*/django.po - language files that may also contain your site-specific messages + if you want to start with english messages file - look for words like "forum" and + "CNPROG" in the msgstr lines +* templates/header.html and templates/footer.html may contain extra links +* templates/about.html - a place to explain for is your forum for +* templates/faq.html - put answers to users frequent questions +* templates/content/style/style.css - modify style sheet to add disctinctive look to your forum diff --git a/README b/README new file mode 100644 index 00000000..2a209b7a --- /dev/null +++ b/README @@ -0,0 +1,6 @@ +This is OSQA project - open source Q&A system + +Demo site is http://osqa.net + +OSQA is based on code of CNPROG, originally created by Mike Chen and Sailing Cai. + diff --git a/django_authopenid/urls.py b/django_authopenid/urls.py index 9ed621bc..112cbbe1 100755 --- a/django_authopenid/urls.py +++ b/django_authopenid/urls.py @@ -1,31 +1,31 @@ -# -*- coding: utf-8 -*- -from django.conf.urls.defaults import patterns, url -from django.utils.translation import ugettext as _ - -urlpatterns = patterns('django_authopenid.views', - # yadis rdf - url(r'^yadis.xrdf$', 'xrdf', name='yadis_xrdf'), - # manage account registration - url(r'^%s$' % _('signin/'), 'signin', name='user_signin'), - url(r'^%s%s$' % (_('signin/'),_('newquestion/')), 'signin', kwargs = {'newquestion':True}, name='user_signin_new_question'), - url(r'^%s%s$' % (_('signin/'),_('newanswer/')), 'signin', kwargs = {'newanswer':True}, name='user_signin_new_answer'), - url(r'^%s$' % _('signout/'), 'signout', name='user_signout'), - url(r'^%s%s$' % (_('signin/'), _('complete/')), 'complete_signin', - name='user_complete_signin'), - url('^%s$' % _('external-login/'),'external_legacy_login_info', name='user_external_legacy_login_issues'), - url(r'^%s$' % _('register/'), 'register', name='user_register'), - url(r'^%s$' % _('signup/'), 'signup', name='user_signup'), - #disable current sendpw function - url(r'^%s$' % _('sendpw/'), 'sendpw', name='user_sendpw'), - url(r'^%s%s$' % (_('password/'), _('confirm/')), 'confirmchangepw', name='user_confirmchangepw'), - - # manage account settings - url(r'^$', _('account_settings'), name='user_account_settings'), - url(r'^%s$' % _('password/'), 'changepw', name='user_changepw'), - url(r'^%s%s$' % (_('email/'),_('validate/')), 'changeemail', name='user_validateemail',kwargs = {'action':'validate'}), - url(r'^%s%s$' % (_('email/'), _('change/')), 'changeemail', name='user_changeemail'), - url(r'^%s%s$' % (_('email/'), _('sendkey/')), 'send_email_key', name='send_email_key'), - url(r'^%s%s(?P\d+)/(?P[\dabcdef]{32})/$' % (_('email/'), _('verify/')), 'verifyemail', name='user_verifyemail'), - url(r'^%s$' % _('openid/'), 'changeopenid', name='user_changeopenid'), - url(r'^%s$' % _('delete/'), 'delete', name='user_delete'), -) +# -*- coding: utf-8 -*- +from django.conf.urls.defaults import patterns, url +from django.utils.translation import ugettext as _ + +urlpatterns = patterns('django_authopenid.views', + # yadis rdf + url(r'^yadis.xrdf$', 'xrdf', name='yadis_xrdf'), + # manage account registration + url(r'^%s$' % _('signin/'), 'signin', name='user_signin'), + url(r'^%s%s$' % (_('signin/'),_('newquestion/')), 'signin', kwargs = {'newquestion':True}, name='user_signin_new_question'), + url(r'^%s%s$' % (_('signin/'),_('newanswer/')), 'signin', kwargs = {'newanswer':True}, name='user_signin_new_answer'), + url(r'^%s$' % _('signout/'), 'signout', name='user_signout'), + url(r'^%s%s$' % (_('signin/'), _('complete/')), 'complete_signin', + name='user_complete_signin'), + url('^%s$' % _('external-login/'),'external_legacy_login_info', name='user_external_legacy_login_issues'), + url(r'^%s$' % _('register/'), 'register', name='user_register'), + url(r'^%s$' % _('signup/'), 'signup', name='user_signup'), + #disable current sendpw function + url(r'^%s$' % _('sendpw/'), 'sendpw', name='user_sendpw'), + url(r'^%s%s$' % (_('password/'), _('confirm/')), 'confirmchangepw', name='user_confirmchangepw'), + + # manage account settings + url(r'^$', _('account_settings'), name='user_account_settings'), + url(r'^%s$' % _('password/'), 'changepw', name='user_changepw'), + url(r'^%s%s$' % (_('email/'),_('validate/')), 'changeemail', name='user_validateemail',kwargs = {'action':'validate'}), + url(r'^%s%s$' % (_('email/'), _('change/')), 'changeemail', name='user_changeemail'), + url(r'^%s%s$' % (_('email/'), _('sendkey/')), 'send_email_key', name='send_email_key'), + url(r'^%s%s(?P\d+)/(?P[\dabcdef]{32})/$' % (_('email/'), _('verify/')), 'verifyemail', name='user_verifyemail'), + url(r'^%s$' % _('openid/'), 'changeopenid', name='user_changeopenid'), + url(r'^%s$' % _('delete/'), 'delete', name='user_delete'), +) diff --git a/django_authopenid/views.py b/django_authopenid/views.py index 36f08018..d087d215 100755 --- a/django_authopenid/views.py +++ b/django_authopenid/views.py @@ -1,1071 +1,1071 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2007, 2008, Benoît Chesneau -# Copyright (c) 2007 Simon Willison, original work on django-openid -# -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# * Redistributions of source code must retain the above copyright -# * notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# * notice, this list of conditions and the following disclaimer in the -# * documentation and/or other materials provided with the -# * distribution. Neither the name of the nor the names -# * of its contributors may be used to endorse or promote products -# * derived from this software without specific prior written -# * permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS -# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, -# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, -# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -from django.http import HttpResponseRedirect, get_host, Http404, \ - HttpResponseServerError -from django.shortcuts import render_to_response as render -from django.template import RequestContext, loader, Context -from django.conf import settings -from django.contrib.auth.models import User -from django.contrib.auth.decorators import login_required -from django.contrib.auth import authenticate -from django.core.urlresolvers import reverse -from django.utils.encoding import smart_unicode -from django.utils.html import escape -from django.utils.translation import ugettext as _ -from django.utils.http import urlquote_plus -from django.utils.safestring import mark_safe -from django.core.mail import send_mail -from django.views.defaults import server_error - -from openid.consumer.consumer import Consumer, \ - SUCCESS, CANCEL, FAILURE, SETUP_NEEDED -from openid.consumer.discover import DiscoveryFailure -from openid.extensions import sreg -# needed for some linux distributions like debian -try: - from openid.yadis import xri -except ImportError: - from yadis import xri - -import re -import urllib - - -from forum.forms import EditUserEmailFeedsForm -from django_authopenid.util import OpenID, DjangoOpenIDStore, from_openid_response, get_next_url -from django_authopenid.models import UserAssociation, UserPasswordQueue, ExternalLoginData -from django_authopenid.forms import OpenidSigninForm, ClassicLoginForm, OpenidRegisterForm, \ - OpenidVerifyForm, ClassicRegisterForm, ChangePasswordForm, ChangeEmailForm, \ - ChangeopenidForm, DeleteForm, EmailPasswordForm -import external_login -import logging - -def login(request,user): - from django.contrib.auth import login as _login - from forum.models import user_logged_in #custom signal - - print 'in login call' - - if settings.USE_EXTERNAL_LEGACY_LOGIN == True: - external_login.login(request,user) - - #1) get old session key - session_key = request.session.session_key - #2) login and get new session key - _login(request,user) - #3) send signal with old session key as argument - user_logged_in.send(user=user,session_key=session_key,sender=None) - -def logout(request): - from django.contrib.auth import logout as _logout#for login I've added wrapper below - called login - _logout(request) - if settings.USE_EXTERNAL_LEGACY_LOGIN == True: - external_login.logout(request) - -def get_url_host(request): - if request.is_secure(): - protocol = 'https' - 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() - -def ask_openid(request, openid_url, redirect_to, on_failure=None, - sreg_request=None): - """ basic function to ask openid and return response """ - request.encoding = 'UTF-8' - on_failure = on_failure or signin_failure - - trust_root = getattr( - settings, 'OPENID_TRUST_ROOT', get_url_host(request) + '/' - ) - if xri.identifierScheme(openid_url) == 'XRI' and getattr( - settings, 'OPENID_DISALLOW_INAMES', False - ): - msg = _("i-names are not supported") - return on_failure(request, msg) - consumer = Consumer(request.session, DjangoOpenIDStore()) - try: - auth_request = consumer.begin(openid_url) - except DiscoveryFailure: - msg = _(u"OpenID %(openid_url)s is invalid" % {'openid_url':openid_url}) - return on_failure(request, msg) - - if sreg_request: - auth_request.addExtension(sreg_request) - redirect_url = auth_request.redirectURL(trust_root, redirect_to) - return HttpResponseRedirect(redirect_url) - -def complete(request, on_success=None, on_failure=None, return_to=None): - """ complete openid signin """ - on_success = on_success or default_on_success - on_failure = on_failure or default_on_failure - - consumer = Consumer(request.session, DjangoOpenIDStore()) - # make sure params are encoded in utf8 - params = dict((k,smart_unicode(v)) for k, v in request.GET.items()) - openid_response = consumer.complete(params, return_to) - - if openid_response.status == SUCCESS: - return on_success(request, openid_response.identity_url, - openid_response) - elif openid_response.status == CANCEL: - return on_failure(request, 'The request was canceled') - elif openid_response.status == FAILURE: - return on_failure(request, openid_response.message) - elif openid_response.status == SETUP_NEEDED: - return on_failure(request, 'Setup needed') - else: - assert False, "Bad openid status: %s" % openid_response.status - -def default_on_success(request, identity_url, openid_response): - """ default action on openid signin success """ - request.session['openid'] = from_openid_response(openid_response) - return HttpResponseRedirect(get_next_url(request)) - -def default_on_failure(request, message): - """ default failure action on signin """ - return render('openid_failure.html', { - 'message': message - }) - - -def not_authenticated(func): - """ decorator that redirect user to next page if - he is already logged.""" - def decorated(request, *args, **kwargs): - if request.user.is_authenticated(): - return HttpResponseRedirect(get_next_url(request)) - return func(request, *args, **kwargs) - return decorated - -@not_authenticated -def signin(request,newquestion=False,newanswer=False): - """ - signin page. It manages the legacy authentification (user/password) - and openid authentification - - url: /signin/ - - template : authopenid/signin.htm - """ - request.encoding = 'UTF-8' - on_failure = signin_failure - email_feeds_form = EditUserEmailFeedsForm() - next = get_next_url(request) - form_signin = OpenidSigninForm(initial={'next':next}) - form_auth = ClassicLoginForm(initial={'next':next}) - - if request.POST: - #'blogin' - password login - if 'blogin' in request.POST.keys(): - form_auth = ClassicLoginForm(request.POST) - if form_auth.is_valid(): - #have login and password and need to login through external website - if settings.USE_EXTERNAL_LEGACY_LOGIN == True: - username = form_auth.cleaned_data['username'] - password = form_auth.cleaned_data['password'] - next = form_auth.cleaned_data['next'] - if form_auth.get_user() == None: - #need to create internal user - - #1) save login and password temporarily in session - request.session['external_username'] = username - request.session['external_password'] = password - - #2) see if username clashes with some existing user - #if so, we have to prompt the user to pick a different name - username_taken = User.is_username_taken(username) - #try: - # User.objects.get(username=username) - # username_taken = True - #except User.DoesNotExist: - # username_taken = False - - #3) try to extract user email from external service - email = external_login.get_email(username,password) - - email_feeds_form = EditUserEmailFeedsForm() - form_data = {'username':username,'email':email,'next':next} - form = OpenidRegisterForm(initial=form_data) - template_data = {'form1':form,'username':username,\ - 'email_feeds_form':email_feeds_form,\ - 'provider':mark_safe(settings.EXTERNAL_LEGACY_LOGIN_PROVIDER_NAME),\ - 'login_type':'legacy',\ - 'gravatar_faq_url':reverse('faq') + '#gravatar',\ - 'external_login_name_is_taken':username_taken} - return render('authopenid/complete.html',template_data,\ - context_instance=RequestContext(request)) - else: - #user existed, external password is ok - user = form_auth.get_user() - login(request,user) - response = HttpResponseRedirect(get_next_url(request)) - external_login.set_login_cookies(response,user) - return response - else: - #regular password authentication - user = form_auth.get_user() - login(request, user) - return HttpResponseRedirect(get_next_url(request)) - - elif 'bnewaccount' in request.POST.keys(): - #register externally logged in password user with a new local account - if settings.USE_EXTERNAL_LEGACY_LOGIN == True: - form = OpenidRegisterForm(request.POST) - email_feeds_form = EditUserEmailFeedsForm(request.POST) - form1_is_valid = form.is_valid() - form2_is_valid = email_feeds_form.is_valid() - if form1_is_valid and form2_is_valid: - #create the user - username = form.cleaned_data['username'] - password = request.session.get('external_password',None) - email = form.cleaned_data['email'] - print 'got email addr %s' % email - if password and username: - User.objects.create_user(username,email,password) - user = authenticate(username=username,password=password) - external_username = request.session['external_username'] - eld = ExternalLoginData.objects.get(external_username=external_username) - eld.user = user - eld.save() - login(request,user) - email_feeds_form.save(user) - del request.session['external_username'] - del request.session['external_password'] - return HttpResponseRedirect(reverse('index')) - else: - if password: - del request.session['external_username'] - if username: - del request.session['external_password'] - return HttpResponseServerError() - else: - username = request.POST.get('username',None) - provider = mark_safe(settings.EXTERNAL_LEGACY_LOGIN_PROVIDER_NAME) - username_taken = User.is_username_taken(username) - data = {'login_type':'legacy','form1':form,'username':username,\ - 'email_feeds_form':email_feeds_form,'provider':provider,\ - 'gravatar_faq_url':reverse('faq') + '#gravatar',\ - 'external_login_name_is_taken':username_taken} - return render('authopenid/complete.html',data, - context_instance=RequestContext(request)) - else: - raise Http404 - - elif 'bsignin' in request.POST.keys() or 'openid_username' in request.POST.keys(): - form_signin = OpenidSigninForm(request.POST) - if form_signin.is_valid(): - next = form_signin.cleaned_data['next'] - sreg_req = sreg.SRegRequest(optional=['nickname', 'email']) - redirect_to = "%s%s?%s" % ( - get_url_host(request), - reverse('user_complete_signin'), - urllib.urlencode({'next':next}) - ) - return ask_openid(request, - form_signin.cleaned_data['openid_url'], - redirect_to, - on_failure=signin_failure, - sreg_request=sreg_req) - - - #if request is GET - question = None - if newquestion == True: - from forum.models import AnonymousQuestion as AQ - session_key = request.session.session_key - qlist = AQ.objects.filter(session_key=session_key).order_by('-added_at') - if len(qlist) > 0: - question = qlist[0] - answer = None - if newanswer == True: - from forum.models import AnonymousAnswer as AA - session_key = request.session.session_key - alist = AA.objects.filter(session_key=session_key).order_by('-added_at') - if len(alist) > 0: - answer = alist[0] - - return render('authopenid/signin.html', { - 'question':question, - 'answer':answer, - 'form1': form_auth, - 'form2': form_signin, - 'msg': request.GET.get('msg',''), - 'sendpw_url': reverse('user_sendpw'), - 'fb_api_key': settings.FB_API_KEY, - }, context_instance=RequestContext(request)) - -def complete_signin(request): - """ in case of complete signin with openid """ - return complete(request, signin_success, signin_failure, - get_url_host(request) + reverse('user_complete_signin')) - -def signin_success(request, identity_url, openid_response): - """ - openid signin success. - - If the openid is already registered, the user is redirected to - url set par next or in settings with OPENID_REDIRECT_NEXT variable. - If none of these urls are set user is redirectd to /. - - if openid isn't registered user is redirected to register page. - """ - - openid_ = from_openid_response(openid_response) #create janrain OpenID object - request.session['openid'] = openid_ - try: - rel = UserAssociation.objects.get(openid_url__exact = str(openid_)) - except: - # try to register this new user - return register(request) - user_ = rel.user - if user_.is_active: - user_.backend = "django.contrib.auth.backends.ModelBackend" - login(request, user_) - - return HttpResponseRedirect(get_next_url(request)) - -def is_association_exist(openid_url): - """ test if an openid is already in database """ - is_exist = True - try: - uassoc = UserAssociation.objects.get(openid_url__exact = openid_url) - except: - is_exist = False - return is_exist - -@not_authenticated -def register(request): - """ - register an openid. - - If user is already a member he can associate its openid with - its account. - - A new account could also be created and automaticaly associated - to the openid. - - url : /complete/ - - template : authopenid/complete.html - """ - - openid_ = request.session.get('openid', None) - next = get_next_url(request) - if not openid_: - return HttpResponseRedirect(reverse('user_signin') + '?next=%s' % next) - - nickname = openid_.sreg.get('nickname', '') - email = openid_.sreg.get('email', '') - form1 = OpenidRegisterForm(initial={ - 'next': next, - 'username': nickname, - 'email': email, - }) - form2 = OpenidVerifyForm(initial={ - 'next': next, - 'username': nickname, - }) - email_feeds_form = EditUserEmailFeedsForm() - - user_ = None - is_redirect = False - if request.POST: - if 'bnewaccount' in request.POST.keys(): - form1 = OpenidRegisterForm(request.POST) - email_feeds_form = EditUserEmailFeedsForm(request.POST) - if form1.is_valid() and email_feeds_form.is_valid(): - next = form1.cleaned_data['next'] - is_redirect = True - tmp_pwd = User.objects.make_random_password() - user_ = User.objects.create_user(form1.cleaned_data['username'], - form1.cleaned_data['email'], tmp_pwd) - - user_.set_unusable_password() - # make association with openid - uassoc = UserAssociation(openid_url=str(openid_), - user_id=user_.id) - uassoc.save() - - # login - user_.backend = "django.contrib.auth.backends.ModelBackend" - login(request, user_) - email_feeds_form.save(user_) - elif 'bverify' in request.POST.keys(): - form2 = OpenidVerifyForm(request.POST) - if form2.is_valid(): - is_redirect = True - next = form2.cleaned_data['next'] - user_ = form2.get_user() - - uassoc = UserAssociation(openid_url=str(openid_), - user_id=user_.id) - uassoc.save() - login(request, user_) - - #check if we need to post a question that was added anonymously - #this needs to be a function call becase this is also done - #if user just logged in and did not need to create the new account - - if user_ != None: - if settings.EMAIL_VALIDATION == 'on': - send_new_email_key(user_,nomessage=True) - output = validation_email_sent(request) - set_email_validation_message(user_) #message set after generating view - return output - if user_.is_authenticated(): - return HttpResponseRedirect(reverse('index')) - else: - raise Exception('openid login failed')#should not ever get here - - openid_str = str(openid_) - bits = openid_str.split('/') - base_url = bits[2] #assume this is base url - url_bits = base_url.split('.') - provider_name = url_bits[-2].lower() - - providers = {'yahoo':'Yahoo!', - 'flickr':'flickr™', - 'google':'Google™', - 'aol':'AOL', - 'myopenid':'MyOpenID', - } - if provider_name not in providers: - provider_logo = provider_name - else: - provider_logo = providers[provider_name] - - return render('authopenid/complete.html', { - 'form1': form1, - 'form2': form2, - 'email_feeds_form': email_feeds_form, - 'provider':mark_safe(provider_logo), - 'username': nickname, - 'email': email, - 'login_type':'openid', - 'gravatar_faq_url':reverse('faq') + '#gravatar', - }, context_instance=RequestContext(request)) - -def signin_failure(request, message): - """ - falure with openid signin. Go back to signin page. - - template : "authopenid/signin.html" - """ - next = get_next_url(request) - form_signin = OpenidSigninForm(initial={'next': next}) - form_auth = ClassicLoginForm(initial={'next': next}) - - return render('authopenid/signin.html', { - 'msg': message, - 'form1': form_auth, - 'form2': form_signin, - }, context_instance=RequestContext(request)) - -@not_authenticated -def signup(request): - """ - signup page. Create a legacy account - - url : /signup/" - - templates: authopenid/signup.html, authopenid/confirm_email.txt - """ - if settings.USE_EXTERNAL_LEGACY_LOGIN == True: - return HttpResponseRedirect(reverse('user_external_legacy_login_issues')) - next = get_next_url(request) - if request.POST: - form = ClassicRegisterForm(request.POST) - email_feeds_form = EditUserEmailFeedsForm(request.POST) - - #validation outside if to remember form values - form1_is_valid = form.is_valid() - form2_is_valid = email_feeds_form.is_valid() - if form1_is_valid and form2_is_valid: - next = form.cleaned_data['next'] - username = form.cleaned_data['username'] - password = form.cleaned_data['password1'] - email = form.cleaned_data['email'] - - user_ = User.objects.create_user( username,email,password ) - if settings.USE_EXTERNAL_LEGACY_LOGIN == True: - external_login.create_user(username,email,password) - - user_.backend = "django.contrib.auth.backends.ModelBackend" - login(request, user_) - email_feeds_form.save(user_) - - # send email - subject = _("Welcome email subject line") - message_template = loader.get_template( - 'authopenid/confirm_email.txt' - ) - message_context = Context({ - 'signup_url': settings.APP_URL + reverse('user_signin'), - 'username': username, - 'password': password, - }) - message = message_template.render(message_context) - send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, - [user_.email]) - return HttpResponseRedirect(next) - else: - form = ClassicRegisterForm(initial={'next':next}) - email_feeds_form = EditUserEmailFeedsForm() - return render('authopenid/signup.html', { - 'form': form, - 'email_feeds_form': email_feeds_form - }, context_instance=RequestContext(request)) - #what if request is not posted? - -@login_required -def signout(request): - """ - signout from the website. Remove openid from session and kill it. - - url : /signout/" - """ - try: - del request.session['openid'] - except KeyError: - pass - logout(request) - return HttpResponseRedirect(get_next_url(request)) - -def xrdf(request): - url_host = get_url_host(request) - return_to = [ - "%s%s" % (url_host, reverse('user_complete_signin')) - ] - return render('authopenid/yadis.xrdf', { - 'return_to': return_to - }, 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 - """ - msg = request.GET.get('msg', '') - is_openid = True - - try: - uassoc = UserAssociation.objects.get( - user__username__exact=request.user.username - ) - except: - is_openid = False - - - return render('authopenid/settings.html', { - 'msg': msg, - 'is_openid': is_openid - }, context_instance=RequestContext(request)) - -@login_required -def changepw(request): - """ - change password view. - - url : /changepw/ - template: authopenid/changepw.html - """ - user_ = request.user - - if user_.has_usable_password(): - if settings.USE_EXTERNAL_LEGACY_LOGIN == True: - return HttpResponseRedirect(reverse('user_external_legacy_login_issues')) - else: - raise Http404 - - if request.POST: - form = 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('authopenid/changepw.html', {'form': form }, - context_instance=RequestContext(request)) - -def find_email_validation_messages(user): - msg_text = _('your email needs to be validated see %(details_url)s') \ - % {'details_url':reverse('faq') + '#validate'} - return user.message_set.filter(message__exact=msg_text) - -def set_email_validation_message(user): - messages = find_email_validation_messages(user) - msg_text = _('your email needs to be validated see %(details_url)s') \ - % {'details_url':reverse('faq') + '#validate'} - if len(messages) == 0: - user.message_set.create(message=msg_text) - -def clear_email_validation_message(user): - messages = find_email_validation_messages(user) - messages.delete() - -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) - -def _send_email_key(user): - """private function. sends email containing validation key - to user's email address - """ - subject = _("Email verification subject line") - message_template = loader.get_template('authopenid/email_validation.txt') - import settings - message_context = Context({ - 'validation_link': settings.APP_URL + reverse('user_verifyemail', kwargs={'id':user.id,'key':user.email_key}) - }) - message = message_template.render(message_context) - send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [user.email]) - -def send_new_email_key(user,nomessage=False): - import random - random.seed() - user.email_key = '%032x' % random.getrandbits(128) - user.save() - _send_email_key(user) - if nomessage==False: - set_email_validation_message(user) - -@login_required -def send_email_key(request): - """ - url = /email/sendkey/ - - view that is shown right after sending email key - email sending is called internally - - raises 404 if email validation is off - if current email is valid shows 'key_not_sent' view of - authopenid/changeemail.html template - """ - - if settings.EMAIL_VALIDATION != 'off': - if request.user.email_isvalid: - return render('authopenid/changeemail.html', - { 'email': request.user.email, - 'action_type': 'key_not_sent', - 'change_link': reverse('user_changeemail')}, - context_instance=RequestContext(request) - ) - else: - send_new_email_key(request.user) - return validation_email_sent(request) - else: - raise Http404 - - -#internal server view used as return value by other views -def validation_email_sent(request): - return render('authopenid/changeemail.html', - { 'email': request.user.email, - 'change_email_url': reverse('user_changeemail'), - 'action_type': 'validate', }, - context_instance=RequestContext(request)) - -def verifyemail(request,id=None,key=None): - """ - view that is shown when user clicks email validation link - url = /email/verify/{{user.id}}/{{user.email_key}}/ - """ - if settings.EMAIL_VALIDATION != 'off': - user = User.objects.get(id=id) - if user: - if user.email_key == key: - user.email_isvalid = True - clear_email_validation_message(user) - user.save() - return render('authopenid/changeemail.html', { - 'action_type': 'validation_complete', - }, context_instance=RequestContext(request)) - raise Http404 - -@login_required -def changeemail(request, action='change'): - """ - changeemail view. requires openid with request type GET - - url: /email/* - - template : authopenid/changeemail.html - """ - msg = request.GET.get('msg', None) - extension_args = {} - user_ = request.user - - if request.POST: - if 'cancel' in request.POST: - msg = _('your email was not changed') - request.user.message_set.create(message=msg) - return HttpResponseRedirect(get_next_url(request)) - form = ChangeEmailForm(request.POST, user=user_) - if form.is_valid(): - new_email = form.cleaned_data['email'] - if new_email != user_.email: - if settings.EMAIL_VALIDATION == 'on': - action = 'validate' - else: - action = 'done_novalidate' - set_new_email(user_, new_email,nomessage=True) - else: - action = 'keep' - - elif not request.POST and 'openid.mode' in request.GET: - redirect_to = get_url_host(request) + reverse('user_changeemail') - return complete(request, emailopenid_success, - emailopenid_failure, redirect_to) - else: - form = ChangeEmailForm(initial={'email': user_.email}, - user=user_) - - output = render('authopenid/changeemail.html', { - 'form': form, - 'email': user_.email, - 'action_type': action, - 'gravatar_faq_url': reverse('faq') + '#gravatar', - 'change_email_url': reverse('user_changeemail'), - 'msg': msg - }, context_instance=RequestContext(request)) - - if action == 'validate': - set_email_validation_message(user_) - - return output - -def emailopenid_success(request, identity_url, openid_response): - openid_ = from_openid_response(openid_response) - - user_ = request.user - try: - uassoc = UserAssociation.objects.get( - openid_url__exact=identity_url - ) - except: - return emailopenid_failure(request, - _("No OpenID %s found associated in our database" % identity_url)) - - if uassoc.user.username != request.user.username: - return emailopenid_failure(request, - _("The OpenID %s isn't associated to current user logged in" % - identity_url)) - - new_email = request.session.get('new_email', '') - if new_email: - user_.email = new_email - user_.save() - del request.session['new_email'] - msg = _("Email Changed.") - - redirect = "%s?msg=%s" % (reverse('user_account_settings'), - urlquote_plus(msg)) - return HttpResponseRedirect(redirect) - - -def emailopenid_failure(request, message): - redirect_to = "%s?msg=%s" % ( - reverse('user_changeemail'), urlquote_plus(message)) - return HttpResponseRedirect(redirect_to) - -@login_required -def changeopenid(request): - """ - change openid view. Allow user to change openid - associated to its username. - - url : /changeopenid/ - - template: authopenid/changeopenid.html - """ - - extension_args = {} - openid_url = '' - has_openid = True - msg = request.GET.get('msg', '') - - user_ = request.user - - try: - uopenid = UserAssociation.objects.get(user=user_) - openid_url = uopenid.openid_url - except: - has_openid = False - - redirect_to = get_url_host(request) + reverse('user_changeopenid') - if request.POST and has_openid: - form = ChangeopenidForm(request.POST, user=user_) - if form.is_valid(): - return ask_openid(request, form.cleaned_data['openid_url'], - redirect_to, on_failure=changeopenid_failure) - elif not request.POST and has_openid: - if 'openid.mode' in request.GET: - return complete(request, changeopenid_success, - changeopenid_failure, redirect_to) - - form = ChangeopenidForm(initial={'openid_url': openid_url }, user=user_) - return render('authopenid/changeopenid.html', { - 'form': form, - 'has_openid': has_openid, - 'msg': msg - }, context_instance=RequestContext(request)) - -def changeopenid_success(request, identity_url, openid_response): - openid_ = from_openid_response(openid_response) - is_exist = True - try: - uassoc = UserAssociation.objects.get(openid_url__exact=identity_url) - except: - is_exist = False - - if not is_exist: - try: - uassoc = UserAssociation.objects.get( - user__username__exact=request.user.username - ) - uassoc.openid_url = identity_url - uassoc.save() - except: - uassoc = UserAssociation(user=request.user, - openid_url=identity_url) - uassoc.save() - elif uassoc.user.username != request.user.username: - return changeopenid_failure(request, - _('This OpenID is already associated with another account.')) - - request.session['openids'] = [] - request.session['openids'].append(openid_) - - msg = _("OpenID %s is now associated with your account." % identity_url) - redirect = "%s?msg=%s" % ( - reverse('user_account_settings'), - urlquote_plus(msg)) - return HttpResponseRedirect(redirect) - - -def changeopenid_failure(request, message): - redirect_to = "%s?msg=%s" % ( - reverse('user_changeopenid'), - urlquote_plus(message)) - return HttpResponseRedirect(redirect_to) - -@login_required -def delete(request): - """ - delete view. Allow user to delete its account. Password/openid are required to - confirm it. He should also check the confirm checkbox. - - url : /delete - - template : authopenid/delete.html - """ - - extension_args = {} - - user_ = request.user - - redirect_to = get_url_host(request) + reverse('user_delete') - if request.POST: - form = DeleteForm(request.POST, user=user_) - if form.is_valid(): - if not form.test_openid: - user_.delete() - return signout(request) - else: - return ask_openid(request, form.cleaned_data['password'], - redirect_to, on_failure=deleteopenid_failure) - elif not request.POST and 'openid.mode' in request.GET: - return complete(request, deleteopenid_success, deleteopenid_failure, - redirect_to) - - form = DeleteForm(user=user_) - - msg = request.GET.get('msg','') - return render('authopenid/delete.html', { - 'form': form, - 'msg': msg, - }, context_instance=RequestContext(request)) - -def deleteopenid_success(request, identity_url, openid_response): - openid_ = from_openid_response(openid_response) - - user_ = request.user - try: - uassoc = UserAssociation.objects.get( - openid_url__exact=identity_url - ) - except: - return deleteopenid_failure(request, - _("No OpenID %s found associated in our database" % identity_url)) - - if uassoc.user.username == user_.username: - user_.delete() - return signout(request) - else: - return deleteopenid_failure(request, - _("The OpenID %s isn't associated to current user logged in" % - identity_url)) - - msg = _("Account deleted.") - redirect = reverse('index') + u"/?msg=%s" % (urlquote_plus(msg)) - return HttpResponseRedirect(redirect) - - -def deleteopenid_failure(request, message): - redirect_to = "%s?msg=%s" % (reverse('user_delete'), urlquote_plus(message)) - return HttpResponseRedirect(redirect_to) - -def external_legacy_login_info(request): - return render('authopenid/external_legacy_login_info.html', context_instance=RequestContext(request)) - -def sendpw(request): - """ - send a new password to the user. It return a mail with - a new pasword and a confirm link in. To activate the - new password, the user should click on confirm link. - - url : /sendpw/ - - templates : authopenid/sendpw_email.txt, authopenid/sendpw.html - """ - if settings.USE_EXTERNAL_LEGACY_LOGIN == True: - return HttpResponseRedirect(reverse('user_external_legacy_login_issues')) - - msg = request.GET.get('msg','') - if request.POST: - form = EmailPasswordForm(request.POST) - if form.is_valid(): - new_pw = User.objects.make_random_password() - confirm_key = UserPasswordQueue.objects.get_new_confirm_key() - try: - uqueue = UserPasswordQueue.objects.get( - user=form.user_cache - ) - except: - uqueue = UserPasswordQueue( - user=form.user_cache - ) - uqueue.new_password = new_pw - uqueue.confirm_key = confirm_key - uqueue.save() - # send email - subject = _("Request for new password") - message_template = loader.get_template( - 'authopenid/sendpw_email.txt') - key_link = settings.APP_URL + reverse('user_confirmchangepw') + '?key=' + confirm_key - message_context = Context({ - 'site_url': settings.APP_URL + reverse('index'), - 'key_link': key_link, - 'username': form.user_cache.username, - 'password': new_pw, - }) - message = message_template.render(message_context) - send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, - [form.user_cache.email]) - msg = _("A new password and the activation link were sent to your email address.") - else: - form = EmailPasswordForm() - - return render('authopenid/sendpw.html', { - 'form': form, - 'msg': msg - }, context_instance=RequestContext(request)) - - -def confirmchangepw(request): - """ - view to set new password when the user click on confirm link - in its mail. Basically it check if the confirm key exist, then - replace old password with new password and remove confirm - ley from the queue. Then it redirect the user to signin - page. - - url : /sendpw/confirm/?key - - """ - confirm_key = request.GET.get('key', '') - if not confirm_key: - return HttpResponseRedirect(reverse('index')) - - try: - uqueue = UserPasswordQueue.objects.get( - confirm_key__exact=confirm_key - ) - except: - msg = _("Could not change password. Confirmation key '%s'\ - is not registered." % confirm_key) - redirect = "%s?msg=%s" % ( - reverse('user_sendpw'), urlquote_plus(msg)) - return HttpResponseRedirect(redirect) - - try: - user_ = User.objects.get(id=uqueue.user.id) - except: - msg = _("Can not change password. User don't exist anymore \ - in our database.") - redirect = "%s?msg=%s" % (reverse('user_sendpw'), - urlquote_plus(msg)) - return HttpResponseRedirect(redirect) - - user_.set_password(uqueue.new_password) - user_.save() - uqueue.delete() - msg = _("Password changed for %s. You may now sign in." % - user_.username) - redirect = "%s?msg=%s" % (reverse('user_signin'), - urlquote_plus(msg)) - - return HttpResponseRedirect(redirect) +# -*- coding: utf-8 -*- +# Copyright (c) 2007, 2008, Benoît Chesneau +# Copyright (c) 2007 Simon Willison, original work on django-openid +# +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# * notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# * notice, this list of conditions and the following disclaimer in the +# * documentation and/or other materials provided with the +# * distribution. Neither the name of the nor the names +# * of its contributors may be used to endorse or promote products +# * derived from this software without specific prior written +# * permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from django.http import HttpResponseRedirect, get_host, Http404, \ + HttpResponseServerError +from django.shortcuts import render_to_response as render +from django.template import RequestContext, loader, Context +from django.conf import settings +from django.contrib.auth.models import User +from django.contrib.auth.decorators import login_required +from django.contrib.auth import authenticate +from django.core.urlresolvers import reverse +from django.utils.encoding import smart_unicode +from django.utils.html import escape +from django.utils.translation import ugettext as _ +from django.utils.http import urlquote_plus +from django.utils.safestring import mark_safe +from django.core.mail import send_mail +from django.views.defaults import server_error + +from openid.consumer.consumer import Consumer, \ + SUCCESS, CANCEL, FAILURE, SETUP_NEEDED +from openid.consumer.discover import DiscoveryFailure +from openid.extensions import sreg +# needed for some linux distributions like debian +try: + from openid.yadis import xri +except ImportError: + from yadis import xri + +import re +import urllib + + +from forum.forms import EditUserEmailFeedsForm +from django_authopenid.util import OpenID, DjangoOpenIDStore, from_openid_response, get_next_url +from django_authopenid.models import UserAssociation, UserPasswordQueue, ExternalLoginData +from django_authopenid.forms import OpenidSigninForm, ClassicLoginForm, OpenidRegisterForm, \ + OpenidVerifyForm, ClassicRegisterForm, ChangePasswordForm, ChangeEmailForm, \ + ChangeopenidForm, DeleteForm, EmailPasswordForm +import external_login +import logging + +def login(request,user): + from django.contrib.auth import login as _login + from forum.models import user_logged_in #custom signal + + print 'in login call' + + if settings.USE_EXTERNAL_LEGACY_LOGIN == True: + external_login.login(request,user) + + #1) get old session key + session_key = request.session.session_key + #2) login and get new session key + _login(request,user) + #3) send signal with old session key as argument + user_logged_in.send(user=user,session_key=session_key,sender=None) + +def logout(request): + from django.contrib.auth import logout as _logout#for login I've added wrapper below - called login + _logout(request) + if settings.USE_EXTERNAL_LEGACY_LOGIN == True: + external_login.logout(request) + +def get_url_host(request): + if request.is_secure(): + protocol = 'https' + 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() + +def ask_openid(request, openid_url, redirect_to, on_failure=None, + sreg_request=None): + """ basic function to ask openid and return response """ + request.encoding = 'UTF-8' + on_failure = on_failure or signin_failure + + trust_root = getattr( + settings, 'OPENID_TRUST_ROOT', get_url_host(request) + '/' + ) + if xri.identifierScheme(openid_url) == 'XRI' and getattr( + settings, 'OPENID_DISALLOW_INAMES', False + ): + msg = _("i-names are not supported") + return on_failure(request, msg) + consumer = Consumer(request.session, DjangoOpenIDStore()) + try: + auth_request = consumer.begin(openid_url) + except DiscoveryFailure: + msg = _(u"OpenID %(openid_url)s is invalid" % {'openid_url':openid_url}) + return on_failure(request, msg) + + if sreg_request: + auth_request.addExtension(sreg_request) + redirect_url = auth_request.redirectURL(trust_root, redirect_to) + return HttpResponseRedirect(redirect_url) + +def complete(request, on_success=None, on_failure=None, return_to=None): + """ complete openid signin """ + on_success = on_success or default_on_success + on_failure = on_failure or default_on_failure + + consumer = Consumer(request.session, DjangoOpenIDStore()) + # make sure params are encoded in utf8 + params = dict((k,smart_unicode(v)) for k, v in request.GET.items()) + openid_response = consumer.complete(params, return_to) + + if openid_response.status == SUCCESS: + return on_success(request, openid_response.identity_url, + openid_response) + elif openid_response.status == CANCEL: + return on_failure(request, 'The request was canceled') + elif openid_response.status == FAILURE: + return on_failure(request, openid_response.message) + elif openid_response.status == SETUP_NEEDED: + return on_failure(request, 'Setup needed') + else: + assert False, "Bad openid status: %s" % openid_response.status + +def default_on_success(request, identity_url, openid_response): + """ default action on openid signin success """ + request.session['openid'] = from_openid_response(openid_response) + return HttpResponseRedirect(get_next_url(request)) + +def default_on_failure(request, message): + """ default failure action on signin """ + return render('openid_failure.html', { + 'message': message + }) + + +def not_authenticated(func): + """ decorator that redirect user to next page if + he is already logged.""" + def decorated(request, *args, **kwargs): + if request.user.is_authenticated(): + return HttpResponseRedirect(get_next_url(request)) + return func(request, *args, **kwargs) + return decorated + +@not_authenticated +def signin(request,newquestion=False,newanswer=False): + """ + signin page. It manages the legacy authentification (user/password) + and openid authentification + + url: /signin/ + + template : authopenid/signin.htm + """ + request.encoding = 'UTF-8' + on_failure = signin_failure + email_feeds_form = EditUserEmailFeedsForm() + next = get_next_url(request) + form_signin = OpenidSigninForm(initial={'next':next}) + form_auth = ClassicLoginForm(initial={'next':next}) + + if request.POST: + #'blogin' - password login + if 'blogin' in request.POST.keys(): + form_auth = ClassicLoginForm(request.POST) + if form_auth.is_valid(): + #have login and password and need to login through external website + if settings.USE_EXTERNAL_LEGACY_LOGIN == True: + username = form_auth.cleaned_data['username'] + password = form_auth.cleaned_data['password'] + next = form_auth.cleaned_data['next'] + if form_auth.get_user() == None: + #need to create internal user + + #1) save login and password temporarily in session + request.session['external_username'] = username + request.session['external_password'] = password + + #2) see if username clashes with some existing user + #if so, we have to prompt the user to pick a different name + username_taken = User.is_username_taken(username) + #try: + # User.objects.get(username=username) + # username_taken = True + #except User.DoesNotExist: + # username_taken = False + + #3) try to extract user email from external service + email = external_login.get_email(username,password) + + email_feeds_form = EditUserEmailFeedsForm() + form_data = {'username':username,'email':email,'next':next} + form = OpenidRegisterForm(initial=form_data) + template_data = {'form1':form,'username':username,\ + 'email_feeds_form':email_feeds_form,\ + 'provider':mark_safe(settings.EXTERNAL_LEGACY_LOGIN_PROVIDER_NAME),\ + 'login_type':'legacy',\ + 'gravatar_faq_url':reverse('faq') + '#gravatar',\ + 'external_login_name_is_taken':username_taken} + return render('authopenid/complete.html',template_data,\ + context_instance=RequestContext(request)) + else: + #user existed, external password is ok + user = form_auth.get_user() + login(request,user) + response = HttpResponseRedirect(get_next_url(request)) + external_login.set_login_cookies(response,user) + return response + else: + #regular password authentication + user = form_auth.get_user() + login(request, user) + return HttpResponseRedirect(get_next_url(request)) + + elif 'bnewaccount' in request.POST.keys(): + #register externally logged in password user with a new local account + if settings.USE_EXTERNAL_LEGACY_LOGIN == True: + form = OpenidRegisterForm(request.POST) + email_feeds_form = EditUserEmailFeedsForm(request.POST) + form1_is_valid = form.is_valid() + form2_is_valid = email_feeds_form.is_valid() + if form1_is_valid and form2_is_valid: + #create the user + username = form.cleaned_data['username'] + password = request.session.get('external_password',None) + email = form.cleaned_data['email'] + print 'got email addr %s' % email + if password and username: + User.objects.create_user(username,email,password) + user = authenticate(username=username,password=password) + external_username = request.session['external_username'] + eld = ExternalLoginData.objects.get(external_username=external_username) + eld.user = user + eld.save() + login(request,user) + email_feeds_form.save(user) + del request.session['external_username'] + del request.session['external_password'] + return HttpResponseRedirect(reverse('index')) + else: + if password: + del request.session['external_username'] + if username: + del request.session['external_password'] + return HttpResponseServerError() + else: + username = request.POST.get('username',None) + provider = mark_safe(settings.EXTERNAL_LEGACY_LOGIN_PROVIDER_NAME) + username_taken = User.is_username_taken(username) + data = {'login_type':'legacy','form1':form,'username':username,\ + 'email_feeds_form':email_feeds_form,'provider':provider,\ + 'gravatar_faq_url':reverse('faq') + '#gravatar',\ + 'external_login_name_is_taken':username_taken} + return render('authopenid/complete.html',data, + context_instance=RequestContext(request)) + else: + raise Http404 + + elif 'bsignin' in request.POST.keys() or 'openid_username' in request.POST.keys(): + form_signin = OpenidSigninForm(request.POST) + if form_signin.is_valid(): + next = form_signin.cleaned_data['next'] + sreg_req = sreg.SRegRequest(optional=['nickname', 'email']) + redirect_to = "%s%s?%s" % ( + get_url_host(request), + reverse('user_complete_signin'), + urllib.urlencode({'next':next}) + ) + return ask_openid(request, + form_signin.cleaned_data['openid_url'], + redirect_to, + on_failure=signin_failure, + sreg_request=sreg_req) + + + #if request is GET + question = None + if newquestion == True: + from forum.models import AnonymousQuestion as AQ + session_key = request.session.session_key + qlist = AQ.objects.filter(session_key=session_key).order_by('-added_at') + if len(qlist) > 0: + question = qlist[0] + answer = None + if newanswer == True: + from forum.models import AnonymousAnswer as AA + session_key = request.session.session_key + alist = AA.objects.filter(session_key=session_key).order_by('-added_at') + if len(alist) > 0: + answer = alist[0] + + return render('authopenid/signin.html', { + 'question':question, + 'answer':answer, + 'form1': form_auth, + 'form2': form_signin, + 'msg': request.GET.get('msg',''), + 'sendpw_url': reverse('user_sendpw'), + 'fb_api_key': settings.FB_API_KEY, + }, context_instance=RequestContext(request)) + +def complete_signin(request): + """ in case of complete signin with openid """ + return complete(request, signin_success, signin_failure, + get_url_host(request) + reverse('user_complete_signin')) + +def signin_success(request, identity_url, openid_response): + """ + openid signin success. + + If the openid is already registered, the user is redirected to + url set par next or in settings with OPENID_REDIRECT_NEXT variable. + If none of these urls are set user is redirectd to /. + + if openid isn't registered user is redirected to register page. + """ + + openid_ = from_openid_response(openid_response) #create janrain OpenID object + request.session['openid'] = openid_ + try: + rel = UserAssociation.objects.get(openid_url__exact = str(openid_)) + except: + # try to register this new user + return register(request) + user_ = rel.user + if user_.is_active: + user_.backend = "django.contrib.auth.backends.ModelBackend" + login(request, user_) + + return HttpResponseRedirect(get_next_url(request)) + +def is_association_exist(openid_url): + """ test if an openid is already in database """ + is_exist = True + try: + uassoc = UserAssociation.objects.get(openid_url__exact = openid_url) + except: + is_exist = False + return is_exist + +@not_authenticated +def register(request): + """ + register an openid. + + If user is already a member he can associate its openid with + its account. + + A new account could also be created and automaticaly associated + to the openid. + + url : /complete/ + + template : authopenid/complete.html + """ + + openid_ = request.session.get('openid', None) + next = get_next_url(request) + if not openid_: + return HttpResponseRedirect(reverse('user_signin') + '?next=%s' % next) + + nickname = openid_.sreg.get('nickname', '') + email = openid_.sreg.get('email', '') + form1 = OpenidRegisterForm(initial={ + 'next': next, + 'username': nickname, + 'email': email, + }) + form2 = OpenidVerifyForm(initial={ + 'next': next, + 'username': nickname, + }) + email_feeds_form = EditUserEmailFeedsForm() + + user_ = None + is_redirect = False + if request.POST: + if 'bnewaccount' in request.POST.keys(): + form1 = OpenidRegisterForm(request.POST) + email_feeds_form = EditUserEmailFeedsForm(request.POST) + if form1.is_valid() and email_feeds_form.is_valid(): + next = form1.cleaned_data['next'] + is_redirect = True + tmp_pwd = User.objects.make_random_password() + user_ = User.objects.create_user(form1.cleaned_data['username'], + form1.cleaned_data['email'], tmp_pwd) + + user_.set_unusable_password() + # make association with openid + uassoc = UserAssociation(openid_url=str(openid_), + user_id=user_.id) + uassoc.save() + + # login + user_.backend = "django.contrib.auth.backends.ModelBackend" + login(request, user_) + email_feeds_form.save(user_) + elif 'bverify' in request.POST.keys(): + form2 = OpenidVerifyForm(request.POST) + if form2.is_valid(): + is_redirect = True + next = form2.cleaned_data['next'] + user_ = form2.get_user() + + uassoc = UserAssociation(openid_url=str(openid_), + user_id=user_.id) + uassoc.save() + login(request, user_) + + #check if we need to post a question that was added anonymously + #this needs to be a function call becase this is also done + #if user just logged in and did not need to create the new account + + if user_ != None: + if settings.EMAIL_VALIDATION == 'on': + send_new_email_key(user_,nomessage=True) + output = validation_email_sent(request) + set_email_validation_message(user_) #message set after generating view + return output + if user_.is_authenticated(): + return HttpResponseRedirect(reverse('index')) + else: + raise Exception('openid login failed')#should not ever get here + + openid_str = str(openid_) + bits = openid_str.split('/') + base_url = bits[2] #assume this is base url + url_bits = base_url.split('.') + provider_name = url_bits[-2].lower() + + providers = {'yahoo':'Yahoo!', + 'flickr':'flickr™', + 'google':'Google™', + 'aol':'AOL', + 'myopenid':'MyOpenID', + } + if provider_name not in providers: + provider_logo = provider_name + else: + provider_logo = providers[provider_name] + + return render('authopenid/complete.html', { + 'form1': form1, + 'form2': form2, + 'email_feeds_form': email_feeds_form, + 'provider':mark_safe(provider_logo), + 'username': nickname, + 'email': email, + 'login_type':'openid', + 'gravatar_faq_url':reverse('faq') + '#gravatar', + }, context_instance=RequestContext(request)) + +def signin_failure(request, message): + """ + falure with openid signin. Go back to signin page. + + template : "authopenid/signin.html" + """ + next = get_next_url(request) + form_signin = OpenidSigninForm(initial={'next': next}) + form_auth = ClassicLoginForm(initial={'next': next}) + + return render('authopenid/signin.html', { + 'msg': message, + 'form1': form_auth, + 'form2': form_signin, + }, context_instance=RequestContext(request)) + +@not_authenticated +def signup(request): + """ + signup page. Create a legacy account + + url : /signup/" + + templates: authopenid/signup.html, authopenid/confirm_email.txt + """ + if settings.USE_EXTERNAL_LEGACY_LOGIN == True: + return HttpResponseRedirect(reverse('user_external_legacy_login_issues')) + next = get_next_url(request) + if request.POST: + form = ClassicRegisterForm(request.POST) + email_feeds_form = EditUserEmailFeedsForm(request.POST) + + #validation outside if to remember form values + form1_is_valid = form.is_valid() + form2_is_valid = email_feeds_form.is_valid() + if form1_is_valid and form2_is_valid: + next = form.cleaned_data['next'] + username = form.cleaned_data['username'] + password = form.cleaned_data['password1'] + email = form.cleaned_data['email'] + + user_ = User.objects.create_user( username,email,password ) + if settings.USE_EXTERNAL_LEGACY_LOGIN == True: + external_login.create_user(username,email,password) + + user_.backend = "django.contrib.auth.backends.ModelBackend" + login(request, user_) + email_feeds_form.save(user_) + + # send email + subject = _("Welcome email subject line") + message_template = loader.get_template( + 'authopenid/confirm_email.txt' + ) + message_context = Context({ + 'signup_url': settings.APP_URL + reverse('user_signin'), + 'username': username, + 'password': password, + }) + message = message_template.render(message_context) + send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, + [user_.email]) + return HttpResponseRedirect(next) + else: + form = ClassicRegisterForm(initial={'next':next}) + email_feeds_form = EditUserEmailFeedsForm() + return render('authopenid/signup.html', { + 'form': form, + 'email_feeds_form': email_feeds_form + }, context_instance=RequestContext(request)) + #what if request is not posted? + +@login_required +def signout(request): + """ + signout from the website. Remove openid from session and kill it. + + url : /signout/" + """ + try: + del request.session['openid'] + except KeyError: + pass + logout(request) + return HttpResponseRedirect(get_next_url(request)) + +def xrdf(request): + url_host = get_url_host(request) + return_to = [ + "%s%s" % (url_host, reverse('user_complete_signin')) + ] + return render('authopenid/yadis.xrdf', { + 'return_to': return_to + }, 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 + """ + msg = request.GET.get('msg', '') + is_openid = True + + try: + uassoc = UserAssociation.objects.get( + user__username__exact=request.user.username + ) + except: + is_openid = False + + + return render('authopenid/settings.html', { + 'msg': msg, + 'is_openid': is_openid + }, context_instance=RequestContext(request)) + +@login_required +def changepw(request): + """ + change password view. + + url : /changepw/ + template: authopenid/changepw.html + """ + user_ = request.user + + if user_.has_usable_password(): + if settings.USE_EXTERNAL_LEGACY_LOGIN == True: + return HttpResponseRedirect(reverse('user_external_legacy_login_issues')) + else: + raise Http404 + + if request.POST: + form = 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('authopenid/changepw.html', {'form': form }, + context_instance=RequestContext(request)) + +def find_email_validation_messages(user): + msg_text = _('your email needs to be validated see %(details_url)s') \ + % {'details_url':reverse('faq') + '#validate'} + return user.message_set.filter(message__exact=msg_text) + +def set_email_validation_message(user): + messages = find_email_validation_messages(user) + msg_text = _('your email needs to be validated see %(details_url)s') \ + % {'details_url':reverse('faq') + '#validate'} + if len(messages) == 0: + user.message_set.create(message=msg_text) + +def clear_email_validation_message(user): + messages = find_email_validation_messages(user) + messages.delete() + +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) + +def _send_email_key(user): + """private function. sends email containing validation key + to user's email address + """ + subject = _("Email verification subject line") + message_template = loader.get_template('authopenid/email_validation.txt') + import settings + message_context = Context({ + 'validation_link': settings.APP_URL + reverse('user_verifyemail', kwargs={'id':user.id,'key':user.email_key}) + }) + message = message_template.render(message_context) + send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [user.email]) + +def send_new_email_key(user,nomessage=False): + import random + random.seed() + user.email_key = '%032x' % random.getrandbits(128) + user.save() + _send_email_key(user) + if nomessage==False: + set_email_validation_message(user) + +@login_required +def send_email_key(request): + """ + url = /email/sendkey/ + + view that is shown right after sending email key + email sending is called internally + + raises 404 if email validation is off + if current email is valid shows 'key_not_sent' view of + authopenid/changeemail.html template + """ + + if settings.EMAIL_VALIDATION != 'off': + if request.user.email_isvalid: + return render('authopenid/changeemail.html', + { 'email': request.user.email, + 'action_type': 'key_not_sent', + 'change_link': reverse('user_changeemail')}, + context_instance=RequestContext(request) + ) + else: + send_new_email_key(request.user) + return validation_email_sent(request) + else: + raise Http404 + + +#internal server view used as return value by other views +def validation_email_sent(request): + return render('authopenid/changeemail.html', + { 'email': request.user.email, + 'change_email_url': reverse('user_changeemail'), + 'action_type': 'validate', }, + context_instance=RequestContext(request)) + +def verifyemail(request,id=None,key=None): + """ + view that is shown when user clicks email validation link + url = /email/verify/{{user.id}}/{{user.email_key}}/ + """ + if settings.EMAIL_VALIDATION != 'off': + user = User.objects.get(id=id) + if user: + if user.email_key == key: + user.email_isvalid = True + clear_email_validation_message(user) + user.save() + return render('authopenid/changeemail.html', { + 'action_type': 'validation_complete', + }, context_instance=RequestContext(request)) + raise Http404 + +@login_required +def changeemail(request, action='change'): + """ + changeemail view. requires openid with request type GET + + url: /email/* + + template : authopenid/changeemail.html + """ + msg = request.GET.get('msg', None) + extension_args = {} + user_ = request.user + + if request.POST: + if 'cancel' in request.POST: + msg = _('your email was not changed') + request.user.message_set.create(message=msg) + return HttpResponseRedirect(get_next_url(request)) + form = ChangeEmailForm(request.POST, user=user_) + if form.is_valid(): + new_email = form.cleaned_data['email'] + if new_email != user_.email: + if settings.EMAIL_VALIDATION == 'on': + action = 'validate' + else: + action = 'done_novalidate' + set_new_email(user_, new_email,nomessage=True) + else: + action = 'keep' + + elif not request.POST and 'openid.mode' in request.GET: + redirect_to = get_url_host(request) + reverse('user_changeemail') + return complete(request, emailopenid_success, + emailopenid_failure, redirect_to) + else: + form = ChangeEmailForm(initial={'email': user_.email}, + user=user_) + + output = render('authopenid/changeemail.html', { + 'form': form, + 'email': user_.email, + 'action_type': action, + 'gravatar_faq_url': reverse('faq') + '#gravatar', + 'change_email_url': reverse('user_changeemail'), + 'msg': msg + }, context_instance=RequestContext(request)) + + if action == 'validate': + set_email_validation_message(user_) + + return output + +def emailopenid_success(request, identity_url, openid_response): + openid_ = from_openid_response(openid_response) + + user_ = request.user + try: + uassoc = UserAssociation.objects.get( + openid_url__exact=identity_url + ) + except: + return emailopenid_failure(request, + _("No OpenID %s found associated in our database" % identity_url)) + + if uassoc.user.username != request.user.username: + return emailopenid_failure(request, + _("The OpenID %s isn't associated to current user logged in" % + identity_url)) + + new_email = request.session.get('new_email', '') + if new_email: + user_.email = new_email + user_.save() + del request.session['new_email'] + msg = _("Email Changed.") + + redirect = "%s?msg=%s" % (reverse('user_account_settings'), + urlquote_plus(msg)) + return HttpResponseRedirect(redirect) + + +def emailopenid_failure(request, message): + redirect_to = "%s?msg=%s" % ( + reverse('user_changeemail'), urlquote_plus(message)) + return HttpResponseRedirect(redirect_to) + +@login_required +def changeopenid(request): + """ + change openid view. Allow user to change openid + associated to its username. + + url : /changeopenid/ + + template: authopenid/changeopenid.html + """ + + extension_args = {} + openid_url = '' + has_openid = True + msg = request.GET.get('msg', '') + + user_ = request.user + + try: + uopenid = UserAssociation.objects.get(user=user_) + openid_url = uopenid.openid_url + except: + has_openid = False + + redirect_to = get_url_host(request) + reverse('user_changeopenid') + if request.POST and has_openid: + form = ChangeopenidForm(request.POST, user=user_) + if form.is_valid(): + return ask_openid(request, form.cleaned_data['openid_url'], + redirect_to, on_failure=changeopenid_failure) + elif not request.POST and has_openid: + if 'openid.mode' in request.GET: + return complete(request, changeopenid_success, + changeopenid_failure, redirect_to) + + form = ChangeopenidForm(initial={'openid_url': openid_url }, user=user_) + return render('authopenid/changeopenid.html', { + 'form': form, + 'has_openid': has_openid, + 'msg': msg + }, context_instance=RequestContext(request)) + +def changeopenid_success(request, identity_url, openid_response): + openid_ = from_openid_response(openid_response) + is_exist = True + try: + uassoc = UserAssociation.objects.get(openid_url__exact=identity_url) + except: + is_exist = False + + if not is_exist: + try: + uassoc = UserAssociation.objects.get( + user__username__exact=request.user.username + ) + uassoc.openid_url = identity_url + uassoc.save() + except: + uassoc = UserAssociation(user=request.user, + openid_url=identity_url) + uassoc.save() + elif uassoc.user.username != request.user.username: + return changeopenid_failure(request, + _('This OpenID is already associated with another account.')) + + request.session['openids'] = [] + request.session['openids'].append(openid_) + + msg = _("OpenID %s is now associated with your account." % identity_url) + redirect = "%s?msg=%s" % ( + reverse('user_account_settings'), + urlquote_plus(msg)) + return HttpResponseRedirect(redirect) + + +def changeopenid_failure(request, message): + redirect_to = "%s?msg=%s" % ( + reverse('user_changeopenid'), + urlquote_plus(message)) + return HttpResponseRedirect(redirect_to) + +@login_required +def delete(request): + """ + delete view. Allow user to delete its account. Password/openid are required to + confirm it. He should also check the confirm checkbox. + + url : /delete + + template : authopenid/delete.html + """ + + extension_args = {} + + user_ = request.user + + redirect_to = get_url_host(request) + reverse('user_delete') + if request.POST: + form = DeleteForm(request.POST, user=user_) + if form.is_valid(): + if not form.test_openid: + user_.delete() + return signout(request) + else: + return ask_openid(request, form.cleaned_data['password'], + redirect_to, on_failure=deleteopenid_failure) + elif not request.POST and 'openid.mode' in request.GET: + return complete(request, deleteopenid_success, deleteopenid_failure, + redirect_to) + + form = DeleteForm(user=user_) + + msg = request.GET.get('msg','') + return render('authopenid/delete.html', { + 'form': form, + 'msg': msg, + }, context_instance=RequestContext(request)) + +def deleteopenid_success(request, identity_url, openid_response): + openid_ = from_openid_response(openid_response) + + user_ = request.user + try: + uassoc = UserAssociation.objects.get( + openid_url__exact=identity_url + ) + except: + return deleteopenid_failure(request, + _("No OpenID %s found associated in our database" % identity_url)) + + if uassoc.user.username == user_.username: + user_.delete() + return signout(request) + else: + return deleteopenid_failure(request, + _("The OpenID %s isn't associated to current user logged in" % + identity_url)) + + msg = _("Account deleted.") + redirect = reverse('index') + u"/?msg=%s" % (urlquote_plus(msg)) + return HttpResponseRedirect(redirect) + + +def deleteopenid_failure(request, message): + redirect_to = "%s?msg=%s" % (reverse('user_delete'), urlquote_plus(message)) + return HttpResponseRedirect(redirect_to) + +def external_legacy_login_info(request): + return render('authopenid/external_legacy_login_info.html', context_instance=RequestContext(request)) + +def sendpw(request): + """ + send a new password to the user. It return a mail with + a new pasword and a confirm link in. To activate the + new password, the user should click on confirm link. + + url : /sendpw/ + + templates : authopenid/sendpw_email.txt, authopenid/sendpw.html + """ + if settings.USE_EXTERNAL_LEGACY_LOGIN == True: + return HttpResponseRedirect(reverse('user_external_legacy_login_issues')) + + msg = request.GET.get('msg','') + if request.POST: + form = EmailPasswordForm(request.POST) + if form.is_valid(): + new_pw = User.objects.make_random_password() + confirm_key = UserPasswordQueue.objects.get_new_confirm_key() + try: + uqueue = UserPasswordQueue.objects.get( + user=form.user_cache + ) + except: + uqueue = UserPasswordQueue( + user=form.user_cache + ) + uqueue.new_password = new_pw + uqueue.confirm_key = confirm_key + uqueue.save() + # send email + subject = _("Request for new password") + message_template = loader.get_template( + 'authopenid/sendpw_email.txt') + key_link = settings.APP_URL + reverse('user_confirmchangepw') + '?key=' + confirm_key + message_context = Context({ + 'site_url': settings.APP_URL + reverse('index'), + 'key_link': key_link, + 'username': form.user_cache.username, + 'password': new_pw, + }) + message = message_template.render(message_context) + send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, + [form.user_cache.email]) + msg = _("A new password and the activation link were sent to your email address.") + else: + form = EmailPasswordForm() + + return render('authopenid/sendpw.html', { + 'form': form, + 'msg': msg + }, context_instance=RequestContext(request)) + + +def confirmchangepw(request): + """ + view to set new password when the user click on confirm link + in its mail. Basically it check if the confirm key exist, then + replace old password with new password and remove confirm + ley from the queue. Then it redirect the user to signin + page. + + url : /sendpw/confirm/?key + + """ + confirm_key = request.GET.get('key', '') + if not confirm_key: + return HttpResponseRedirect(reverse('index')) + + try: + uqueue = UserPasswordQueue.objects.get( + confirm_key__exact=confirm_key + ) + except: + msg = _("Could not change password. Confirmation key '%s'\ + is not registered." % confirm_key) + redirect = "%s?msg=%s" % ( + reverse('user_sendpw'), urlquote_plus(msg)) + return HttpResponseRedirect(redirect) + + try: + user_ = User.objects.get(id=uqueue.user.id) + except: + msg = _("Can not change password. User don't exist anymore \ + in our database.") + redirect = "%s?msg=%s" % (reverse('user_sendpw'), + urlquote_plus(msg)) + return HttpResponseRedirect(redirect) + + user_.set_password(uqueue.new_password) + user_.save() + uqueue.delete() + msg = _("Password changed for %s. You may now sign in." % + user_.username) + redirect = "%s?msg=%s" % (reverse('user_signin'), + urlquote_plus(msg)) + + return HttpResponseRedirect(redirect) diff --git a/dos2unix.sh b/dos2unix.sh new file mode 100644 index 00000000..96a51c9d --- /dev/null +++ b/dos2unix.sh @@ -0,0 +1,10 @@ +dos2unix `find . -name '*.py'` +dos2unix `find . -name '*.po'` +dos2unix `find . -name '*.js'` +dos2unix `find . -name '*.css'` +dos2unix `find . -name '*.txt'` +dos2unix `find ./sphinx -type f` +dos2unix `find ./cron -type f` +dos2unix settings_local.py.dist +dos2unix README +dos2unix INSTALL diff --git a/fbconnect/tests.py b/fbconnect/tests.py index a6f218a9..2247054b 100755 --- a/fbconnect/tests.py +++ b/fbconnect/tests.py @@ -1,23 +1,23 @@ -""" -This file demonstrates two different styles of tests (one doctest and one -unittest). These will both pass when you run "manage.py test". - -Replace these with more appropriate tests for your application. -""" - -from django.test import TestCase - -class SimpleTest(TestCase): - def test_basic_addition(self): - """ - Tests that 1 + 1 always equals 2. - """ - self.failUnlessEqual(1 + 1, 2) - -__test__ = {"doctest": """ -Another way to test that 1 + 1 is equal to 2. - ->>> 1 + 1 == 2 -True -"""} - +""" +This file demonstrates two different styles of tests (one doctest and one +unittest). These will both pass when you run "manage.py test". + +Replace these with more appropriate tests for your application. +""" + +from django.test import TestCase + +class SimpleTest(TestCase): + def test_basic_addition(self): + """ + Tests that 1 + 1 always equals 2. + """ + self.failUnlessEqual(1 + 1, 2) + +__test__ = {"doctest": """ +Another way to test that 1 + 1 is equal to 2. + +>>> 1 + 1 == 2 +True +"""} + diff --git a/forum/forms.py b/forum/forms.py index d727440e..2d2021b5 100644 --- a/forum/forms.py +++ b/forum/forms.py @@ -1,318 +1,318 @@ -import re -from datetime import date -from django import forms -from models import * -from const import * -from django.utils.translation import ugettext as _ -from django_authopenid.forms import NextUrlField, UserNameField -import settings - -class TitleField(forms.CharField): - def __init__(self, *args, **kwargs): - super(TitleField, self).__init__(*args, **kwargs) - self.required = True - self.widget = forms.TextInput(attrs={'size' : 70, 'autocomplete' : 'off'}) - self.max_length = 255 - self.label = _('title') - self.help_text = _('please enter a descriptive title for your question') - self.initial = '' - - def clean(self, value): - if len(value) < 10: - raise forms.ValidationError(_('title must be > 10 characters')) - - return value - -class EditorField(forms.CharField): - def __init__(self, *args, **kwargs): - super(EditorField, self).__init__(*args, **kwargs) - self.required = True - self.widget = forms.Textarea(attrs={'id':'editor'}) - self.label = _('content') - self.help_text = u'' - self.initial = '' - - def clean(self, value): - if len(value) < 10: - raise forms.ValidationError(_('question content must be > 10 characters')) - - return value - -class TagNamesField(forms.CharField): - def __init__(self, *args, **kwargs): - super(TagNamesField, self).__init__(*args, **kwargs) - self.required = True - self.widget = forms.TextInput(attrs={'size' : 50, 'autocomplete' : 'off'}) - self.max_length = 255 - self.label = _('tags') - #self.help_text = _('please use space to separate tags (this enables autocomplete feature)') - self.help_text = _('Tags are short keywords, with no spaces within. Up to five tags can be used.') - self.initial = '' - - def clean(self, value): - value = super(TagNamesField, self).clean(value) - data = value.strip() - if len(data) < 1: - raise forms.ValidationError(_('tags are required')) - - split_re = re.compile(r'[ ,]+') - list = split_re.split(data) - list_temp = [] - if len(list) > 5: - raise forms.ValidationError(_('please use 5 tags or less')) - for tag in list: - if len(tag) > 20: - raise forms.ValidationError(_('tags must be shorter than 20 characters')) - #take tag regex from settings - tagname_re = re.compile(r'[a-z0-9]+') - if not tagname_re.match(tag): - raise forms.ValidationError(_('please use following characters in tags: letters \'a-z\', numbers, and characters \'.-_#\'')) - # only keep one same tag - if tag not in list_temp and len(tag.strip()) > 0: - list_temp.append(tag) - return u' '.join(list_temp) - -class WikiField(forms.BooleanField): - def __init__(self, *args, **kwargs): - super(WikiField, self).__init__(*args, **kwargs) - self.required = False - self.label = _('community wiki') - self.help_text = _('if you choose community wiki option, the question and answer do not generate points and name of author will not be shown') - def clean(self,value): - return value and settings.WIKI_ON - -class EmailNotifyField(forms.BooleanField): - def __init__(self, *args, **kwargs): - super(EmailNotifyField, self).__init__(*args, **kwargs) - self.required = False - self.widget.attrs['class'] = 'nomargin' - -class SummaryField(forms.CharField): - def __init__(self, *args, **kwargs): - super(SummaryField, self).__init__(*args, **kwargs) - self.required = False - self.widget = forms.TextInput(attrs={'size' : 50, 'autocomplete' : 'off'}) - self.max_length = 300 - self.label = _('update summary:') - self.help_text = _('enter a brief summary of your revision (e.g. fixed spelling, grammar, improved style, this field is optional)') - -class ModerateUserForm(forms.ModelForm): - is_approved = forms.BooleanField(label=_("Automatically accept user's contributions for the email updates"), - required=False) - - def clean_is_approved(self): - if 'is_approved' not in self.cleaned_data: - self.cleaned_data['is_approved'] = False - return self.cleaned_data['is_approved'] - - class Meta: - model = User - fields = ('is_approved',) - -class FeedbackForm(forms.Form): - name = forms.CharField(label=_('Your name:'), required=False) - email = forms.EmailField(label=_('Email (not shared with anyone):'), required=False) - message = forms.CharField(label=_('Your message:'), max_length=800,widget=forms.Textarea(attrs={'cols':60})) - next = NextUrlField() - -class AskForm(forms.Form): - title = TitleField() - text = EditorField() - tags = TagNamesField() - wiki = WikiField() - - openid = forms.CharField(required=False, max_length=255, widget=forms.TextInput(attrs={'size' : 40, 'class':'openid-input'})) - user = forms.CharField(required=False, max_length=255, widget=forms.TextInput(attrs={'size' : 35})) - email = forms.CharField(required=False, max_length=255, widget=forms.TextInput(attrs={'size' : 35})) - -class AnswerForm(forms.Form): - text = EditorField() - wiki = WikiField() - openid = forms.CharField(required=False, max_length=255, widget=forms.TextInput(attrs={'size' : 40, 'class':'openid-input'})) - user = forms.CharField(required=False, max_length=255, widget=forms.TextInput(attrs={'size' : 35})) - email = forms.CharField(required=False, max_length=255, widget=forms.TextInput(attrs={'size' : 35})) - email_notify = EmailNotifyField() - def __init__(self, question, user, *args, **kwargs): - super(AnswerForm, self).__init__(*args, **kwargs) - self.fields['email_notify'].widget.attrs['id'] = 'question-subscribe-updates'; - if question.wiki and settings.WIKI_ON: - self.fields['wiki'].initial = True - if user.is_authenticated(): - if user in question.followed_by.all(): - self.fields['email_notify'].initial = True - return - self.fields['email_notify'].initial = False - - -class CloseForm(forms.Form): - reason = forms.ChoiceField(choices=CLOSE_REASONS) - -class RetagQuestionForm(forms.Form): - tags = TagNamesField() - # initialize the default values - def __init__(self, question, *args, **kwargs): - super(RetagQuestionForm, self).__init__(*args, **kwargs) - self.fields['tags'].initial = question.tagnames - -class RevisionForm(forms.Form): - """ - Lists revisions of a Question or Answer - """ - revision = forms.ChoiceField(widget=forms.Select(attrs={'style' : 'width:520px'})) - - def __init__(self, post, latest_revision, *args, **kwargs): - super(RevisionForm, self).__init__(*args, **kwargs) - revisions = post.revisions.all().values_list( - 'revision', 'author__username', 'revised_at', 'summary') - date_format = '%c' - self.fields['revision'].choices = [ - (r[0], u'%s - %s (%s) %s' % (r[0], r[1], r[2].strftime(date_format), r[3])) - for r in revisions] - self.fields['revision'].initial = latest_revision.revision - -class EditQuestionForm(forms.Form): - title = TitleField() - text = EditorField() - tags = TagNamesField() - summary = SummaryField() - - def __init__(self, question, revision, *args, **kwargs): - super(EditQuestionForm, self).__init__(*args, **kwargs) - self.fields['title'].initial = revision.title - self.fields['text'].initial = revision.text - self.fields['tags'].initial = revision.tagnames - # Once wiki mode is enabled, it can't be disabled - if not question.wiki: - self.fields['wiki'] = WikiField() - -class EditAnswerForm(forms.Form): - text = EditorField() - summary = SummaryField() - - def __init__(self, answer, revision, *args, **kwargs): - super(EditAnswerForm, self).__init__(*args, **kwargs) - self.fields['text'].initial = revision.text - -class EditUserForm(forms.Form): - email = forms.EmailField(label=u'Email', help_text=_('this email does not have to be linked to gravatar'), required=True, max_length=255, widget=forms.TextInput(attrs={'size' : 35})) - #username = UserNameField(label=_('Screen name')) - realname = forms.CharField(label=_('Real name'), required=False, max_length=255, widget=forms.TextInput(attrs={'size' : 35})) - website = forms.URLField(label=_('Website'), required=False, max_length=255, widget=forms.TextInput(attrs={'size' : 35})) - city = forms.CharField(label=_('Location'), required=False, max_length=255, widget=forms.TextInput(attrs={'size' : 35})) - birthday = forms.DateField(label=_('Date of birth'), help_text=_('will not be shown, used to calculate age, format: YYYY-MM-DD'), required=False, widget=forms.TextInput(attrs={'size' : 35})) - about = forms.CharField(label=_('Profile'), required=False, widget=forms.Textarea(attrs={'cols' : 60})) - - def __init__(self, user, *args, **kwargs): - super(EditUserForm, self).__init__(*args, **kwargs) - #self.fields['username'].initial = user.username - #self.fields['username'].user_instance = user - self.fields['email'].initial = user.email - self.fields['realname'].initial = user.real_name - self.fields['website'].initial = user.website - self.fields['city'].initial = user.location - - if user.date_of_birth is not None: - self.fields['birthday'].initial = user.date_of_birth - else: - self.fields['birthday'].initial = '1990-01-01' - self.fields['about'].initial = user.about - self.user = user - - def clean_email(self): - """For security reason one unique email in database""" - if self.user.email != self.cleaned_data['email']: - #todo dry it, there is a similar thing in openidauth - if settings.EMAIL_UNIQUE == True: - if 'email' in self.cleaned_data: - try: - user = User.objects.get(email = self.cleaned_data['email']) - except User.DoesNotExist: - return self.cleaned_data['email'] - except User.MultipleObjectsReturned: - raise forms.ValidationError(_('this email has already been registered, please use another one')) - raise forms.ValidationError(_('this email has already been registered, please use another one')) - return self.cleaned_data['email'] - -class TagFilterSelectionForm(forms.ModelForm): - tag_filter_setting = forms.ChoiceField(choices=TAG_EMAIL_FILTER_CHOICES, #imported from forum/const.py - initial='ignored', - label=_('Choose email tag filter'), - widget=forms.RadioSelect) - class Meta: - model = User - fields = ('tag_filter_setting',) - - def save(self): - before = self.instance.tag_filter_setting - super(TagFilterSelectionForm, self).save() - after = self.instance.tag_filter_setting #User.objects.get(pk=self.instance.id).tag_filter_setting - if before != after: - return True - return False - -class EditUserEmailFeedsForm(forms.Form): - WN = (('w',_('weekly')),('n',_('no email'))) - DWN = (('d',_('daily')),('w',_('weekly')),('n',_('no email'))) - FORM_TO_MODEL_MAP = { - 'all_questions':'q_all', - 'asked_by_me':'q_ask', - 'answered_by_me':'q_ans', - 'individually_selected':'q_sel', - } - NO_EMAIL_INITIAL = { - 'all_questions':'n', - 'asked_by_me':'n', - 'answered_by_me':'n', - 'individually_selected':'n', - } - asked_by_me = forms.ChoiceField(choices=DWN,initial='w', - widget=forms.RadioSelect, - label=_('Asked by me')) - answered_by_me = forms.ChoiceField(choices=DWN,initial='w', - widget=forms.RadioSelect, - label=_('Answered by me')) - individually_selected = forms.ChoiceField(choices=DWN,initial='w', - widget=forms.RadioSelect, - label=_('Individually selected')) - all_questions = forms.ChoiceField(choices=DWN,initial='w', - widget=forms.RadioSelect, - label=_('Entire forum (tag filtered)'),) - - def set_initial_values(self,user=None): - KEY_MAP = dict([(v,k) for k,v in self.FORM_TO_MODEL_MAP.iteritems()]) - if user != None: - settings = EmailFeedSetting.objects.filter(subscriber=user) - initial_values = {} - for setting in settings: - feed_type = setting.feed_type - form_field = KEY_MAP[feed_type] - frequency = setting.frequency - initial_values[form_field] = frequency - self.initial = initial_values - return self - - def reset(self): - self.cleaned_data['all_questions'] = 'n' - self.cleaned_data['asked_by_me'] = 'n' - self.cleaned_data['answered_by_me'] = 'n' - self.cleaned_data['individually_selected'] = 'n' - self.initial = self.NO_EMAIL_INITIAL - return self - - def save(self,user): - changed = False - for form_field, feed_type in self.FORM_TO_MODEL_MAP.items(): - s, created = EmailFeedSetting.objects.get_or_create(subscriber=user,\ - feed_type=feed_type) - new_value = self.cleaned_data[form_field] - if s.frequency != new_value: - s.frequency = self.cleaned_data[form_field] - s.save() - changed = True - else: - if created: - s.save() - if form_field == 'individually_selected': - feed_type = ContentType.objects.get_for_model(Question) - user.followed_questions.clear() - return changed +import re +from datetime import date +from django import forms +from models import * +from const import * +from django.utils.translation import ugettext as _ +from django_authopenid.forms import NextUrlField, UserNameField +import settings + +class TitleField(forms.CharField): + def __init__(self, *args, **kwargs): + super(TitleField, self).__init__(*args, **kwargs) + self.required = True + self.widget = forms.TextInput(attrs={'size' : 70, 'autocomplete' : 'off'}) + self.max_length = 255 + self.label = _('title') + self.help_text = _('please enter a descriptive title for your question') + self.initial = '' + + def clean(self, value): + if len(value) < 10: + raise forms.ValidationError(_('title must be > 10 characters')) + + return value + +class EditorField(forms.CharField): + def __init__(self, *args, **kwargs): + super(EditorField, self).__init__(*args, **kwargs) + self.required = True + self.widget = forms.Textarea(attrs={'id':'editor'}) + self.label = _('content') + self.help_text = u'' + self.initial = '' + + def clean(self, value): + if len(value) < 10: + raise forms.ValidationError(_('question content must be > 10 characters')) + + return value + +class TagNamesField(forms.CharField): + def __init__(self, *args, **kwargs): + super(TagNamesField, self).__init__(*args, **kwargs) + self.required = True + self.widget = forms.TextInput(attrs={'size' : 50, 'autocomplete' : 'off'}) + self.max_length = 255 + self.label = _('tags') + #self.help_text = _('please use space to separate tags (this enables autocomplete feature)') + self.help_text = _('Tags are short keywords, with no spaces within. Up to five tags can be used.') + self.initial = '' + + def clean(self, value): + value = super(TagNamesField, self).clean(value) + data = value.strip() + if len(data) < 1: + raise forms.ValidationError(_('tags are required')) + + split_re = re.compile(r'[ ,]+') + list = split_re.split(data) + list_temp = [] + if len(list) > 5: + raise forms.ValidationError(_('please use 5 tags or less')) + for tag in list: + if len(tag) > 20: + raise forms.ValidationError(_('tags must be shorter than 20 characters')) + #take tag regex from settings + tagname_re = re.compile(r'[a-z0-9]+') + if not tagname_re.match(tag): + raise forms.ValidationError(_('please use following characters in tags: letters \'a-z\', numbers, and characters \'.-_#\'')) + # only keep one same tag + if tag not in list_temp and len(tag.strip()) > 0: + list_temp.append(tag) + return u' '.join(list_temp) + +class WikiField(forms.BooleanField): + def __init__(self, *args, **kwargs): + super(WikiField, self).__init__(*args, **kwargs) + self.required = False + self.label = _('community wiki') + self.help_text = _('if you choose community wiki option, the question and answer do not generate points and name of author will not be shown') + def clean(self,value): + return value and settings.WIKI_ON + +class EmailNotifyField(forms.BooleanField): + def __init__(self, *args, **kwargs): + super(EmailNotifyField, self).__init__(*args, **kwargs) + self.required = False + self.widget.attrs['class'] = 'nomargin' + +class SummaryField(forms.CharField): + def __init__(self, *args, **kwargs): + super(SummaryField, self).__init__(*args, **kwargs) + self.required = False + self.widget = forms.TextInput(attrs={'size' : 50, 'autocomplete' : 'off'}) + self.max_length = 300 + self.label = _('update summary:') + self.help_text = _('enter a brief summary of your revision (e.g. fixed spelling, grammar, improved style, this field is optional)') + +class ModerateUserForm(forms.ModelForm): + is_approved = forms.BooleanField(label=_("Automatically accept user's contributions for the email updates"), + required=False) + + def clean_is_approved(self): + if 'is_approved' not in self.cleaned_data: + self.cleaned_data['is_approved'] = False + return self.cleaned_data['is_approved'] + + class Meta: + model = User + fields = ('is_approved',) + +class FeedbackForm(forms.Form): + name = forms.CharField(label=_('Your name:'), required=False) + email = forms.EmailField(label=_('Email (not shared with anyone):'), required=False) + message = forms.CharField(label=_('Your message:'), max_length=800,widget=forms.Textarea(attrs={'cols':60})) + next = NextUrlField() + +class AskForm(forms.Form): + title = TitleField() + text = EditorField() + tags = TagNamesField() + wiki = WikiField() + + openid = forms.CharField(required=False, max_length=255, widget=forms.TextInput(attrs={'size' : 40, 'class':'openid-input'})) + user = forms.CharField(required=False, max_length=255, widget=forms.TextInput(attrs={'size' : 35})) + email = forms.CharField(required=False, max_length=255, widget=forms.TextInput(attrs={'size' : 35})) + +class AnswerForm(forms.Form): + text = EditorField() + wiki = WikiField() + openid = forms.CharField(required=False, max_length=255, widget=forms.TextInput(attrs={'size' : 40, 'class':'openid-input'})) + user = forms.CharField(required=False, max_length=255, widget=forms.TextInput(attrs={'size' : 35})) + email = forms.CharField(required=False, max_length=255, widget=forms.TextInput(attrs={'size' : 35})) + email_notify = EmailNotifyField() + def __init__(self, question, user, *args, **kwargs): + super(AnswerForm, self).__init__(*args, **kwargs) + self.fields['email_notify'].widget.attrs['id'] = 'question-subscribe-updates'; + if question.wiki and settings.WIKI_ON: + self.fields['wiki'].initial = True + if user.is_authenticated(): + if user in question.followed_by.all(): + self.fields['email_notify'].initial = True + return + self.fields['email_notify'].initial = False + + +class CloseForm(forms.Form): + reason = forms.ChoiceField(choices=CLOSE_REASONS) + +class RetagQuestionForm(forms.Form): + tags = TagNamesField() + # initialize the default values + def __init__(self, question, *args, **kwargs): + super(RetagQuestionForm, self).__init__(*args, **kwargs) + self.fields['tags'].initial = question.tagnames + +class RevisionForm(forms.Form): + """ + Lists revisions of a Question or Answer + """ + revision = forms.ChoiceField(widget=forms.Select(attrs={'style' : 'width:520px'})) + + def __init__(self, post, latest_revision, *args, **kwargs): + super(RevisionForm, self).__init__(*args, **kwargs) + revisions = post.revisions.all().values_list( + 'revision', 'author__username', 'revised_at', 'summary') + date_format = '%c' + self.fields['revision'].choices = [ + (r[0], u'%s - %s (%s) %s' % (r[0], r[1], r[2].strftime(date_format), r[3])) + for r in revisions] + self.fields['revision'].initial = latest_revision.revision + +class EditQuestionForm(forms.Form): + title = TitleField() + text = EditorField() + tags = TagNamesField() + summary = SummaryField() + + def __init__(self, question, revision, *args, **kwargs): + super(EditQuestionForm, self).__init__(*args, **kwargs) + self.fields['title'].initial = revision.title + self.fields['text'].initial = revision.text + self.fields['tags'].initial = revision.tagnames + # Once wiki mode is enabled, it can't be disabled + if not question.wiki: + self.fields['wiki'] = WikiField() + +class EditAnswerForm(forms.Form): + text = EditorField() + summary = SummaryField() + + def __init__(self, answer, revision, *args, **kwargs): + super(EditAnswerForm, self).__init__(*args, **kwargs) + self.fields['text'].initial = revision.text + +class EditUserForm(forms.Form): + email = forms.EmailField(label=u'Email', help_text=_('this email does not have to be linked to gravatar'), required=True, max_length=255, widget=forms.TextInput(attrs={'size' : 35})) + #username = UserNameField(label=_('Screen name')) + realname = forms.CharField(label=_('Real name'), required=False, max_length=255, widget=forms.TextInput(attrs={'size' : 35})) + website = forms.URLField(label=_('Website'), required=False, max_length=255, widget=forms.TextInput(attrs={'size' : 35})) + city = forms.CharField(label=_('Location'), required=False, max_length=255, widget=forms.TextInput(attrs={'size' : 35})) + birthday = forms.DateField(label=_('Date of birth'), help_text=_('will not be shown, used to calculate age, format: YYYY-MM-DD'), required=False, widget=forms.TextInput(attrs={'size' : 35})) + about = forms.CharField(label=_('Profile'), required=False, widget=forms.Textarea(attrs={'cols' : 60})) + + def __init__(self, user, *args, **kwargs): + super(EditUserForm, self).__init__(*args, **kwargs) + #self.fields['username'].initial = user.username + #self.fields['username'].user_instance = user + self.fields['email'].initial = user.email + self.fields['realname'].initial = user.real_name + self.fields['website'].initial = user.website + self.fields['city'].initial = user.location + + if user.date_of_birth is not None: + self.fields['birthday'].initial = user.date_of_birth + else: + self.fields['birthday'].initial = '1990-01-01' + self.fields['about'].initial = user.about + self.user = user + + def clean_email(self): + """For security reason one unique email in database""" + if self.user.email != self.cleaned_data['email']: + #todo dry it, there is a similar thing in openidauth + if settings.EMAIL_UNIQUE == True: + if 'email' in self.cleaned_data: + try: + user = User.objects.get(email = self.cleaned_data['email']) + except User.DoesNotExist: + return self.cleaned_data['email'] + except User.MultipleObjectsReturned: + raise forms.ValidationError(_('this email has already been registered, please use another one')) + raise forms.ValidationError(_('this email has already been registered, please use another one')) + return self.cleaned_data['email'] + +class TagFilterSelectionForm(forms.ModelForm): + tag_filter_setting = forms.ChoiceField(choices=TAG_EMAIL_FILTER_CHOICES, #imported from forum/const.py + initial='ignored', + label=_('Choose email tag filter'), + widget=forms.RadioSelect) + class Meta: + model = User + fields = ('tag_filter_setting',) + + def save(self): + before = self.instance.tag_filter_setting + super(TagFilterSelectionForm, self).save() + after = self.instance.tag_filter_setting #User.objects.get(pk=self.instance.id).tag_filter_setting + if before != after: + return True + return False + +class EditUserEmailFeedsForm(forms.Form): + WN = (('w',_('weekly')),('n',_('no email'))) + DWN = (('d',_('daily')),('w',_('weekly')),('n',_('no email'))) + FORM_TO_MODEL_MAP = { + 'all_questions':'q_all', + 'asked_by_me':'q_ask', + 'answered_by_me':'q_ans', + 'individually_selected':'q_sel', + } + NO_EMAIL_INITIAL = { + 'all_questions':'n', + 'asked_by_me':'n', + 'answered_by_me':'n', + 'individually_selected':'n', + } + asked_by_me = forms.ChoiceField(choices=DWN,initial='w', + widget=forms.RadioSelect, + label=_('Asked by me')) + answered_by_me = forms.ChoiceField(choices=DWN,initial='w', + widget=forms.RadioSelect, + label=_('Answered by me')) + individually_selected = forms.ChoiceField(choices=DWN,initial='w', + widget=forms.RadioSelect, + label=_('Individually selected')) + all_questions = forms.ChoiceField(choices=DWN,initial='w', + widget=forms.RadioSelect, + label=_('Entire forum (tag filtered)'),) + + def set_initial_values(self,user=None): + KEY_MAP = dict([(v,k) for k,v in self.FORM_TO_MODEL_MAP.iteritems()]) + if user != None: + settings = EmailFeedSetting.objects.filter(subscriber=user) + initial_values = {} + for setting in settings: + feed_type = setting.feed_type + form_field = KEY_MAP[feed_type] + frequency = setting.frequency + initial_values[form_field] = frequency + self.initial = initial_values + return self + + def reset(self): + self.cleaned_data['all_questions'] = 'n' + self.cleaned_data['asked_by_me'] = 'n' + self.cleaned_data['answered_by_me'] = 'n' + self.cleaned_data['individually_selected'] = 'n' + self.initial = self.NO_EMAIL_INITIAL + return self + + def save(self,user): + changed = False + for form_field, feed_type in self.FORM_TO_MODEL_MAP.items(): + s, created = EmailFeedSetting.objects.get_or_create(subscriber=user,\ + feed_type=feed_type) + new_value = self.cleaned_data[form_field] + if s.frequency != new_value: + s.frequency = self.cleaned_data[form_field] + s.save() + changed = True + else: + if created: + s.save() + if form_field == 'individually_selected': + feed_type = ContentType.objects.get_for_model(Question) + user.followed_questions.clear() + return changed diff --git a/forum/managers.py b/forum/managers.py index ba174998..b352ad37 100644 --- a/forum/managers.py +++ b/forum/managers.py @@ -1,241 +1,241 @@ -import datetime -import time -import logging -from django.contrib.auth.models import User, UserManager -from django.db import connection, models, transaction -from django.db.models import Q -from forum.models import * -from urllib import quote, unquote - -class QuestionManager(models.Manager): - - 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. - """ - from forum.models import Tag - 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 forum.models 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 - """ - from forum.models import FavoriteQuestion - 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 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=0' - ') ' - 'WHERE id IN (%s)') - - def get_valid_tags(self, page_size): - from forum.models import Tag - tags = Tag.objects.all().filter(deleted=False).exclude(used_count=0).order_by("-id")[:page_size] - 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 AnswerManager(models.Manager): - 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(Q(question=question), - Q(deleted=False) | Q(deleted_by=user)) - - 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 VoteManager(models.Manager): - COUNT_UP_VOTE_BY_USER = "SELECT count(*) FROM vote WHERE user_id = %s AND vote = 1" - COUNT_DOWN_VOTE_BY_USER = "SELECT count(*) FROM vote WHERE user_id = %s AND vote = -1" - COUNT_VOTES_PER_DAY_BY_USER = "SELECT COUNT(*) FROM vote WHERE user_id = %s AND DATE(voted_at) = %s" - def get_up_vote_count_from_user(self, user): - if user is not None: - cursor = connection.cursor() - cursor.execute(self.COUNT_UP_VOTE_BY_USER, [user.id]) - row = cursor.fetchone() - return row[0] - else: - return 0 - - def get_down_vote_count_from_user(self, user): - if user is not None: - cursor = connection.cursor() - cursor.execute(self.COUNT_DOWN_VOTE_BY_USER, [user.id]) - row = cursor.fetchone() - return row[0] - else: - return 0 - - def get_votes_count_today_from_user(self, user): - if user is not None: - cursor = connection.cursor() - cursor.execute(self.COUNT_VOTES_PER_DAY_BY_USER, [user.id, time.strftime("%Y-%m-%d", datetime.datetime.now().timetuple())]) - row = cursor.fetchone() - return row[0] - - else: - return 0 - -class FlaggedItemManager(models.Manager): - COUNT_FLAGS_PER_DAY_BY_USER = "SELECT COUNT(*) FROM flagged_item WHERE user_id = %s AND DATE(flagged_at) = %s" - def get_flagged_items_count_today(self, user): - if user is not None: - cursor = connection.cursor() - cursor.execute(self.COUNT_FLAGS_PER_DAY_BY_USER, [user.id, time.strftime("%Y-%m-%d", datetime.datetime.now().timetuple())]) - row = cursor.fetchone() - return row[0] - - else: - return 0 - -class ReputeManager(models.Manager): - COUNT_REPUTATION_PER_DAY_BY_USER = "SELECT SUM(positive)+SUM(negative) FROM repute WHERE user_id = %s AND (reputation_type=1 OR reputation_type=-8) AND DATE(reputed_at) = %s" - 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: - cursor = connection.cursor() - cursor.execute(self.COUNT_REPUTATION_PER_DAY_BY_USER, [user.id, time.strftime("%Y-%m-%d", datetime.datetime.now().timetuple())]) - row = cursor.fetchone() - return row[0] - - else: - return 0 -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 +import datetime +import time +import logging +from django.contrib.auth.models import User, UserManager +from django.db import connection, models, transaction +from django.db.models import Q +from forum.models import * +from urllib import quote, unquote + +class QuestionManager(models.Manager): + + 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. + """ + from forum.models import Tag + 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 forum.models 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 + """ + from forum.models import FavoriteQuestion + 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 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=0' + ') ' + 'WHERE id IN (%s)') + + def get_valid_tags(self, page_size): + from forum.models import Tag + tags = Tag.objects.all().filter(deleted=False).exclude(used_count=0).order_by("-id")[:page_size] + 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 AnswerManager(models.Manager): + 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(Q(question=question), + Q(deleted=False) | Q(deleted_by=user)) + + 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 VoteManager(models.Manager): + COUNT_UP_VOTE_BY_USER = "SELECT count(*) FROM vote WHERE user_id = %s AND vote = 1" + COUNT_DOWN_VOTE_BY_USER = "SELECT count(*) FROM vote WHERE user_id = %s AND vote = -1" + COUNT_VOTES_PER_DAY_BY_USER = "SELECT COUNT(*) FROM vote WHERE user_id = %s AND DATE(voted_at) = %s" + def get_up_vote_count_from_user(self, user): + if user is not None: + cursor = connection.cursor() + cursor.execute(self.COUNT_UP_VOTE_BY_USER, [user.id]) + row = cursor.fetchone() + return row[0] + else: + return 0 + + def get_down_vote_count_from_user(self, user): + if user is not None: + cursor = connection.cursor() + cursor.execute(self.COUNT_DOWN_VOTE_BY_USER, [user.id]) + row = cursor.fetchone() + return row[0] + else: + return 0 + + def get_votes_count_today_from_user(self, user): + if user is not None: + cursor = connection.cursor() + cursor.execute(self.COUNT_VOTES_PER_DAY_BY_USER, [user.id, time.strftime("%Y-%m-%d", datetime.datetime.now().timetuple())]) + row = cursor.fetchone() + return row[0] + + else: + return 0 + +class FlaggedItemManager(models.Manager): + COUNT_FLAGS_PER_DAY_BY_USER = "SELECT COUNT(*) FROM flagged_item WHERE user_id = %s AND DATE(flagged_at) = %s" + def get_flagged_items_count_today(self, user): + if user is not None: + cursor = connection.cursor() + cursor.execute(self.COUNT_FLAGS_PER_DAY_BY_USER, [user.id, time.strftime("%Y-%m-%d", datetime.datetime.now().timetuple())]) + row = cursor.fetchone() + return row[0] + + else: + return 0 + +class ReputeManager(models.Manager): + COUNT_REPUTATION_PER_DAY_BY_USER = "SELECT SUM(positive)+SUM(negative) FROM repute WHERE user_id = %s AND (reputation_type=1 OR reputation_type=-8) AND DATE(reputed_at) = %s" + 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: + cursor = connection.cursor() + cursor.execute(self.COUNT_REPUTATION_PER_DAY_BY_USER, [user.id, time.strftime("%Y-%m-%d", datetime.datetime.now().timetuple())]) + row = cursor.fetchone() + return row[0] + + else: + return 0 +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 diff --git a/forum/templatetags/extra_tags.py b/forum/templatetags/extra_tags.py index 938d853f..4f79e497 100644 --- a/forum/templatetags/extra_tags.py +++ b/forum/templatetags/extra_tags.py @@ -1,351 +1,351 @@ -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 - -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 href(url): - url = '///' + settings.FORUM_SCRIPT_ALIAS + '/' + 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 BlockResourceNode(template.Node): - def __init__(self,nodelist): - self.items = nodelist - def render(self,context): - out = '///' + settings.FORUM_SCRIPT_ALIAS - if self.items: - out += '/' - for item in self.items: - bit = item.render(context) - out += bit - out = os.path.normpath(out) + '?v=%d' % settings.RESOURCE_REVISION - return out.replace(' ','') - -@register.tag(name='blockresource') -def blockresource(parser,token): - try: - tagname = token.split_contents() - except ValueError: - raise template.TemplateSyntaxError("blockresource tag does not use arguments") - nodelist = [] - while True: - nodelist.append(parser.parse(('endblockresource'))) - next = parser.next_token() - if next.contents == 'endblockresource': - break - return BlockResourceNode(nodelist) +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 + +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 href(url): + url = '///' + settings.FORUM_SCRIPT_ALIAS + '/' + 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 BlockResourceNode(template.Node): + def __init__(self,nodelist): + self.items = nodelist + def render(self,context): + out = '///' + settings.FORUM_SCRIPT_ALIAS + if self.items: + out += '/' + for item in self.items: + bit = item.render(context) + out += bit + out = os.path.normpath(out) + '?v=%d' % settings.RESOURCE_REVISION + return out.replace(' ','') + +@register.tag(name='blockresource') +def blockresource(parser,token): + try: + tagname = token.split_contents() + except ValueError: + raise template.TemplateSyntaxError("blockresource tag does not use arguments") + nodelist = [] + while True: + nodelist.append(parser.parse(('endblockresource'))) + next = parser.next_token() + if next.contents == 'endblockresource': + break + return BlockResourceNode(nodelist) diff --git a/forum/templatetags/smart_if.py b/forum/templatetags/smart_if.py index a8fc1944..ca3b43fe 100644 --- a/forum/templatetags/smart_if.py +++ b/forum/templatetags/smart_if.py @@ -1,401 +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() +""" +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/views.py b/forum/views.py index 04d9d497..c4514912 100644 --- a/forum/views.py +++ b/forum/views.py @@ -1,2387 +1,2387 @@ -# encoding:utf-8 -import os.path -import time, datetime, calendar, random -import logging -from urllib import quote, unquote -from django.conf import settings -from django.core.files.storage import default_storage -from django.shortcuts import render_to_response, get_object_or_404 -from django.contrib.auth.decorators import login_required -from django.http import HttpResponseRedirect, HttpResponse, HttpResponseForbidden, Http404 -from django.core.paginator import Paginator, EmptyPage, InvalidPage -from django.template import RequestContext, loader -from django.utils.html import * -from django.utils import simplejson -from django.core import serializers -from django.core.mail import mail_admins -from django.db import transaction -from django.db.models import Count, Q -from django.contrib.contenttypes.models import ContentType -from django.utils.translation import ugettext as _ -from django.utils.datastructures import SortedDict -from django.template.defaultfilters import slugify -from django.core.exceptions import PermissionDenied - -from utils.html import sanitize_html -from utils.decorators import ajax_method, ajax_login_required -from markdown2 import Markdown -#from lxml.html.diff import htmldiff -from forum.diff import textDiff as htmldiff -from forum.forms import * -from forum.models import * -from forum.auth import * -from forum.const import * -from forum.user import * -from forum import auth -from django_authopenid.util import get_next_url - -# used in index page -INDEX_PAGE_SIZE = 20 -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 users -USERS_PAGE_SIZE = 35 -# used in answers -ANSWERS_PAGE_SIZE = 10 -markdowner = Markdown(html4tags=True) -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 -def _get_tags_cache_json(): - tags = Tag.objects.filter(deleted=False).all() - tags_list = [] - for tag in tags: - dic = {'n': tag.name, 'c': tag.used_count} - tags_list.append(dic) - tags = simplejson.dumps(tags_list) - return tags - -def _get_and_remember_questions_sort_method(request, view_dic, default): - if default not in view_dic: - raise Exception('default value must be in view_dic') - - q_sort_method = request.REQUEST.get('sort', None) - if q_sort_method == None: - q_sort_method = request.session.get('questions_sort_method', default) - - if q_sort_method not in view_dic: - q_sort_method = default - request.session['questions_sort_method'] = q_sort_method - return q_sort_method, view_dic[q_sort_method] - -def index(request): - view_dic = { - "latest":"-last_activity_at", - "hottest":"-answer_count", - "mostvoted":"-score", - } - view_id, orderby = _get_and_remember_questions_sort_method(request, view_dic, 'latest') - - page_size = request.session.get('pagesize', QUESTIONS_PAGE_SIZE) - questions = Question.objects.exclude(deleted=True).order_by(orderby)[:page_size] - # RISK - inner join queries - questions = questions.select_related() - tags = Tag.objects.get_valid_tags(INDEX_TAGS_SIZE) - - awards = Award.objects.get_recent_awards() - - (interesting_tag_names, ignored_tag_names) = (None, None) - if request.user.is_authenticated(): - pt = MarkedTag.objects.filter(user=request.user) - interesting_tag_names = pt.filter(reason='good').values_list('tag__name', flat=True) - ignored_tag_names = pt.filter(reason='bad').values_list('tag__name', flat=True) - - tags_autocomplete = _get_tags_cache_json() - - return render_to_response('index.html', { - 'interesting_tag_names': interesting_tag_names, - 'tags_autocomplete': tags_autocomplete, - 'ignored_tag_names': ignored_tag_names, - "questions" : questions, - "tab_id" : view_id, - "tags" : tags, - "awards" : awards[:INDEX_AWARD_SIZE], - }, context_instance=RequestContext(request)) - -def about(request): - return render_to_response('about.html', context_instance=RequestContext(request)) - -def faq(request): - 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 unanswered(request): - return questions(request, unanswered=True) - -def questions(request, tagname=None, unanswered=False): - """ - 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",10) - 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 create_new_answer( question=None, author=None,\ - added_at=None, wiki=False,\ - text='', email_notify=False): - - html = sanitize_html(markdowner.convert(text)) - - #create answer - answer = Answer( - question = question, - author = author, - added_at = added_at, - wiki = wiki, - html = html - ) - 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) - - #update revision - 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 - -def create_new_question(title=None,author=None,added_at=None, - wiki=False,tagnames=None,summary=None, - text=None): - """this is not a view - and maybe should become one of the methods on Question object? - """ - html = sanitize_html(markdowner.convert(text)) - question = Question( - title = title, - author = author, - added_at = added_at, - last_activity_at = added_at, - last_activity_by = author, - wiki = wiki, - tagnames = tagnames, - html = html, - 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 - -#TODO: allow anynomus user to ask question by providing email and username. -#@login_required -def ask(request): - 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 = create_new_question( - title = title, - author = author, - added_at = added_at, - wiki = wiki, - tagnames = tagnames, - summary = summary, - text = 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('user_signin_new_question')) - 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)) - -def question(request, id): - 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) - if question.deleted and not 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)) - -@login_required -def close(request, id): - question = get_object_or_404(Question, id=id) - if not 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): - question = get_object_or_404(Question, id=id) - # open question - if not 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)) - -@login_required -def edit_question(request, id): - question = get_object_or_404(Question, id=id) - if question.deleted and not can_view_deleted_post(request.user, question): - raise Http404 - if can_edit_post(request.user, question): - return _edit_question(request, question) - elif can_retag_questions(request.user): - return _retag_question(request, question) - else: - raise Http404 - -def _retag_question(request, 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): - 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 can_view_deleted_post(request.user, answer): - raise Http404 - elif not 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)) - -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(['' % 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(['' % 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)) - -def answer(request, id): - 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(): - create_new_answer( - question=question, - author=request.user, - added_at=update_time, - wiki=wiki, - text=text, - email_notify=form.cleaned_data['email_notify'] - ) - else: - request.session.flush() - html = sanitize_html(markdowner.convert(text)) - summary = strip_tags(html)[:120] - anon = AnonymousAnswer( - question=question, - wiki=wiki, - text=text, - summary=summary, - session_key=request.session.session_key, - ip_addr=request.META['REMOTE_ADDR'], - ) - anon.save() - return HttpResponseRedirect(reverse('user_signin_new_answer')) - - return HttpResponseRedirect(question.get_absolute_url()) - -def tags(request): - 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 tag(request, tag): - return questions(request, tagname=tag) - -def vote(request, id): - """ - vote_type: - acceptAnswer : 0, - questionUpVote : 1, - questionDownVote : 2, - favorite : 4, - answerUpVote: 5, - answerDownVote:6, - offensiveQuestion : 7, - offensiveAnswer:8, - removeQuestion: 9, - removeAnswer:10 - questionSubscribeUpdates:11 - - 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): - if vote_score == 1: - return can_vote_up(request.user) - else: - return can_vote_down(request.user) - - try: - if not request.user.is_authenticated(): - response_data['allowed'] = 0 - response_data['success'] = 0 - - elif request.is_ajax(): - 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: - 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: - 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) - 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) >= VOTE_RULES['scope_deny_unvote_days']: - response_data['status'] = 2 - else: - voted = vote.vote - if voted > 0: - # cancel upvote - onUpVotedCanceled(vote, post, request.user) - - else: - # cancel downvote - 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) >= 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 - onUpVoted(vote, post, request.user) - else: - # downvote - onDownVoted(vote, post, request.user) - - votes_left = VOTE_RULES['scope_votes_per_user_per_day'] - Vote.objects.get_votes_count_today_from_user(request.user) - if votes_left <= 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) >= VOTE_RULES['scope_flags_per_user_per_day']: - response_data['allowed'] = -3 - elif not 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()) - 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 can_delete_post(request.user, post): - response_data['allowed'] = -2 - elif post.deleted == True: - logging.debug('debug restoring post in view') - onDeleteCanceled(post, request.user) - response_data['status'] = 1 - else: - 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") - -@ajax_login_required -def mark_tag(request, tag=None, **kwargs): - action = kwargs['action'] - ts = MarkedTag.objects.filter(user=request.user, tag__name=tag) - if action == 'remove': - logging.debug('deleting tag %s' % tag) - ts.delete() - else: - reason = kwargs['reason'] - if len(ts) == 0: - try: - t = Tag.objects.get(name=tag) - mt = MarkedTag(user=request.user, reason=reason, tag=t) - mt.save() - except: - pass - else: - ts.update(reason=reason) - return HttpResponse(simplejson.dumps(''), mimetype="application/json") - -@ajax_login_required -def ajax_toggle_ignored_questions(request): - if request.user.hide_ignored_questions: - new_hide_setting = False - else: - new_hide_setting = True - request.user.hide_ignored_questions = new_hide_setting - request.user.save() - -@ajax_method -def ajax_command(request): - if 'command' not in request.POST: - return HttpResponseForbidden(mimetype="application/json") - if request.POST['command'] == 'toggle-ignored-questions': - return ajax_toggle_ignored_questions(request) - -def users(request): - is_paginated = True - sortby = request.GET.get('sort', 'reputation') - 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)) - -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 import views - func = getattr(views, user_view.view_name) - return func(request, id, user_view) - -@login_required -def moderate_user(request, id): - """ajax handler of user moderation - """ - if not auth.can_moderate_users(request.user) or request.method != 'POST': - raise Http404 - if not request.is_ajax(): - return HttpResponseForbidden(mimetype="application/json") - - user = get_object_or_404(User, id=id) - form = ModerateUserForm(request.POST, instance=user) - - if form.is_valid(): - form.save() - logging.debug('data saved') - response = HttpResponse(simplejson.dumps(''), mimetype="application/json") - else: - response = HttpResponseForbidden(mimetype="application/json") - return response - -@login_required -def edit_user(request, id): - user = get_object_or_404(User, id=id) - 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']) - - from django_authopenid.views import set_new_email - set_new_email(user, new_email) - - #user.username = sanitize_html(form.cleaned_data['username']) - user.real_name = sanitize_html(form.cleaned_data['realname']) - user.website = sanitize_html(form.cleaned_data['website']) - user.location = sanitize_html(form.cleaned_data['city']) - 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 = 0 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=0 AND question.deleted=0 AND answer.author_id=%s AND answer.question_id=question.id'], - params=[user_id], - order_by=['-vote_count', '-answer_id'], - select_params=[user_id] - ).distinct().values('comment_count', - 'id', - 'answer_id', - 'title', - 'author_id', - 'accepted', - 'vote_count', - 'answer_count', - 'vote_up_count', - 'vote_down_count')[:100] - - up_votes = Vote.objects.get_up_vote_count_from_user(user) - down_votes = Vote.objects.get_down_vote_count_from_user(user) - votes_today = Vote.objects.get_votes_count_today_from_user(user) - votes_total = VOTE_RULES['scope_votes_per_user_per_day'] - - 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], - "tags" : tags, - "awards": awards, - "total_awards" : total_awards, - }, context_instance=RequestContext(request)) - -def user_recent(request, user_id, user_view): - user = get_object_or_404(User, id=user_id) - 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=0 AND activity.user_id = %s AND activity.activity_type = %s'], - params=[question_type_id, user_id, TYPE_ACTIVITY_ASK_QUESTION], - order_by=['-activity.active_at'] - ).values( - 'title', - 'question_id', - 'active_at', - 'activity_type' - ) - if len(questions) > 0: - questions = [(Event(q['active_at'], q['activity_type'], q['title'], '', '0', \ - q['question_id'])) for q in questions] - 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=0 AND activity.user_id=%s AND '+ - 'activity.activity_type=%s AND question.deleted=0'], - params=[answer_type_id, user_id, TYPE_ACTIVITY_ANSWER], - order_by=['-activity.active_at'] - ).values( - 'title', - 'question_id', - 'answer_id', - 'active_at', - 'activity_type' - ) - if len(answers) > 0: - answers = [(Event(q['active_at'], q['activity_type'], q['title'], '', q['answer_id'], \ - q['question_id'])) for q in answers] - 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=0'], - params=[comment_type_id, question_type_id, user_id, TYPE_ACTIVITY_COMMENT_QUESTION], - order_by=['-comment.added_at'] - ).values( - 'title', - 'question_id', - 'added_at', - 'activity_type' - ) - - if len(comments) > 0: - comments = [(Event(q['added_at'], q['activity_type'], q['title'], '', '0', \ - 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=0 AND question.deleted=0'], - params=[comment_type_id, answer_type_id, user_id, TYPE_ACTIVITY_COMMENT_ANSWER], - order_by=['-comment.added_at'] - ).values( - 'title', - 'question_id', - 'answer_id', - 'added_at', - 'activity_type' - ) - - if len(comments) > 0: - comments = [(Event(q['added_at'], q['activity_type'], q['title'], '', q['answer_id'], \ - 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=0 AND '+ - 'activity.user_id = question_revision.author_id AND activity.user_id = %s AND '+ - 'activity.activity_type=%s'], - params=[question_revision_type_id, user_id, TYPE_ACTIVITY_UPDATE_QUESTION], - order_by=['-activity.active_at'] - ).values( - 'title', - 'question_id', - 'added_at', - 'activity_type', - 'summary' - ) - - if len(revisions) > 0: - revisions = [(Event(q['added_at'], q['activity_type'], q['title'], q['summary'], '0', \ - 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=0 AND answer.deleted=0 AND '+ - 'activity.activity_type=%s'], - params=[answer_revision_type_id, user_id, TYPE_ACTIVITY_UPDATE_ANSWER], - order_by=['-activity.active_at'] - ).values( - 'title', - 'question_id', - 'added_at', - 'answer_id', - 'activity_type', - 'summary' - ) - - if len(revisions) > 0: - revisions = [(Event(q['added_at'], q['activity_type'], q['title'], q['summary'], \ - 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=0 AND question.deleted=0 AND '+ - 'answer.question_id=question.id AND activity.activity_type=%s'], - params=[answer_type_id, user_id, TYPE_ACTIVITY_MARK_ANSWER], - order_by=['-activity.active_at'] - ).values( - 'title', - 'question_id', - 'added_at', - 'activity_type', - ) - if len(accept_answers) > 0: - accept_answers = [(Event(q['added_at'], q['activity_type'], q['title'], '', '0', \ - q['question_id'])) for q in accept_answers] - 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=0 AND question.deleted = 0 AND '+ - 'question.author_id = %s AND answer.author_id <> %s AND answer.author_id=auth_user.id'], - params=[user_id, user_id], - order_by=['-answer.id'] - ).values( - 'title', - 'question_id', - 'answer_id', - 'added_at', - 'html', - 'username', - 'user_id' - ) - if len(answers) > 0: - answers = [(Response(TYPE_RESPONSE['QUESTION_ANSWERED'], a['title'], a['question_id'], - a['answer_id'], a['added_at'], a['username'], a['user_id'], a['html'])) for a in answers] - 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 = 0 AND question.author_id = %s AND comment.object_id=question.id AND '+ - 'comment.content_type_id=%s AND comment.user_id <> %s AND comment.user_id = auth_user.id'], - params=[user_id, question_type_id, user_id], - order_by=['-comment.added_at'] - ).values( - 'title', - 'question_id', - 'added_at', - 'comment', - 'username', - 'user_id' - ) - - if len(comments) > 0: - comments = [(Response(TYPE_RESPONSE['QUESTION_COMMENTED'], c['title'], c['question_id'], - '', 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 = 0 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=0 AND question.deleted = 0 AND '+ - 'answer.author_id = %s AND answer.accepted=1 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 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 = 0 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)) - -def question_comments(request, id): - question = get_object_or_404(Question, id=id) - user = request.user - return __comments(request, question, 'question') - -def answer_comments(request, id): - answer = get_object_or_404(Answer, id=id) - user = request.user - return __comments(request, answer, 'answer') - -def __comments(request, obj, type): - # only support get 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 __generate_comments_json(obj, type, user): - 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 delete_comment(request, object_id='', comment_id='', commented_object_type=None): - response = None - commented_object = None - if commented_object_type == 'question': - commented_object = Question - elif commented_object_type == 'answer': - commented_object = Answer - - 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() - -def logout(request): - return render_to_response('logout.html', { - 'next' : get_next_url(request), - }, context_instance=RequestContext(request)) - -def badges(request): - badges = Badge.objects.all().order_by('type') - my_badges = [] - if request.user.is_authenticated(): - my_badges = Award.objects.filter(user=request.user) - 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)) - -def read_message(request): - 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('') - -def upload(request): - 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 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") - -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)) - -def search(request): - """ - Search by question, user and tag keywords. - For questions now we only search keywords in question title. - """ - 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" - - if settings.USE_SPHINX_SEARCH == True: - #search index is now free of delete questions and answers - #so there is not "antideleted" filtering here - objects = Question.search.query(keywords) - #no related selection either because we're relying on full text search here - else: - objects = Question.objects.filter(deleted=False).extra(where=['title like %s'], params=['%' + keywords + '%']).order_by(orderby) - # RISK - inner join queries - objects = objects.select_related(); - - 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 +# encoding:utf-8 +import os.path +import time, datetime, calendar, random +import logging +from urllib import quote, unquote +from django.conf import settings +from django.core.files.storage import default_storage +from django.shortcuts import render_to_response, get_object_or_404 +from django.contrib.auth.decorators import login_required +from django.http import HttpResponseRedirect, HttpResponse, HttpResponseForbidden, Http404 +from django.core.paginator import Paginator, EmptyPage, InvalidPage +from django.template import RequestContext, loader +from django.utils.html import * +from django.utils import simplejson +from django.core import serializers +from django.core.mail import mail_admins +from django.db import transaction +from django.db.models import Count, Q +from django.contrib.contenttypes.models import ContentType +from django.utils.translation import ugettext as _ +from django.utils.datastructures import SortedDict +from django.template.defaultfilters import slugify +from django.core.exceptions import PermissionDenied + +from utils.html import sanitize_html +from utils.decorators import ajax_method, ajax_login_required +from markdown2 import Markdown +#from lxml.html.diff import htmldiff +from forum.diff import textDiff as htmldiff +from forum.forms import * +from forum.models import * +from forum.auth import * +from forum.const import * +from forum.user import * +from forum import auth +from django_authopenid.util import get_next_url + +# used in index page +INDEX_PAGE_SIZE = 20 +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 users +USERS_PAGE_SIZE = 35 +# used in answers +ANSWERS_PAGE_SIZE = 10 +markdowner = Markdown(html4tags=True) +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 +def _get_tags_cache_json(): + tags = Tag.objects.filter(deleted=False).all() + tags_list = [] + for tag in tags: + dic = {'n': tag.name, 'c': tag.used_count} + tags_list.append(dic) + tags = simplejson.dumps(tags_list) + return tags + +def _get_and_remember_questions_sort_method(request, view_dic, default): + if default not in view_dic: + raise Exception('default value must be in view_dic') + + q_sort_method = request.REQUEST.get('sort', None) + if q_sort_method == None: + q_sort_method = request.session.get('questions_sort_method', default) + + if q_sort_method not in view_dic: + q_sort_method = default + request.session['questions_sort_method'] = q_sort_method + return q_sort_method, view_dic[q_sort_method] + +def index(request): + view_dic = { + "latest":"-last_activity_at", + "hottest":"-answer_count", + "mostvoted":"-score", + } + view_id, orderby = _get_and_remember_questions_sort_method(request, view_dic, 'latest') + + page_size = request.session.get('pagesize', QUESTIONS_PAGE_SIZE) + questions = Question.objects.exclude(deleted=True).order_by(orderby)[:page_size] + # RISK - inner join queries + questions = questions.select_related() + tags = Tag.objects.get_valid_tags(INDEX_TAGS_SIZE) + + awards = Award.objects.get_recent_awards() + + (interesting_tag_names, ignored_tag_names) = (None, None) + if request.user.is_authenticated(): + pt = MarkedTag.objects.filter(user=request.user) + interesting_tag_names = pt.filter(reason='good').values_list('tag__name', flat=True) + ignored_tag_names = pt.filter(reason='bad').values_list('tag__name', flat=True) + + tags_autocomplete = _get_tags_cache_json() + + return render_to_response('index.html', { + 'interesting_tag_names': interesting_tag_names, + 'tags_autocomplete': tags_autocomplete, + 'ignored_tag_names': ignored_tag_names, + "questions" : questions, + "tab_id" : view_id, + "tags" : tags, + "awards" : awards[:INDEX_AWARD_SIZE], + }, context_instance=RequestContext(request)) + +def about(request): + return render_to_response('about.html', context_instance=RequestContext(request)) + +def faq(request): + 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 unanswered(request): + return questions(request, unanswered=True) + +def questions(request, tagname=None, unanswered=False): + """ + 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",10) + 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 create_new_answer( question=None, author=None,\ + added_at=None, wiki=False,\ + text='', email_notify=False): + + html = sanitize_html(markdowner.convert(text)) + + #create answer + answer = Answer( + question = question, + author = author, + added_at = added_at, + wiki = wiki, + html = html + ) + 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) + + #update revision + 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 + +def create_new_question(title=None,author=None,added_at=None, + wiki=False,tagnames=None,summary=None, + text=None): + """this is not a view + and maybe should become one of the methods on Question object? + """ + html = sanitize_html(markdowner.convert(text)) + question = Question( + title = title, + author = author, + added_at = added_at, + last_activity_at = added_at, + last_activity_by = author, + wiki = wiki, + tagnames = tagnames, + html = html, + 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 + +#TODO: allow anynomus user to ask question by providing email and username. +#@login_required +def ask(request): + 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 = create_new_question( + title = title, + author = author, + added_at = added_at, + wiki = wiki, + tagnames = tagnames, + summary = summary, + text = 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('user_signin_new_question')) + 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)) + +def question(request, id): + 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) + if question.deleted and not 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)) + +@login_required +def close(request, id): + question = get_object_or_404(Question, id=id) + if not 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): + question = get_object_or_404(Question, id=id) + # open question + if not 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)) + +@login_required +def edit_question(request, id): + question = get_object_or_404(Question, id=id) + if question.deleted and not can_view_deleted_post(request.user, question): + raise Http404 + if can_edit_post(request.user, question): + return _edit_question(request, question) + elif can_retag_questions(request.user): + return _retag_question(request, question) + else: + raise Http404 + +def _retag_question(request, 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): + 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 can_view_deleted_post(request.user, answer): + raise Http404 + elif not 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)) + +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(['' % 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(['' % 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)) + +def answer(request, id): + 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(): + create_new_answer( + question=question, + author=request.user, + added_at=update_time, + wiki=wiki, + text=text, + email_notify=form.cleaned_data['email_notify'] + ) + else: + request.session.flush() + html = sanitize_html(markdowner.convert(text)) + summary = strip_tags(html)[:120] + anon = AnonymousAnswer( + question=question, + wiki=wiki, + text=text, + summary=summary, + session_key=request.session.session_key, + ip_addr=request.META['REMOTE_ADDR'], + ) + anon.save() + return HttpResponseRedirect(reverse('user_signin_new_answer')) + + return HttpResponseRedirect(question.get_absolute_url()) + +def tags(request): + 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 tag(request, tag): + return questions(request, tagname=tag) + +def vote(request, id): + """ + vote_type: + acceptAnswer : 0, + questionUpVote : 1, + questionDownVote : 2, + favorite : 4, + answerUpVote: 5, + answerDownVote:6, + offensiveQuestion : 7, + offensiveAnswer:8, + removeQuestion: 9, + removeAnswer:10 + questionSubscribeUpdates:11 + + 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): + if vote_score == 1: + return can_vote_up(request.user) + else: + return can_vote_down(request.user) + + try: + if not request.user.is_authenticated(): + response_data['allowed'] = 0 + response_data['success'] = 0 + + elif request.is_ajax(): + 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: + 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: + 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) + 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) >= VOTE_RULES['scope_deny_unvote_days']: + response_data['status'] = 2 + else: + voted = vote.vote + if voted > 0: + # cancel upvote + onUpVotedCanceled(vote, post, request.user) + + else: + # cancel downvote + 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) >= 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 + onUpVoted(vote, post, request.user) + else: + # downvote + onDownVoted(vote, post, request.user) + + votes_left = VOTE_RULES['scope_votes_per_user_per_day'] - Vote.objects.get_votes_count_today_from_user(request.user) + if votes_left <= 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) >= VOTE_RULES['scope_flags_per_user_per_day']: + response_data['allowed'] = -3 + elif not 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()) + 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 can_delete_post(request.user, post): + response_data['allowed'] = -2 + elif post.deleted == True: + logging.debug('debug restoring post in view') + onDeleteCanceled(post, request.user) + response_data['status'] = 1 + else: + 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") + +@ajax_login_required +def mark_tag(request, tag=None, **kwargs): + action = kwargs['action'] + ts = MarkedTag.objects.filter(user=request.user, tag__name=tag) + if action == 'remove': + logging.debug('deleting tag %s' % tag) + ts.delete() + else: + reason = kwargs['reason'] + if len(ts) == 0: + try: + t = Tag.objects.get(name=tag) + mt = MarkedTag(user=request.user, reason=reason, tag=t) + mt.save() + except: + pass + else: + ts.update(reason=reason) + return HttpResponse(simplejson.dumps(''), mimetype="application/json") + +@ajax_login_required +def ajax_toggle_ignored_questions(request): + if request.user.hide_ignored_questions: + new_hide_setting = False + else: + new_hide_setting = True + request.user.hide_ignored_questions = new_hide_setting + request.user.save() + +@ajax_method +def ajax_command(request): + if 'command' not in request.POST: + return HttpResponseForbidden(mimetype="application/json") + if request.POST['command'] == 'toggle-ignored-questions': + return ajax_toggle_ignored_questions(request) + +def users(request): + is_paginated = True + sortby = request.GET.get('sort', 'reputation') + 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)) + +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 import views + func = getattr(views, user_view.view_name) + return func(request, id, user_view) + +@login_required +def moderate_user(request, id): + """ajax handler of user moderation + """ + if not auth.can_moderate_users(request.user) or request.method != 'POST': + raise Http404 + if not request.is_ajax(): + return HttpResponseForbidden(mimetype="application/json") + + user = get_object_or_404(User, id=id) + form = ModerateUserForm(request.POST, instance=user) + + if form.is_valid(): + form.save() + logging.debug('data saved') + response = HttpResponse(simplejson.dumps(''), mimetype="application/json") + else: + response = HttpResponseForbidden(mimetype="application/json") + return response + +@login_required +def edit_user(request, id): + user = get_object_or_404(User, id=id) + 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']) + + from django_authopenid.views import set_new_email + set_new_email(user, new_email) + + #user.username = sanitize_html(form.cleaned_data['username']) + user.real_name = sanitize_html(form.cleaned_data['realname']) + user.website = sanitize_html(form.cleaned_data['website']) + user.location = sanitize_html(form.cleaned_data['city']) + 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 = 0 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=0 AND question.deleted=0 AND answer.author_id=%s AND answer.question_id=question.id'], + params=[user_id], + order_by=['-vote_count', '-answer_id'], + select_params=[user_id] + ).distinct().values('comment_count', + 'id', + 'answer_id', + 'title', + 'author_id', + 'accepted', + 'vote_count', + 'answer_count', + 'vote_up_count', + 'vote_down_count')[:100] + + up_votes = Vote.objects.get_up_vote_count_from_user(user) + down_votes = Vote.objects.get_down_vote_count_from_user(user) + votes_today = Vote.objects.get_votes_count_today_from_user(user) + votes_total = VOTE_RULES['scope_votes_per_user_per_day'] + + 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], + "tags" : tags, + "awards": awards, + "total_awards" : total_awards, + }, context_instance=RequestContext(request)) + +def user_recent(request, user_id, user_view): + user = get_object_or_404(User, id=user_id) + 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=0 AND activity.user_id = %s AND activity.activity_type = %s'], + params=[question_type_id, user_id, TYPE_ACTIVITY_ASK_QUESTION], + order_by=['-activity.active_at'] + ).values( + 'title', + 'question_id', + 'active_at', + 'activity_type' + ) + if len(questions) > 0: + questions = [(Event(q['active_at'], q['activity_type'], q['title'], '', '0', \ + q['question_id'])) for q in questions] + 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=0 AND activity.user_id=%s AND '+ + 'activity.activity_type=%s AND question.deleted=0'], + params=[answer_type_id, user_id, TYPE_ACTIVITY_ANSWER], + order_by=['-activity.active_at'] + ).values( + 'title', + 'question_id', + 'answer_id', + 'active_at', + 'activity_type' + ) + if len(answers) > 0: + answers = [(Event(q['active_at'], q['activity_type'], q['title'], '', q['answer_id'], \ + q['question_id'])) for q in answers] + 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=0'], + params=[comment_type_id, question_type_id, user_id, TYPE_ACTIVITY_COMMENT_QUESTION], + order_by=['-comment.added_at'] + ).values( + 'title', + 'question_id', + 'added_at', + 'activity_type' + ) + + if len(comments) > 0: + comments = [(Event(q['added_at'], q['activity_type'], q['title'], '', '0', \ + 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=0 AND question.deleted=0'], + params=[comment_type_id, answer_type_id, user_id, TYPE_ACTIVITY_COMMENT_ANSWER], + order_by=['-comment.added_at'] + ).values( + 'title', + 'question_id', + 'answer_id', + 'added_at', + 'activity_type' + ) + + if len(comments) > 0: + comments = [(Event(q['added_at'], q['activity_type'], q['title'], '', q['answer_id'], \ + 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=0 AND '+ + 'activity.user_id = question_revision.author_id AND activity.user_id = %s AND '+ + 'activity.activity_type=%s'], + params=[question_revision_type_id, user_id, TYPE_ACTIVITY_UPDATE_QUESTION], + order_by=['-activity.active_at'] + ).values( + 'title', + 'question_id', + 'added_at', + 'activity_type', + 'summary' + ) + + if len(revisions) > 0: + revisions = [(Event(q['added_at'], q['activity_type'], q['title'], q['summary'], '0', \ + 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=0 AND answer.deleted=0 AND '+ + 'activity.activity_type=%s'], + params=[answer_revision_type_id, user_id, TYPE_ACTIVITY_UPDATE_ANSWER], + order_by=['-activity.active_at'] + ).values( + 'title', + 'question_id', + 'added_at', + 'answer_id', + 'activity_type', + 'summary' + ) + + if len(revisions) > 0: + revisions = [(Event(q['added_at'], q['activity_type'], q['title'], q['summary'], \ + 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=0 AND question.deleted=0 AND '+ + 'answer.question_id=question.id AND activity.activity_type=%s'], + params=[answer_type_id, user_id, TYPE_ACTIVITY_MARK_ANSWER], + order_by=['-activity.active_at'] + ).values( + 'title', + 'question_id', + 'added_at', + 'activity_type', + ) + if len(accept_answers) > 0: + accept_answers = [(Event(q['added_at'], q['activity_type'], q['title'], '', '0', \ + q['question_id'])) for q in accept_answers] + 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=0 AND question.deleted = 0 AND '+ + 'question.author_id = %s AND answer.author_id <> %s AND answer.author_id=auth_user.id'], + params=[user_id, user_id], + order_by=['-answer.id'] + ).values( + 'title', + 'question_id', + 'answer_id', + 'added_at', + 'html', + 'username', + 'user_id' + ) + if len(answers) > 0: + answers = [(Response(TYPE_RESPONSE['QUESTION_ANSWERED'], a['title'], a['question_id'], + a['answer_id'], a['added_at'], a['username'], a['user_id'], a['html'])) for a in answers] + 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 = 0 AND question.author_id = %s AND comment.object_id=question.id AND '+ + 'comment.content_type_id=%s AND comment.user_id <> %s AND comment.user_id = auth_user.id'], + params=[user_id, question_type_id, user_id], + order_by=['-comment.added_at'] + ).values( + 'title', + 'question_id', + 'added_at', + 'comment', + 'username', + 'user_id' + ) + + if len(comments) > 0: + comments = [(Response(TYPE_RESPONSE['QUESTION_COMMENTED'], c['title'], c['question_id'], + '', 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 = 0 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=0 AND question.deleted = 0 AND '+ + 'answer.author_id = %s AND answer.accepted=1 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 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 = 0 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)) + +def question_comments(request, id): + question = get_object_or_404(Question, id=id) + user = request.user + return __comments(request, question, 'question') + +def answer_comments(request, id): + answer = get_object_or_404(Answer, id=id) + user = request.user + return __comments(request, answer, 'answer') + +def __comments(request, obj, type): + # only support get 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 __generate_comments_json(obj, type, user): + 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 delete_comment(request, object_id='', comment_id='', commented_object_type=None): + response = None + commented_object = None + if commented_object_type == 'question': + commented_object = Question + elif commented_object_type == 'answer': + commented_object = Answer + + 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() + +def logout(request): + return render_to_response('logout.html', { + 'next' : get_next_url(request), + }, context_instance=RequestContext(request)) + +def badges(request): + badges = Badge.objects.all().order_by('type') + my_badges = [] + if request.user.is_authenticated(): + my_badges = Award.objects.filter(user=request.user) + 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)) + +def read_message(request): + 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('') + +def upload(request): + 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 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") + +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)) + +def search(request): + """ + Search by question, user and tag keywords. + For questions now we only search keywords in question title. + """ + 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" + + if settings.USE_SPHINX_SEARCH == True: + #search index is now free of delete questions and answers + #so there is not "antideleted" filtering here + objects = Question.search.query(keywords) + #no related selection either because we're relying on full text search here + else: + objects = Question.objects.filter(deleted=False).extra(where=['title like %s'], params=['%' + keywords + '%']).order_by(orderby) + # RISK - inner join queries + objects = objects.select_related(); + + 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 diff --git a/templates/content/js/com.cnprog.post.js b/templates/content/js/com.cnprog.post.js index 02ed7757..a073d20f 100644 --- a/templates/content/js/com.cnprog.post.js +++ b/templates/content/js/com.cnprog.post.js @@ -1,690 +1,690 @@ -/* -Scripts for cnprog.com -Project Name: Lanai -All Rights Resevred 2008. CNPROG.COM -*/ -var lanai = -{ - /** - * Finds any
tags which aren't registered for - * pretty printing, adds the appropriate class name and invokes prettify. - */ - highlightSyntax: function(){ - var styled = false; - $("pre code").parent().each(function(){ - if (!$(this).hasClass('prettyprint')){ - $(this).addClass('prettyprint'); - styled = true; - } - }); - - if (styled){ - prettyPrint(); - } - } -}; - -var Vote = function(){ - // All actions are related to a question - var questionId; - //question slug to build redirect urls - var questionSlug; - // The object we operate on actually. It can be a question or an answer. - var postId; - var questionAuthorId; - var currentUserId; - var answerContainerIdPrefix = 'answer-container-'; - var voteContainerId = 'vote-buttons'; - var imgIdPrefixAccept = 'answer-img-accept-'; - var imgClassPrefixFavorite = 'question-img-favorite'; - var imgIdPrefixQuestionVoteup = 'question-img-upvote-'; - var imgIdPrefixQuestionVotedown = 'question-img-downvote-'; - var imgIdPrefixAnswerVoteup = 'answer-img-upvote-'; - var imgIdPrefixAnswerVotedown = 'answer-img-downvote-'; - var divIdFavorite = 'favorite-number'; - var commentLinkIdPrefix = 'comment-'; - var voteNumberClass = "vote-number"; - var offensiveIdPrefixQuestionFlag = 'question-offensive-flag-'; - var offensiveIdPrefixAnswerFlag = 'answer-offensive-flag-'; - var offensiveClassFlag = 'offensive-flag'; - var questionControlsId = 'question-controls'; - var removeQuestionLinkIdPrefix = 'question-delete-link-'; - var removeAnswerLinkIdPrefix = 'answer-delete-link-'; - var questionSubscribeUpdates = 'question-subscribe-updates'; - - var acceptAnonymousMessage = $.i18n._('insufficient privilege'); - var acceptOwnAnswerMessage = $.i18n._('cannot pick own answer as best'); - - var pleaseLogin = "" - + $.i18n._('please login') + ""; - - var pleaseSeeFAQ = $.i18n._('please see') + "faq"; - - var favoriteAnonymousMessage = $.i18n._('anonymous users cannot select favorite questions') - var voteAnonymousMessage = $.i18n._('anonymous users cannot vote') + pleaseLogin; - var upVoteRequiredScoreMessage = $.i18n._('>15 points requried to upvote') + pleaseSeeFAQ; - var downVoteRequiredScoreMessage = $.i18n._('>100 points required to downvote') + pleaseSeeFAQ; - var voteOwnDeniedMessage = $.i18n._('cannot vote for own posts'); - var voteRequiredMoreVotes = $.i18n._('daily vote cap exhausted') + pleaseSeeFAQ; - var voteDenyCancelMessage = $.i18n._('cannot revoke old vote') + pleaseSeeFAQ; - var offensiveConfirmation = $.i18n._('please confirm offensive'); - var offensiveAnonymousMessage = $.i18n._('anonymous users cannot flag offensive posts') + pleaseLogin; - var offensiveTwiceMessage = $.i18n._('cannot flag message as offensive twice') + pleaseSeeFAQ; - var offensiveNoFlagsLeftMessage = $.i18n._('flag offensive cap exhausted') + pleaseSeeFAQ; - var offensiveNoPermissionMessage = $.i18n._('need >15 points to report spam') + pleaseSeeFAQ; - var removeConfirmation = $.i18n._('confirm delete'); - var removeAnonymousMessage = $.i18n._('anonymous users cannot delete/undelete'); - var recoveredMessage = $.i18n._('post recovered'); - var deletedMessage = $.i18n._('post deleted'); - - var VoteType = { - acceptAnswer : 0, - questionUpVote : 1, - questionDownVote : 2, - favorite : 4, - answerUpVote: 5, - answerDownVote:6, - offensiveQuestion : 7, - offensiveAnswer:8, - removeQuestion: 9, - removeAnswer:10, - questionSubscribeUpdates:11, - questionUnsubscribeUpdates:12 - }; - - var getFavoriteButton = function(){ - var favoriteButton = 'div.'+ voteContainerId +' img[class='+ imgClassPrefixFavorite +']'; - return $(favoriteButton); - }; - var getFavoriteNumber = function(){ - var favoriteNumber = '#'+ divIdFavorite ; - return $(favoriteNumber); - }; - var getQuestionVoteUpButton = function(){ - var questionVoteUpButton = 'div.'+ voteContainerId +' img[id^='+ imgIdPrefixQuestionVoteup +']'; - return $(questionVoteUpButton); - }; - var getQuestionVoteDownButton = function(){ - var questionVoteDownButton = 'div.'+ voteContainerId +' img[id^='+ imgIdPrefixQuestionVotedown +']'; - return $(questionVoteDownButton); - }; - var getAnswerVoteUpButtons = function(){ - var answerVoteUpButton = 'div.'+ voteContainerId +' img[id^='+ imgIdPrefixAnswerVoteup +']'; - return $(answerVoteUpButton); - }; - var getAnswerVoteDownButtons = function(){ - var answerVoteDownButton = 'div.'+ voteContainerId +' img[id^='+ imgIdPrefixAnswerVotedown +']'; - return $(answerVoteDownButton); - }; - var getAnswerVoteUpButton = function(id){ - var answerVoteUpButton = 'div.'+ voteContainerId +' img[id='+ imgIdPrefixAnswerVoteup + id + ']'; - return $(answerVoteUpButton); - }; - var getAnswerVoteDownButton = function(id){ - var answerVoteDownButton = 'div.'+ voteContainerId +' img[id='+ imgIdPrefixAnswerVotedown + id + ']'; - return $(answerVoteDownButton); - }; - - var getOffensiveQuestionFlag = function(){ - var offensiveQuestionFlag = '#question-table span[class='+ offensiveClassFlag +']'; - return $(offensiveQuestionFlag); - }; - - var getOffensiveAnswerFlags = function(){ - var offensiveQuestionFlag = 'div.answer span[class='+ offensiveClassFlag +']'; - return $(offensiveQuestionFlag); - }; - - var getremoveQuestionLink = function(){ - var removeQuestionLink = 'div#question-controls a[id^='+ removeQuestionLinkIdPrefix +']'; - return $(removeQuestionLink); - }; - - var getquestionSubscribeUpdatesCheckbox = function(){ - return $('#' + questionSubscribeUpdates); - }; - - var getremoveAnswersLinks = function(){ - var removeAnswerLinks = 'div.answer-controls a[id^='+ removeAnswerLinkIdPrefix +']'; - return $(removeAnswerLinks); - }; - - var setVoteImage = function(voteType, undo, object){ - var flag = undo ? "" : "-on"; - var arrow = (voteType == VoteType.questionUpVote || voteType == VoteType.answerUpVote) ? "up" : "down"; - object.attr("src", scriptUrl + "content/images/vote-arrow-"+ arrow + flag +".png"); - - // if undo voting, then undo the pair of arrows. - if(undo){ - if(voteType == VoteType.questionUpVote || voteType == VoteType.questionDownVote){ - $(getQuestionVoteUpButton()).attr("src", scriptUrl + "content/images/vote-arrow-up.png"); - $(getQuestionVoteDownButton()).attr("src", scriptUrl + "content/images/vote-arrow-down.png"); - } - else{ - $(getAnswerVoteUpButton(postId)).attr("src", scriptUrl + "content/images/vote-arrow-up.png"); - $(getAnswerVoteDownButton(postId)).attr("src", scriptUrl + "content/images/vote-arrow-down.png"); - } - } - }; - - var setVoteNumber = function(object, number){ - var voteNumber = object.parent('div.'+ voteContainerId).find('div.'+ voteNumberClass); - $(voteNumber).text(number); - }; - - var bindEvents = function(){ - // accept answers - if(questionAuthorId == currentUserId){ - var acceptedButtons = 'div.'+ voteContainerId +' img[id^='+ imgIdPrefixAccept +']'; - $(acceptedButtons).unbind('click').click(function(event){ - Vote.accept($(event.target)); - }); - } - // set favorite question - var favoriteButton = getFavoriteButton(); - favoriteButton.unbind('click').click(function(event){ - Vote.favorite($(event.target)); - }); - - // question vote up - var questionVoteUpButton = getQuestionVoteUpButton(); - questionVoteUpButton.unbind('click').click(function(event){ - Vote.vote($(event.target), VoteType.questionUpVote); - }); - - var questionVoteDownButton = getQuestionVoteDownButton(); - questionVoteDownButton.unbind('click').click(function(event){ - Vote.vote($(event.target), VoteType.questionDownVote); - }); - - var answerVoteUpButton = getAnswerVoteUpButtons(); - answerVoteUpButton.unbind('click').click(function(event){ - Vote.vote($(event.target), VoteType.answerUpVote); - }); - - var answerVoteDownButton = getAnswerVoteDownButtons(); - answerVoteDownButton.unbind('click').click(function(event){ - Vote.vote($(event.target), VoteType.answerDownVote); - }); - - getOffensiveQuestionFlag().unbind('click').click(function(event){ - Vote.offensive(this, VoteType.offensiveQuestion); - }); - - getOffensiveAnswerFlags().unbind('click').click(function(event){ - Vote.offensive(this, VoteType.offensiveAnswer); - }); - - getremoveQuestionLink().unbind('click').click(function(event){ - Vote.remove(this, VoteType.removeQuestion); - }); - - getquestionSubscribeUpdatesCheckbox().unbind('click').click(function(event){ - if (this.checked){ - Vote.vote($(event.target), VoteType.questionSubscribeUpdates); - } - else { - Vote.vote($(event.target), VoteType.questionUnsubscribeUpdates); - } - }); - - getremoveAnswersLinks().unbind('click').click(function(event){ - Vote.remove(this, VoteType.removeAnswer); - }); - }; - - var submit = function(object, voteType, callback) { - $.ajax({ - type: "POST", - cache: false, - dataType: "json", - url: scriptUrl + $.i18n._("questions/") + questionId + "/" + $.i18n._("vote/"), - data: { "type": voteType, "postId": postId }, - error: handleFail, - success: function(data){callback(object, voteType, data)}}); - }; - - var handleFail = function(xhr, msg){ - alert("Callback invoke error: " + msg); - }; - - // callback function for Accept Answer action - var callback_accept = function(object, voteType, data){ - if(data.allowed == "0" && data.success == "0"){ - showMessage(object, acceptAnonymousMessage); - } - else if(data.allowed == "-1"){ - showMessage(object, acceptOwnAnswerMessage); - } - else if(data.status == "1"){ - object.attr("src", scriptUrl + "content/images/vote-accepted.png"); - $("#"+answerContainerIdPrefix+postId).removeClass("accepted-answer"); - $("#"+commentLinkIdPrefix+postId).removeClass("comment-link-accepted"); - } - else if(data.success == "1"){ - var acceptedButtons = 'div.'+ voteContainerId +' img[id^='+ imgIdPrefixAccept +']'; - $(acceptedButtons).attr("src", scriptUrl + "content/images/vote-accepted.png"); - var answers = ("div[id^="+answerContainerIdPrefix +"]"); - $(answers).removeClass("accepted-answer"); - var commentLinks = ("div[id^="+answerContainerIdPrefix +"] div[id^="+ commentLinkIdPrefix +"]"); - $(commentLinks).removeClass("comment-link-accepted"); - - object.attr("src", scriptUrl + "content/images/vote-accepted-on.png"); - $("#"+answerContainerIdPrefix+postId).addClass("accepted-answer"); - $("#"+commentLinkIdPrefix+postId).addClass("comment-link-accepted"); - } - else{ - showMessage(object, data.message); - } - }; - - var callback_favorite = function(object, voteType, data){ - if(data.allowed == "0" && data.success == "0"){ - showMessage(object, favoriteAnonymousMessage.replace("{{QuestionID}}", questionId)); - } - else if(data.status == "1"){ - object.attr("src", scriptUrl + "content/images/vote-favorite-off.png"); - var fav = getFavoriteNumber(); - fav.removeClass("my-favorite-number"); - if(data.count == 0) - data.count = ''; - fav.text(data.count); - } - else if(data.success == "1"){ - object.attr("src", scriptUrl + "content/images/vote-favorite-on.png"); - var fav = getFavoriteNumber(); - fav.text(data.count); - fav.addClass("my-favorite-number"); - } - else{ - showMessage(object, data.message); - } - }; - - var callback_vote = function(object, voteType, data){ - if(data.allowed == "0" && data.success == "0"){ - showMessage(object, voteAnonymousMessage.replace("{{QuestionID}}", questionId)); - } - else if (data.allowed == "-3"){ - showMessage(object, voteRequiredMoreVotes); - } - else if (data.allowed == "-2"){ - if (voteType == VoteType.questionUpVote || voteType == VoteType.answerUpVote){ - showMessage(object, upVoteRequiredScoreMessage); - } - else if (voteType == VoteType.questionDownVote || voteType == VoteType.answerDownVote){ - showMessage(object, downVoteRequiredScoreMessage); - } - } - else if (data.allowed == "-1"){ - showMessage(object, voteOwnDeniedMessage); - } - else if (data.status == "2"){ - showMessage(object, voteDenyCancelMessage); - } - else if (data.status == "1"){ - setVoteImage(voteType, true, object); - setVoteNumber(object, data.count); - } - else if (data.success == "1"){ - setVoteImage(voteType, false, object); - setVoteNumber(object, data.count); - if (data.message.length > 0){ - showMessage(object, data.message); - } - } - }; - - var callback_offensive = function(object, voteType, data){ - object = $(object); - if (data.allowed == "0" && data.success == "0"){ - showMessage(object, offensiveAnonymousMessage.replace("{{QuestionID}}", questionId)); - } - else if (data.allowed == "-3"){ - showMessage(object, offensiveNoFlagsLeftMessage); - } - else if (data.allowed == "-2"){ - showMessage(object, offensiveNoPermissionMessage); - } - else if (data.status == "1"){ - showMessage(object, offensiveTwiceMessage); - } - else if (data.success == "1"){ - $(object).children('span[class=darkred]').text("("+ data.count +")"); - } - }; - - var callback_remove = function(object, voteType, data){ - if (data.allowed == "0" && data.success == "0"){ - showMessage(object, removeAnonymousMessage.replace("{{QuestionID}}", questionId)); - } - else if (data.success == "1"){ - if (voteType == VoteType.removeQuestion){ - window.location.href = scriptUrl + $.i18n._("questions/"); - } - else { - if (removeActionType == 'delete'){ - postNode.addClass('deleted'); - postRemoveLink.innerHTML = $.i18n._('undelete'); - showMessage(object, deletedMessage); - } - else if (removeActionType == 'undelete') { - postNode.removeClass('deleted'); - postRemoveLink.innerHTML = $.i18n._('delete'); - showMessage(object, recoveredMessage); - } - } - } - }; - - return { - init : function(qId, qSlug, questionAuthor, userId){ - questionId = qId; - questionSlug = qSlug; - questionAuthorId = questionAuthor; - currentUserId = userId; - bindEvents(); - }, - - // Accept answer public function - accept: function(object){ - postId = object.attr("id").substring(imgIdPrefixAccept.length); - submit(object, VoteType.acceptAnswer, callback_accept); - }, - - favorite: function(object){ - if (!currentUserId || currentUserId.toUpperCase() == "NONE"){ - showMessage(object, favoriteAnonymousMessage.replace("{{QuestionID}}", questionId)); - return false; - } - submit(object, VoteType.favorite, callback_favorite); - }, - - vote: function(object, voteType){ - if (!currentUserId || currentUserId.toUpperCase() == "NONE"){ - showMessage(object, voteAnonymousMessage.replace("{{QuestionID}}", questionId).replace("{{questionSlug}}", questionSlug)); - return false; - } - if (voteType == VoteType.answerUpVote){ - postId = object.attr("id").substring(imgIdPrefixAnswerVoteup.length); - } - else if (voteType == VoteType.answerDownVote){ - postId = object.attr("id").substring(imgIdPrefixAnswerVotedown.length); - } - - submit(object, voteType, callback_vote); - }, - - offensive: function(object, voteType){ - if (!currentUserId || currentUserId.toUpperCase() == "NONE"){ - showMessage($(object), offensiveAnonymousMessage.replace("{{QuestionID}}", questionId)); - return false; - } - if (confirm(offensiveConfirmation)){ - postId = object.id.substr(object.id.lastIndexOf('-') + 1); - submit(object, voteType, callback_offensive); - } - }, - - remove: function(object, voteType){ - if (!currentUserId || currentUserId.toUpperCase() == "NONE"){ - showMessage($(object), removeAnonymousMessage.replace("{{QuestionID}}", questionId)); - return false; - } - bits = object.id.split('-'); - postId = bits.pop();/* this seems to be used within submit! */ - postType = bits.shift(); - - var do_proceed = false; - if (postType == 'answer'){ - postNode = $('#answer-container-' + postId); - postRemoveLink = object; - if (postNode.hasClass('deleted')){ - removeActionType = 'undelete'; - do_proceed = true; - } - else { - removeActionType = 'delete'; - do_proceed = confirm(removeConfirmation); - } - } - else { - do_proceed = confirm(removeConfirmation); - } - if (do_proceed) { - submit($(object), voteType, callback_remove); - } - } - } -} (); - - -// site comments -function createComments(type) { - var objectType = type; - var jDivInit = function(id) { - return $("#comments-container-" + objectType + '-' + id); - }; - - var appendLoaderImg = function(id) { - appendLoader("#comments-container-" + objectType + '-' + id); - }; - - var canPostComments = function(id) { - var jHidden = $("#can-post-comments-" + objectType + '-' + id); - return jHidden.val().toLowerCase() == "true"; - }; - - var renderForm = function(id) { - var formId = "form-comments-" + objectType + "-" + id; - var jDiv = $('#comments-link-' + objectType + "-" + id).parent(); - $(jDiv).css('background','none'); - $(jDiv).css('padding-left',0); - if (canPostComments(id)) { - if (jDiv.find("#" + formId).length == 0) { - var form = '
'; - form += ''; - form += '
'; - form += '
'; - - jDiv.append(form); - - setupFormValidation("#" + formId, - { comment: { required: true, minlength: 10} }, '', - function() { postComment(id, formId); }); - } - } - else { - var divId = "comments-rep-needed-" + objectType + '-' + id; - if (jDiv.find("#" + divId).length == 0) { - jDiv.append('

' - + $.i18n._('to comment, need') + ' ' + - + repNeededForComments + ' ' + $.i18n._('community karma points') - + '' - + $.i18n._('please see') + 'faq

'); - } - } - }; - - var getComments = function(id, jDiv) { - //appendLoaderImg(id); - $.getJSON(scriptUrl + objectType + "s/" + id + "/" + $.i18n._("comments/") - , function(json) { showComments(id, json); }); - }; - - var showComments = function(id, json) { - var jDiv = jDivInit(id); - - //jDiv = jDiv.find("div.comments"); // this div should contain any fetched comments.. - //jDiv.find("div[id^='comment-" + objectType + "-'" + "]").remove(); // clean previous calls.. - jDiv.children().remove(); - removeLoader(); - if (json && json.length > 0) { - for (var i = 0; i < json.length; i++) - renderComment(jDiv, json[i]); - jDiv.children().show(); - } - }; - - var renderDeleteCommentIcon = function(post_id, delete_url){ - if (canPostComments(post_id)){ - var html = ''; - var img = scriptUrl + "content/images/close-small.png"; - var imgHover = scriptUrl + "content/images/close-small-hover.png"; - html += ''; - return html; - } - else{ - return ''; - } - } - - // {"Id":6,"PostId":38589,"CreationDate":"an hour ago","Text":"hello there!","UserDisplayName":"Jarrod Dixon","UserUrl":"/users/3/jarrod-dixon","DeleteUrl":null} - var renderComment = function(jDiv, json) { - var html = ''; - - jDiv.append(html); - }; - - var postComment = function(id, formId) { - //appendLoaderImg(id); - - var formSelector = "#" + formId; - var textarea = $(formSelector + " textarea"); - - //todo fix url translations!!! - $.ajax({ - type: "POST", - url: scriptUrl + objectType + "s/" + id + "/" + $.i18n._("comments/"), - dataType: "json", - data: { comment: textarea.val() }, - success: function(json) { - showComments(id, json); - textarea.val(""); - commentsFactory[objectType].updateTextCounter(textarea); - enableSubmitButton(formSelector); - }, - error: function(res, textStatus, errorThrown) { - removeLoader(); - showMessage(formSelector, res.responseText); - enableSubmitButton(formSelector); - } - }); - }; - - // public methods.. - return { - - init: function() { - // Setup "show comments" clicks.. - $("a[id^='comments-link-" + objectType + "-" + "']").unbind("click").click(function() { - commentsFactory[objectType].show($(this).attr("id").substr(("comments-link-" + objectType + "-").length)); - }); - - var cBox = $("[id^='comments-container-" + objectType + "']"); - cBox.each( function(i){ - var post_id = $(this).attr('id').replace('comments-container-' + objectType + '-', ''); - $(this).children().each( - function(i){ - var comment_id = $(this).attr('id').replace('comment-',''); - var delete_url = scriptUrl + objectType + 's/' + post_id + '/' - + $.i18n._('comments/') + comment_id + '/' + $.i18n._('delete/'); - var html = $(this).html(); - var CommentsClass; - if (objectType == 'question'){ - CommentsClass = questionComments; - } - else if (objectType == 'answer') { - CommentsClass = answerComments; - } - var delete_icon = $(this).find('img.delete-icon'); - delete_icon.click(function(){CommentsClass.deleteComment($(this),comment_id,delete_url);}); - delete_icon.unbind('mouseover').bind('mouseover', - function(){ - $(this).attr('src',scriptUrl + 'content/images/close-small-hover.png'); - } - ); - delete_icon.unbind('mouseout').bind('mouseout', - function(){ - $(this).attr('src',scriptUrl + 'content/images/close-small.png'); - } - ); - } - ); - }); - }, - - show: function(id) { - var jDiv = jDivInit(id); - getComments(id, jDiv); - renderForm(id); - jDiv.show(); - - var link = $('#comments-link-' + objectType + '-' + id); - if (canPostComments(id)) link.parent().find("textarea").get(0).focus(); - link.remove(); - }, - - hide: function(id) { - var jDiv = jDivInit(id); - var len = jDiv.children("div.comments").children().length; - var anchorText = len == 0 ? $.i18n._('add a comment') : $.i18n._('comments') + ' (' + len + ")"; - - jDiv.hide(); - jDiv.siblings("a").unbind("click").click(function() { commentsFactory[objectType].show(id); }).html(anchorText); - jDiv.children("div.comments").children().hide(); - }, - - deleteComment: function(jImg, id, deleteUrl) { - if (confirm($.i18n._('confirm delete comment'))) { - jImg.hide(); - $.post(deleteUrl, { dataNeeded: "forIIS7" }, function(json) { - var par = jImg.parent(); - par.remove(); - }, "json"); - } - }, - - updateTextCounter: function(textarea) { - var length = textarea.value ? textarea.value.length : 0; - var color = length > 270 ? "#f00" : length > 200 ? "#f60" : "#999"; - var jSpan = $(textarea).siblings("span.text-counter"); - jSpan.html($.i18n._('can write') - + (300 - length) + ' ' - + $.i18n._('characters')).css("color", color); - } - }; -} - -var questionComments = createComments('question'); -var answerComments = createComments('answer'); - -$().ready(function() { - questionComments.init(); - answerComments.init(); -}); - -var commentsFactory = {'question' : questionComments, 'answer' : answerComments}; - -/* -Prettify -http://www.apache.org/licenses/LICENSE-2.0 -*/ -var PR_SHOULD_USE_CONTINUATION = true; var PR_TAB_WIDTH = 8; var PR_normalizedHtml; var PR; var prettyPrintOne; var prettyPrint; function _pr_isIE6() { var isIE6 = navigator && navigator.userAgent && /\bMSIE 6\./.test(navigator.userAgent); _pr_isIE6 = function() { return isIE6; }; return isIE6; } (function() { function wordSet(words) { words = words.split(/ /g); var set = {}; for (var i = words.length; --i >= 0; ) { var w = words[i]; if (w) { set[w] = null; } } return set; } var FLOW_CONTROL_KEYWORDS = "break continue do else for if return while "; var C_KEYWORDS = FLOW_CONTROL_KEYWORDS + "auto case char const default " + "double enum extern float goto int long register short signed sizeof " + "static struct switch typedef union unsigned void volatile "; var COMMON_KEYWORDS = C_KEYWORDS + "catch class delete false import " + "new operator private protected public this throw true try "; var CPP_KEYWORDS = COMMON_KEYWORDS + "alignof align_union asm axiom bool " + "concept concept_map const_cast constexpr decltype " + "dynamic_cast explicit export friend inline late_check " + "mutable namespace nullptr reinterpret_cast static_assert static_cast " + "template typeid typename typeof using virtual wchar_t where "; var JAVA_KEYWORDS = COMMON_KEYWORDS + "boolean byte extends final finally implements import instanceof null " + "native package strictfp super synchronized throws transient "; var CSHARP_KEYWORDS = JAVA_KEYWORDS + "as base by checked decimal delegate descending event " + "fixed foreach from group implicit in interface internal into is lock " + "object out override orderby params readonly ref sbyte sealed " + "stackalloc string select uint ulong unchecked unsafe ushort var "; var JSCRIPT_KEYWORDS = COMMON_KEYWORDS + "debugger eval export function get null set undefined var with " + "Infinity NaN "; var PERL_KEYWORDS = "caller delete die do dump elsif eval exit foreach for " + "goto if import last local my next no our print package redo require " + "sub undef unless until use wantarray while BEGIN END "; var PYTHON_KEYWORDS = FLOW_CONTROL_KEYWORDS + "and as assert class def del " + "elif except exec finally from global import in is lambda " + "nonlocal not or pass print raise try with yield " + "False True None "; var RUBY_KEYWORDS = FLOW_CONTROL_KEYWORDS + "alias and begin case class def" + " defined elsif end ensure false in module next nil not or redo rescue " + "retry self super then true undef unless until when yield BEGIN END "; var SH_KEYWORDS = FLOW_CONTROL_KEYWORDS + "case done elif esac eval fi " + "function in local set then until "; var ALL_KEYWORDS = (CPP_KEYWORDS + CSHARP_KEYWORDS + JSCRIPT_KEYWORDS + PERL_KEYWORDS + PYTHON_KEYWORDS + RUBY_KEYWORDS + SH_KEYWORDS); var PR_STRING = 'str'; var PR_KEYWORD = 'kwd'; var PR_COMMENT = 'com'; var PR_TYPE = 'typ'; var PR_LITERAL = 'lit'; var PR_PUNCTUATION = 'pun'; var PR_PLAIN = 'pln'; var PR_TAG = 'tag'; var PR_DECLARATION = 'dec'; var PR_SOURCE = 'src'; var PR_ATTRIB_NAME = 'atn'; var PR_ATTRIB_VALUE = 'atv'; var PR_NOCODE = 'nocode'; function isWordChar(ch) { return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z'); } function spliceArrayInto(inserted, container, containerPosition, countReplaced) { inserted.unshift(containerPosition, countReplaced || 0); try { container.splice.apply(container, inserted); } finally { inserted.splice(0, 2); } } var REGEXP_PRECEDER_PATTERN = function() { var preceders = ["!", "!=", "!==", "#", "%", "%=", "&", "&&", "&&=", "&=", "(", "*", "*=", "+=", ",", "-=", "->", "/", "/=", ":", "::", ";", "<", "<<", "<<=", "<=", "=", "==", "===", ">", ">=", ">>", ">>=", ">>>", ">>>=", "?", "@", "[", "^", "^=", "^^", "^^=", "{", "|", "|=", "||", "||=", "~", "break", "case", "continue", "delete", "do", "else", "finally", "instanceof", "return", "throw", "try", "typeof"]; var pattern = '(?:' + '(?:(?:^|[^0-9.])\\.{1,3})|' + '(?:(?:^|[^\\+])\\+)|' + '(?:(?:^|[^\\-])-)'; for (var i = 0; i < preceders.length; ++i) { var preceder = preceders[i]; if (isWordChar(preceder.charAt(0))) { pattern += '|\\b' + preceder; } else { pattern += '|' + preceder.replace(/([^=<>:&])/g, '\\$1'); } } pattern += '|^)\\s*$'; return new RegExp(pattern); } (); var pr_amp = /&/g; var pr_lt = //g; var pr_quot = /\"/g; function attribToHtml(str) { return str.replace(pr_amp, '&').replace(pr_lt, '<').replace(pr_gt, '>').replace(pr_quot, '"'); } function textToHtml(str) { return str.replace(pr_amp, '&').replace(pr_lt, '<').replace(pr_gt, '>'); } var pr_ltEnt = /</g; var pr_gtEnt = />/g; var pr_aposEnt = /'/g; var pr_quotEnt = /"/g; var pr_ampEnt = /&/g; var pr_nbspEnt = / /g; function htmlToText(html) { var pos = html.indexOf('&'); if (pos < 0) { return html; } for (--pos; (pos = html.indexOf('&#', pos + 1)) >= 0; ) { var end = html.indexOf(';', pos); if (end >= 0) { var num = html.substring(pos + 3, end); var radix = 10; if (num && num.charAt(0) === 'x') { num = num.substring(1); radix = 16; } var codePoint = parseInt(num, radix); if (!isNaN(codePoint)) { html = (html.substring(0, pos) + String.fromCharCode(codePoint) + html.substring(end + 1)); } } } return html.replace(pr_ltEnt, '<').replace(pr_gtEnt, '>').replace(pr_aposEnt, "'").replace(pr_quotEnt, '"').replace(pr_ampEnt, '&').replace(pr_nbspEnt, ' '); } function isRawContent(node) { return 'XMP' === node.tagName; } function normalizedHtml(node, out) { switch (node.nodeType) { case 1: var name = node.tagName.toLowerCase(); out.push('<', name); for (var i = 0; i < node.attributes.length; ++i) { var attr = node.attributes[i]; if (!attr.specified) { continue; } out.push(' '); normalizedHtml(attr, out); } out.push('>'); for (var child = node.firstChild; child; child = child.nextSibling) { normalizedHtml(child, out); } if (node.firstChild || !/^(?:br|link|img)$/.test(name)) { out.push('<\/', name, '>'); } break; case 2: out.push(node.name.toLowerCase(), '="', attribToHtml(node.value), '"'); break; case 3: case 4: out.push(textToHtml(node.nodeValue)); break; } } var PR_innerHtmlWorks = null; function getInnerHtml(node) { if (null === PR_innerHtmlWorks) { var testNode = document.createElement('PRE'); testNode.appendChild(document.createTextNode('\n')); PR_innerHtmlWorks = !/= 0; nSpaces -= SPACES.length) { out.push(SPACES.substring(0, nSpaces)); } pos = i + 1; break; case '\n': charInLine = 0; break; default: ++charInLine; } } if (!out) { return plainText; } out.push(plainText.substring(pos)); return out.join(''); }; } var pr_chunkPattern = /(?:[^<]+|||<\/?[a-zA-Z][^>]*>|<)/g; var pr_commentPrefix = /^|$)/, null], [PR_SOURCE, /^<\?[\s\S]*?(?:\?>|$)/, null], [PR_SOURCE, /^<%[\s\S]*?(?:%>|$)/, null], [PR_SOURCE, /^<(script|style|xmp)\b[^>]*>[\s\S]*?<\/\1\b[^>]*>/i, null], [PR_TAG, /^<\/?\w[^<>]*>/, null]]); var PR_SOURCE_CHUNK_PARTS = /^(<[^>]*>)([\s\S]*)(<\/[^>]*>)$/; function tokenizeMarkup(source) { var decorations = PR_MARKUP_LEXER(source); for (var i = 0; i < decorations.length; i += 2) { if (decorations[i + 1] === PR_SOURCE) { var start, end; start = decorations[i]; end = i + 2 < decorations.length ? decorations[i + 2] : source.length; var sourceChunk = source.substring(start, end); var match = sourceChunk.match(PR_SOURCE_CHUNK_PARTS); if (match) { decorations.splice(i, 2, start, PR_TAG, start + match[1].length, PR_SOURCE, start + match[1].length + (match[2] || '').length, PR_TAG); } } } return decorations; } var PR_TAG_LEXER = createSimpleLexer([[PR_ATTRIB_VALUE, /^\'[^\']*(?:\'|$)/, null, "'"], [PR_ATTRIB_VALUE, /^\"[^\"]*(?:\"|$)/, null, '"'], [PR_PUNCTUATION, /^[<>\/=]+/, null, '<>/=']], [[PR_TAG, /^[\w:\-]+/, /^= 2 && /^[\"\']/.test(attribValue) && attribValue.charAt(0) === attribValue.charAt(attribLen - 1)); var attribSource; var attribSourceStart; var attribSourceEnd; if (quoted) { attribSourceStart = start + 1; attribSourceEnd = end - 1; attribSource = attribValue; } else { attribSourceStart = start + 1; attribSourceEnd = end - 1; attribSource = attribValue.substring(1, attribValue.length - 1); } var attribSourceDecorations = decorateSource(attribSource); for (var j = 0, m = attribSourceDecorations.length; j < m; j += 2) { attribSourceDecorations[j] += attribSourceStart; } if (quoted) { attribSourceDecorations.push(attribSourceEnd, PR_ATTRIB_VALUE); spliceArrayInto(attribSourceDecorations, decorations, i + 2, 0); } else { spliceArrayInto(attribSourceDecorations, decorations, i, 2); } } nextValueIsSource = false; } } return decorations; } function decorateMarkup(sourceCode) { var decorations = tokenizeMarkup(sourceCode); decorations = splitTagAttributes(sourceCode, decorations); decorations = splitSourceNodes(sourceCode, decorations); decorations = splitSourceAttributes(sourceCode, decorations); return decorations; } function recombineTagsAndDecorations(sourceText, extractedTags, decorations) { var html = []; var outputIdx = 0; var openDecoration = null; var currentDecoration = null; var tagPos = 0; var decPos = 0; var tabExpander = makeTabExpander(PR_TAB_WIDTH); var adjacentSpaceRe = /([\r\n ]) /g; var startOrSpaceRe = /(^| ) /gm; var newlineRe = /\r\n?|\n/g; var trailingSpaceRe = /[ \r\n]$/; var lastWasSpace = true; function emitTextUpTo(sourceIdx) { if (sourceIdx > outputIdx) { if (openDecoration && openDecoration !== currentDecoration) { html.push(''); openDecoration = null; } if (!openDecoration && currentDecoration) { openDecoration = currentDecoration; html.push(''); } var htmlChunk = textToHtml(tabExpander(sourceText.substring(outputIdx, sourceIdx))).replace(lastWasSpace ? startOrSpaceRe : adjacentSpaceRe, '$1 '); lastWasSpace = trailingSpaceRe.test(htmlChunk); html.push(htmlChunk.replace(newlineRe, '
')); outputIdx = sourceIdx; } } while (true) { var outputTag; if (tagPos < extractedTags.length) { if (decPos < decorations.length) { outputTag = extractedTags[tagPos] <= decorations[decPos]; } else { outputTag = true; } } else { outputTag = false; } if (outputTag) { emitTextUpTo(extractedTags[tagPos]); if (openDecoration) { html.push('
'); openDecoration = null; } html.push(extractedTags[tagPos + 1]); tagPos += 2; } else if (decPos < decorations.length) { emitTextUpTo(decorations[decPos]); currentDecoration = decorations[decPos + 1]; decPos += 2; } else { break; } } emitTextUpTo(sourceText.length); if (openDecoration) { html.push(''); } return html.join(''); } var langHandlerRegistry = {}; function registerLangHandler(handler, fileExtensions) { for (var i = fileExtensions.length; --i >= 0; ) { var ext = fileExtensions[i]; if (!langHandlerRegistry.hasOwnProperty(ext)) { langHandlerRegistry[ext] = handler; } else if ('console' in window) { console.log('cannot override language handler %s', ext); } } } registerLangHandler(decorateSource, ['default-code']); registerLangHandler(decorateMarkup, ['default-markup', 'html', 'htm', 'xhtml', 'xml', 'xsl']); registerLangHandler(sourceDecorator({ keywords: CPP_KEYWORDS, hashComments: true, cStyleComments: true }), ['c', 'cc', 'cpp', 'cs', 'cxx', 'cyc']); registerLangHandler(sourceDecorator({ keywords: JAVA_KEYWORDS, cStyleComments: true }), ['java']); registerLangHandler(sourceDecorator({ keywords: SH_KEYWORDS, hashComments: true, multiLineStrings: true }), ['bsh', 'csh', 'sh']); registerLangHandler(sourceDecorator({ keywords: PYTHON_KEYWORDS, hashComments: true, multiLineStrings: true, tripleQuotedStrings: true }), ['cv', 'py']); registerLangHandler(sourceDecorator({ keywords: PERL_KEYWORDS, hashComments: true, multiLineStrings: true, regexLiterals: true }), ['perl', 'pl', 'pm']); registerLangHandler(sourceDecorator({ keywords: RUBY_KEYWORDS, hashComments: true, multiLineStrings: true, regexLiterals: true }), ['rb']); registerLangHandler(sourceDecorator({ keywords: JSCRIPT_KEYWORDS, cStyleComments: true, regexLiterals: true }), ['js']); function prettyPrintOne(sourceCodeHtml, opt_langExtension) { try { var sourceAndExtractedTags = extractTags(sourceCodeHtml); var source = sourceAndExtractedTags.source; var extractedTags = sourceAndExtractedTags.tags; if (!langHandlerRegistry.hasOwnProperty(opt_langExtension)) { opt_langExtension = /^\s*= 0) { var langExtension = cs.className.match(/\blang-(\w+)\b/); if (langExtension) { langExtension = langExtension[1]; } var nested = false; for (var p = cs.parentNode; p; p = p.parentNode) { if ((p.tagName === 'pre' || p.tagName === 'code' || p.tagName === 'xmp') && p.className && p.className.indexOf('prettyprint') >= 0) { nested = true; break; } } if (!nested) { var content = getInnerHtml(cs); content = content.replace(/(?:\r\n?|\n)$/, ''); var newContent = prettyPrintOne(content, langExtension); if (!isRawContent(cs)) { cs.innerHTML = newContent; } else { var pre = document.createElement('PRE'); for (var i = 0; i < cs.attributes.length; ++i) { var a = cs.attributes[i]; if (a.specified) { var aname = a.name.toLowerCase(); if (aname === 'class') { pre.className = a.value; } else { pre.setAttribute(a.name, a.value); } } } pre.innerHTML = newContent; cs.parentNode.replaceChild(pre, cs); cs = pre; } if (isIE6 && cs.tagName === 'PRE') { var lineBreaks = cs.getElementsByTagName('br'); for (var j = lineBreaks.length; --j >= 0; ) { var lineBreak = lineBreaks[j]; lineBreak.parentNode.replaceChild(document.createTextNode('\r\n'), lineBreak); } } } } } if (k < elements.length) { setTimeout(doWork, 250); } else if (opt_whenDone) { opt_whenDone(); } } doWork(); } window['PR_normalizedHtml'] = normalizedHtml; window['prettyPrintOne'] = prettyPrintOne; window['prettyPrint'] = prettyPrint; window['PR'] = { 'createSimpleLexer': createSimpleLexer, 'registerLangHandler': registerLangHandler, 'sourceDecorator': sourceDecorator, 'PR_ATTRIB_NAME': PR_ATTRIB_NAME, 'PR_ATTRIB_VALUE': PR_ATTRIB_VALUE, 'PR_COMMENT': PR_COMMENT, 'PR_DECLARATION': PR_DECLARATION, 'PR_KEYWORD': PR_KEYWORD, 'PR_LITERAL': PR_LITERAL, 'PR_NOCODE': PR_NOCODE, 'PR_PLAIN': PR_PLAIN, 'PR_PUNCTUATION': PR_PUNCTUATION, 'PR_SOURCE': PR_SOURCE, 'PR_STRING': PR_STRING, 'PR_TAG': PR_TAG, 'PR_TYPE': PR_TYPE }; })(); +/* +Scripts for cnprog.com +Project Name: Lanai +All Rights Resevred 2008. CNPROG.COM +*/ +var lanai = +{ + /** + * Finds any
tags which aren't registered for + * pretty printing, adds the appropriate class name and invokes prettify. + */ + highlightSyntax: function(){ + var styled = false; + $("pre code").parent().each(function(){ + if (!$(this).hasClass('prettyprint')){ + $(this).addClass('prettyprint'); + styled = true; + } + }); + + if (styled){ + prettyPrint(); + } + } +}; + +var Vote = function(){ + // All actions are related to a question + var questionId; + //question slug to build redirect urls + var questionSlug; + // The object we operate on actually. It can be a question or an answer. + var postId; + var questionAuthorId; + var currentUserId; + var answerContainerIdPrefix = 'answer-container-'; + var voteContainerId = 'vote-buttons'; + var imgIdPrefixAccept = 'answer-img-accept-'; + var imgClassPrefixFavorite = 'question-img-favorite'; + var imgIdPrefixQuestionVoteup = 'question-img-upvote-'; + var imgIdPrefixQuestionVotedown = 'question-img-downvote-'; + var imgIdPrefixAnswerVoteup = 'answer-img-upvote-'; + var imgIdPrefixAnswerVotedown = 'answer-img-downvote-'; + var divIdFavorite = 'favorite-number'; + var commentLinkIdPrefix = 'comment-'; + var voteNumberClass = "vote-number"; + var offensiveIdPrefixQuestionFlag = 'question-offensive-flag-'; + var offensiveIdPrefixAnswerFlag = 'answer-offensive-flag-'; + var offensiveClassFlag = 'offensive-flag'; + var questionControlsId = 'question-controls'; + var removeQuestionLinkIdPrefix = 'question-delete-link-'; + var removeAnswerLinkIdPrefix = 'answer-delete-link-'; + var questionSubscribeUpdates = 'question-subscribe-updates'; + + var acceptAnonymousMessage = $.i18n._('insufficient privilege'); + var acceptOwnAnswerMessage = $.i18n._('cannot pick own answer as best'); + + var pleaseLogin = "
" + + $.i18n._('please login') + ""; + + var pleaseSeeFAQ = $.i18n._('please see') + "faq"; + + var favoriteAnonymousMessage = $.i18n._('anonymous users cannot select favorite questions') + var voteAnonymousMessage = $.i18n._('anonymous users cannot vote') + pleaseLogin; + var upVoteRequiredScoreMessage = $.i18n._('>15 points requried to upvote') + pleaseSeeFAQ; + var downVoteRequiredScoreMessage = $.i18n._('>100 points required to downvote') + pleaseSeeFAQ; + var voteOwnDeniedMessage = $.i18n._('cannot vote for own posts'); + var voteRequiredMoreVotes = $.i18n._('daily vote cap exhausted') + pleaseSeeFAQ; + var voteDenyCancelMessage = $.i18n._('cannot revoke old vote') + pleaseSeeFAQ; + var offensiveConfirmation = $.i18n._('please confirm offensive'); + var offensiveAnonymousMessage = $.i18n._('anonymous users cannot flag offensive posts') + pleaseLogin; + var offensiveTwiceMessage = $.i18n._('cannot flag message as offensive twice') + pleaseSeeFAQ; + var offensiveNoFlagsLeftMessage = $.i18n._('flag offensive cap exhausted') + pleaseSeeFAQ; + var offensiveNoPermissionMessage = $.i18n._('need >15 points to report spam') + pleaseSeeFAQ; + var removeConfirmation = $.i18n._('confirm delete'); + var removeAnonymousMessage = $.i18n._('anonymous users cannot delete/undelete'); + var recoveredMessage = $.i18n._('post recovered'); + var deletedMessage = $.i18n._('post deleted'); + + var VoteType = { + acceptAnswer : 0, + questionUpVote : 1, + questionDownVote : 2, + favorite : 4, + answerUpVote: 5, + answerDownVote:6, + offensiveQuestion : 7, + offensiveAnswer:8, + removeQuestion: 9, + removeAnswer:10, + questionSubscribeUpdates:11, + questionUnsubscribeUpdates:12 + }; + + var getFavoriteButton = function(){ + var favoriteButton = 'div.'+ voteContainerId +' img[class='+ imgClassPrefixFavorite +']'; + return $(favoriteButton); + }; + var getFavoriteNumber = function(){ + var favoriteNumber = '#'+ divIdFavorite ; + return $(favoriteNumber); + }; + var getQuestionVoteUpButton = function(){ + var questionVoteUpButton = 'div.'+ voteContainerId +' img[id^='+ imgIdPrefixQuestionVoteup +']'; + return $(questionVoteUpButton); + }; + var getQuestionVoteDownButton = function(){ + var questionVoteDownButton = 'div.'+ voteContainerId +' img[id^='+ imgIdPrefixQuestionVotedown +']'; + return $(questionVoteDownButton); + }; + var getAnswerVoteUpButtons = function(){ + var answerVoteUpButton = 'div.'+ voteContainerId +' img[id^='+ imgIdPrefixAnswerVoteup +']'; + return $(answerVoteUpButton); + }; + var getAnswerVoteDownButtons = function(){ + var answerVoteDownButton = 'div.'+ voteContainerId +' img[id^='+ imgIdPrefixAnswerVotedown +']'; + return $(answerVoteDownButton); + }; + var getAnswerVoteUpButton = function(id){ + var answerVoteUpButton = 'div.'+ voteContainerId +' img[id='+ imgIdPrefixAnswerVoteup + id + ']'; + return $(answerVoteUpButton); + }; + var getAnswerVoteDownButton = function(id){ + var answerVoteDownButton = 'div.'+ voteContainerId +' img[id='+ imgIdPrefixAnswerVotedown + id + ']'; + return $(answerVoteDownButton); + }; + + var getOffensiveQuestionFlag = function(){ + var offensiveQuestionFlag = '#question-table span[class='+ offensiveClassFlag +']'; + return $(offensiveQuestionFlag); + }; + + var getOffensiveAnswerFlags = function(){ + var offensiveQuestionFlag = 'div.answer span[class='+ offensiveClassFlag +']'; + return $(offensiveQuestionFlag); + }; + + var getremoveQuestionLink = function(){ + var removeQuestionLink = 'div#question-controls a[id^='+ removeQuestionLinkIdPrefix +']'; + return $(removeQuestionLink); + }; + + var getquestionSubscribeUpdatesCheckbox = function(){ + return $('#' + questionSubscribeUpdates); + }; + + var getremoveAnswersLinks = function(){ + var removeAnswerLinks = 'div.answer-controls a[id^='+ removeAnswerLinkIdPrefix +']'; + return $(removeAnswerLinks); + }; + + var setVoteImage = function(voteType, undo, object){ + var flag = undo ? "" : "-on"; + var arrow = (voteType == VoteType.questionUpVote || voteType == VoteType.answerUpVote) ? "up" : "down"; + object.attr("src", scriptUrl + "content/images/vote-arrow-"+ arrow + flag +".png"); + + // if undo voting, then undo the pair of arrows. + if(undo){ + if(voteType == VoteType.questionUpVote || voteType == VoteType.questionDownVote){ + $(getQuestionVoteUpButton()).attr("src", scriptUrl + "content/images/vote-arrow-up.png"); + $(getQuestionVoteDownButton()).attr("src", scriptUrl + "content/images/vote-arrow-down.png"); + } + else{ + $(getAnswerVoteUpButton(postId)).attr("src", scriptUrl + "content/images/vote-arrow-up.png"); + $(getAnswerVoteDownButton(postId)).attr("src", scriptUrl + "content/images/vote-arrow-down.png"); + } + } + }; + + var setVoteNumber = function(object, number){ + var voteNumber = object.parent('div.'+ voteContainerId).find('div.'+ voteNumberClass); + $(voteNumber).text(number); + }; + + var bindEvents = function(){ + // accept answers + if(questionAuthorId == currentUserId){ + var acceptedButtons = 'div.'+ voteContainerId +' img[id^='+ imgIdPrefixAccept +']'; + $(acceptedButtons).unbind('click').click(function(event){ + Vote.accept($(event.target)); + }); + } + // set favorite question + var favoriteButton = getFavoriteButton(); + favoriteButton.unbind('click').click(function(event){ + Vote.favorite($(event.target)); + }); + + // question vote up + var questionVoteUpButton = getQuestionVoteUpButton(); + questionVoteUpButton.unbind('click').click(function(event){ + Vote.vote($(event.target), VoteType.questionUpVote); + }); + + var questionVoteDownButton = getQuestionVoteDownButton(); + questionVoteDownButton.unbind('click').click(function(event){ + Vote.vote($(event.target), VoteType.questionDownVote); + }); + + var answerVoteUpButton = getAnswerVoteUpButtons(); + answerVoteUpButton.unbind('click').click(function(event){ + Vote.vote($(event.target), VoteType.answerUpVote); + }); + + var answerVoteDownButton = getAnswerVoteDownButtons(); + answerVoteDownButton.unbind('click').click(function(event){ + Vote.vote($(event.target), VoteType.answerDownVote); + }); + + getOffensiveQuestionFlag().unbind('click').click(function(event){ + Vote.offensive(this, VoteType.offensiveQuestion); + }); + + getOffensiveAnswerFlags().unbind('click').click(function(event){ + Vote.offensive(this, VoteType.offensiveAnswer); + }); + + getremoveQuestionLink().unbind('click').click(function(event){ + Vote.remove(this, VoteType.removeQuestion); + }); + + getquestionSubscribeUpdatesCheckbox().unbind('click').click(function(event){ + if (this.checked){ + Vote.vote($(event.target), VoteType.questionSubscribeUpdates); + } + else { + Vote.vote($(event.target), VoteType.questionUnsubscribeUpdates); + } + }); + + getremoveAnswersLinks().unbind('click').click(function(event){ + Vote.remove(this, VoteType.removeAnswer); + }); + }; + + var submit = function(object, voteType, callback) { + $.ajax({ + type: "POST", + cache: false, + dataType: "json", + url: scriptUrl + $.i18n._("questions/") + questionId + "/" + $.i18n._("vote/"), + data: { "type": voteType, "postId": postId }, + error: handleFail, + success: function(data){callback(object, voteType, data)}}); + }; + + var handleFail = function(xhr, msg){ + alert("Callback invoke error: " + msg); + }; + + // callback function for Accept Answer action + var callback_accept = function(object, voteType, data){ + if(data.allowed == "0" && data.success == "0"){ + showMessage(object, acceptAnonymousMessage); + } + else if(data.allowed == "-1"){ + showMessage(object, acceptOwnAnswerMessage); + } + else if(data.status == "1"){ + object.attr("src", scriptUrl + "content/images/vote-accepted.png"); + $("#"+answerContainerIdPrefix+postId).removeClass("accepted-answer"); + $("#"+commentLinkIdPrefix+postId).removeClass("comment-link-accepted"); + } + else if(data.success == "1"){ + var acceptedButtons = 'div.'+ voteContainerId +' img[id^='+ imgIdPrefixAccept +']'; + $(acceptedButtons).attr("src", scriptUrl + "content/images/vote-accepted.png"); + var answers = ("div[id^="+answerContainerIdPrefix +"]"); + $(answers).removeClass("accepted-answer"); + var commentLinks = ("div[id^="+answerContainerIdPrefix +"] div[id^="+ commentLinkIdPrefix +"]"); + $(commentLinks).removeClass("comment-link-accepted"); + + object.attr("src", scriptUrl + "content/images/vote-accepted-on.png"); + $("#"+answerContainerIdPrefix+postId).addClass("accepted-answer"); + $("#"+commentLinkIdPrefix+postId).addClass("comment-link-accepted"); + } + else{ + showMessage(object, data.message); + } + }; + + var callback_favorite = function(object, voteType, data){ + if(data.allowed == "0" && data.success == "0"){ + showMessage(object, favoriteAnonymousMessage.replace("{{QuestionID}}", questionId)); + } + else if(data.status == "1"){ + object.attr("src", scriptUrl + "content/images/vote-favorite-off.png"); + var fav = getFavoriteNumber(); + fav.removeClass("my-favorite-number"); + if(data.count == 0) + data.count = ''; + fav.text(data.count); + } + else if(data.success == "1"){ + object.attr("src", scriptUrl + "content/images/vote-favorite-on.png"); + var fav = getFavoriteNumber(); + fav.text(data.count); + fav.addClass("my-favorite-number"); + } + else{ + showMessage(object, data.message); + } + }; + + var callback_vote = function(object, voteType, data){ + if(data.allowed == "0" && data.success == "0"){ + showMessage(object, voteAnonymousMessage.replace("{{QuestionID}}", questionId)); + } + else if (data.allowed == "-3"){ + showMessage(object, voteRequiredMoreVotes); + } + else if (data.allowed == "-2"){ + if (voteType == VoteType.questionUpVote || voteType == VoteType.answerUpVote){ + showMessage(object, upVoteRequiredScoreMessage); + } + else if (voteType == VoteType.questionDownVote || voteType == VoteType.answerDownVote){ + showMessage(object, downVoteRequiredScoreMessage); + } + } + else if (data.allowed == "-1"){ + showMessage(object, voteOwnDeniedMessage); + } + else if (data.status == "2"){ + showMessage(object, voteDenyCancelMessage); + } + else if (data.status == "1"){ + setVoteImage(voteType, true, object); + setVoteNumber(object, data.count); + } + else if (data.success == "1"){ + setVoteImage(voteType, false, object); + setVoteNumber(object, data.count); + if (data.message.length > 0){ + showMessage(object, data.message); + } + } + }; + + var callback_offensive = function(object, voteType, data){ + object = $(object); + if (data.allowed == "0" && data.success == "0"){ + showMessage(object, offensiveAnonymousMessage.replace("{{QuestionID}}", questionId)); + } + else if (data.allowed == "-3"){ + showMessage(object, offensiveNoFlagsLeftMessage); + } + else if (data.allowed == "-2"){ + showMessage(object, offensiveNoPermissionMessage); + } + else if (data.status == "1"){ + showMessage(object, offensiveTwiceMessage); + } + else if (data.success == "1"){ + $(object).children('span[class=darkred]').text("("+ data.count +")"); + } + }; + + var callback_remove = function(object, voteType, data){ + if (data.allowed == "0" && data.success == "0"){ + showMessage(object, removeAnonymousMessage.replace("{{QuestionID}}", questionId)); + } + else if (data.success == "1"){ + if (voteType == VoteType.removeQuestion){ + window.location.href = scriptUrl + $.i18n._("questions/"); + } + else { + if (removeActionType == 'delete'){ + postNode.addClass('deleted'); + postRemoveLink.innerHTML = $.i18n._('undelete'); + showMessage(object, deletedMessage); + } + else if (removeActionType == 'undelete') { + postNode.removeClass('deleted'); + postRemoveLink.innerHTML = $.i18n._('delete'); + showMessage(object, recoveredMessage); + } + } + } + }; + + return { + init : function(qId, qSlug, questionAuthor, userId){ + questionId = qId; + questionSlug = qSlug; + questionAuthorId = questionAuthor; + currentUserId = userId; + bindEvents(); + }, + + // Accept answer public function + accept: function(object){ + postId = object.attr("id").substring(imgIdPrefixAccept.length); + submit(object, VoteType.acceptAnswer, callback_accept); + }, + + favorite: function(object){ + if (!currentUserId || currentUserId.toUpperCase() == "NONE"){ + showMessage(object, favoriteAnonymousMessage.replace("{{QuestionID}}", questionId)); + return false; + } + submit(object, VoteType.favorite, callback_favorite); + }, + + vote: function(object, voteType){ + if (!currentUserId || currentUserId.toUpperCase() == "NONE"){ + showMessage(object, voteAnonymousMessage.replace("{{QuestionID}}", questionId).replace("{{questionSlug}}", questionSlug)); + return false; + } + if (voteType == VoteType.answerUpVote){ + postId = object.attr("id").substring(imgIdPrefixAnswerVoteup.length); + } + else if (voteType == VoteType.answerDownVote){ + postId = object.attr("id").substring(imgIdPrefixAnswerVotedown.length); + } + + submit(object, voteType, callback_vote); + }, + + offensive: function(object, voteType){ + if (!currentUserId || currentUserId.toUpperCase() == "NONE"){ + showMessage($(object), offensiveAnonymousMessage.replace("{{QuestionID}}", questionId)); + return false; + } + if (confirm(offensiveConfirmation)){ + postId = object.id.substr(object.id.lastIndexOf('-') + 1); + submit(object, voteType, callback_offensive); + } + }, + + remove: function(object, voteType){ + if (!currentUserId || currentUserId.toUpperCase() == "NONE"){ + showMessage($(object), removeAnonymousMessage.replace("{{QuestionID}}", questionId)); + return false; + } + bits = object.id.split('-'); + postId = bits.pop();/* this seems to be used within submit! */ + postType = bits.shift(); + + var do_proceed = false; + if (postType == 'answer'){ + postNode = $('#answer-container-' + postId); + postRemoveLink = object; + if (postNode.hasClass('deleted')){ + removeActionType = 'undelete'; + do_proceed = true; + } + else { + removeActionType = 'delete'; + do_proceed = confirm(removeConfirmation); + } + } + else { + do_proceed = confirm(removeConfirmation); + } + if (do_proceed) { + submit($(object), voteType, callback_remove); + } + } + } +} (); + + +// site comments +function createComments(type) { + var objectType = type; + var jDivInit = function(id) { + return $("#comments-container-" + objectType + '-' + id); + }; + + var appendLoaderImg = function(id) { + appendLoader("#comments-container-" + objectType + '-' + id); + }; + + var canPostComments = function(id) { + var jHidden = $("#can-post-comments-" + objectType + '-' + id); + return jHidden.val().toLowerCase() == "true"; + }; + + var renderForm = function(id) { + var formId = "form-comments-" + objectType + "-" + id; + var jDiv = $('#comments-link-' + objectType + "-" + id).parent(); + $(jDiv).css('background','none'); + $(jDiv).css('padding-left',0); + if (canPostComments(id)) { + if (jDiv.find("#" + formId).length == 0) { + var form = '
'; + form += ''; + form += '
'; + form += '
'; + + jDiv.append(form); + + setupFormValidation("#" + formId, + { comment: { required: true, minlength: 10} }, '', + function() { postComment(id, formId); }); + } + } + else { + var divId = "comments-rep-needed-" + objectType + '-' + id; + if (jDiv.find("#" + divId).length == 0) { + jDiv.append('

' + + $.i18n._('to comment, need') + ' ' + + + repNeededForComments + ' ' + $.i18n._('community karma points') + + '' + + $.i18n._('please see') + 'faq

'); + } + } + }; + + var getComments = function(id, jDiv) { + //appendLoaderImg(id); + $.getJSON(scriptUrl + objectType + "s/" + id + "/" + $.i18n._("comments/") + , function(json) { showComments(id, json); }); + }; + + var showComments = function(id, json) { + var jDiv = jDivInit(id); + + //jDiv = jDiv.find("div.comments"); // this div should contain any fetched comments.. + //jDiv.find("div[id^='comment-" + objectType + "-'" + "]").remove(); // clean previous calls.. + jDiv.children().remove(); + removeLoader(); + if (json && json.length > 0) { + for (var i = 0; i < json.length; i++) + renderComment(jDiv, json[i]); + jDiv.children().show(); + } + }; + + var renderDeleteCommentIcon = function(post_id, delete_url){ + if (canPostComments(post_id)){ + var html = ''; + var img = scriptUrl + "content/images/close-small.png"; + var imgHover = scriptUrl + "content/images/close-small-hover.png"; + html += ''; + return html; + } + else{ + return ''; + } + } + + // {"Id":6,"PostId":38589,"CreationDate":"an hour ago","Text":"hello there!","UserDisplayName":"Jarrod Dixon","UserUrl":"/users/3/jarrod-dixon","DeleteUrl":null} + var renderComment = function(jDiv, json) { + var html = ''; + + jDiv.append(html); + }; + + var postComment = function(id, formId) { + //appendLoaderImg(id); + + var formSelector = "#" + formId; + var textarea = $(formSelector + " textarea"); + + //todo fix url translations!!! + $.ajax({ + type: "POST", + url: scriptUrl + objectType + "s/" + id + "/" + $.i18n._("comments/"), + dataType: "json", + data: { comment: textarea.val() }, + success: function(json) { + showComments(id, json); + textarea.val(""); + commentsFactory[objectType].updateTextCounter(textarea); + enableSubmitButton(formSelector); + }, + error: function(res, textStatus, errorThrown) { + removeLoader(); + showMessage(formSelector, res.responseText); + enableSubmitButton(formSelector); + } + }); + }; + + // public methods.. + return { + + init: function() { + // Setup "show comments" clicks.. + $("a[id^='comments-link-" + objectType + "-" + "']").unbind("click").click(function() { + commentsFactory[objectType].show($(this).attr("id").substr(("comments-link-" + objectType + "-").length)); + }); + + var cBox = $("[id^='comments-container-" + objectType + "']"); + cBox.each( function(i){ + var post_id = $(this).attr('id').replace('comments-container-' + objectType + '-', ''); + $(this).children().each( + function(i){ + var comment_id = $(this).attr('id').replace('comment-',''); + var delete_url = scriptUrl + objectType + 's/' + post_id + '/' + + $.i18n._('comments/') + comment_id + '/' + $.i18n._('delete/'); + var html = $(this).html(); + var CommentsClass; + if (objectType == 'question'){ + CommentsClass = questionComments; + } + else if (objectType == 'answer') { + CommentsClass = answerComments; + } + var delete_icon = $(this).find('img.delete-icon'); + delete_icon.click(function(){CommentsClass.deleteComment($(this),comment_id,delete_url);}); + delete_icon.unbind('mouseover').bind('mouseover', + function(){ + $(this).attr('src',scriptUrl + 'content/images/close-small-hover.png'); + } + ); + delete_icon.unbind('mouseout').bind('mouseout', + function(){ + $(this).attr('src',scriptUrl + 'content/images/close-small.png'); + } + ); + } + ); + }); + }, + + show: function(id) { + var jDiv = jDivInit(id); + getComments(id, jDiv); + renderForm(id); + jDiv.show(); + + var link = $('#comments-link-' + objectType + '-' + id); + if (canPostComments(id)) link.parent().find("textarea").get(0).focus(); + link.remove(); + }, + + hide: function(id) { + var jDiv = jDivInit(id); + var len = jDiv.children("div.comments").children().length; + var anchorText = len == 0 ? $.i18n._('add a comment') : $.i18n._('comments') + ' (' + len + ")"; + + jDiv.hide(); + jDiv.siblings("a").unbind("click").click(function() { commentsFactory[objectType].show(id); }).html(anchorText); + jDiv.children("div.comments").children().hide(); + }, + + deleteComment: function(jImg, id, deleteUrl) { + if (confirm($.i18n._('confirm delete comment'))) { + jImg.hide(); + $.post(deleteUrl, { dataNeeded: "forIIS7" }, function(json) { + var par = jImg.parent(); + par.remove(); + }, "json"); + } + }, + + updateTextCounter: function(textarea) { + var length = textarea.value ? textarea.value.length : 0; + var color = length > 270 ? "#f00" : length > 200 ? "#f60" : "#999"; + var jSpan = $(textarea).siblings("span.text-counter"); + jSpan.html($.i18n._('can write') + + (300 - length) + ' ' + + $.i18n._('characters')).css("color", color); + } + }; +} + +var questionComments = createComments('question'); +var answerComments = createComments('answer'); + +$().ready(function() { + questionComments.init(); + answerComments.init(); +}); + +var commentsFactory = {'question' : questionComments, 'answer' : answerComments}; + +/* +Prettify +http://www.apache.org/licenses/LICENSE-2.0 +*/ +var PR_SHOULD_USE_CONTINUATION = true; var PR_TAB_WIDTH = 8; var PR_normalizedHtml; var PR; var prettyPrintOne; var prettyPrint; function _pr_isIE6() { var isIE6 = navigator && navigator.userAgent && /\bMSIE 6\./.test(navigator.userAgent); _pr_isIE6 = function() { return isIE6; }; return isIE6; } (function() { function wordSet(words) { words = words.split(/ /g); var set = {}; for (var i = words.length; --i >= 0; ) { var w = words[i]; if (w) { set[w] = null; } } return set; } var FLOW_CONTROL_KEYWORDS = "break continue do else for if return while "; var C_KEYWORDS = FLOW_CONTROL_KEYWORDS + "auto case char const default " + "double enum extern float goto int long register short signed sizeof " + "static struct switch typedef union unsigned void volatile "; var COMMON_KEYWORDS = C_KEYWORDS + "catch class delete false import " + "new operator private protected public this throw true try "; var CPP_KEYWORDS = COMMON_KEYWORDS + "alignof align_union asm axiom bool " + "concept concept_map const_cast constexpr decltype " + "dynamic_cast explicit export friend inline late_check " + "mutable namespace nullptr reinterpret_cast static_assert static_cast " + "template typeid typename typeof using virtual wchar_t where "; var JAVA_KEYWORDS = COMMON_KEYWORDS + "boolean byte extends final finally implements import instanceof null " + "native package strictfp super synchronized throws transient "; var CSHARP_KEYWORDS = JAVA_KEYWORDS + "as base by checked decimal delegate descending event " + "fixed foreach from group implicit in interface internal into is lock " + "object out override orderby params readonly ref sbyte sealed " + "stackalloc string select uint ulong unchecked unsafe ushort var "; var JSCRIPT_KEYWORDS = COMMON_KEYWORDS + "debugger eval export function get null set undefined var with " + "Infinity NaN "; var PERL_KEYWORDS = "caller delete die do dump elsif eval exit foreach for " + "goto if import last local my next no our print package redo require " + "sub undef unless until use wantarray while BEGIN END "; var PYTHON_KEYWORDS = FLOW_CONTROL_KEYWORDS + "and as assert class def del " + "elif except exec finally from global import in is lambda " + "nonlocal not or pass print raise try with yield " + "False True None "; var RUBY_KEYWORDS = FLOW_CONTROL_KEYWORDS + "alias and begin case class def" + " defined elsif end ensure false in module next nil not or redo rescue " + "retry self super then true undef unless until when yield BEGIN END "; var SH_KEYWORDS = FLOW_CONTROL_KEYWORDS + "case done elif esac eval fi " + "function in local set then until "; var ALL_KEYWORDS = (CPP_KEYWORDS + CSHARP_KEYWORDS + JSCRIPT_KEYWORDS + PERL_KEYWORDS + PYTHON_KEYWORDS + RUBY_KEYWORDS + SH_KEYWORDS); var PR_STRING = 'str'; var PR_KEYWORD = 'kwd'; var PR_COMMENT = 'com'; var PR_TYPE = 'typ'; var PR_LITERAL = 'lit'; var PR_PUNCTUATION = 'pun'; var PR_PLAIN = 'pln'; var PR_TAG = 'tag'; var PR_DECLARATION = 'dec'; var PR_SOURCE = 'src'; var PR_ATTRIB_NAME = 'atn'; var PR_ATTRIB_VALUE = 'atv'; var PR_NOCODE = 'nocode'; function isWordChar(ch) { return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z'); } function spliceArrayInto(inserted, container, containerPosition, countReplaced) { inserted.unshift(containerPosition, countReplaced || 0); try { container.splice.apply(container, inserted); } finally { inserted.splice(0, 2); } } var REGEXP_PRECEDER_PATTERN = function() { var preceders = ["!", "!=", "!==", "#", "%", "%=", "&", "&&", "&&=", "&=", "(", "*", "*=", "+=", ",", "-=", "->", "/", "/=", ":", "::", ";", "<", "<<", "<<=", "<=", "=", "==", "===", ">", ">=", ">>", ">>=", ">>>", ">>>=", "?", "@", "[", "^", "^=", "^^", "^^=", "{", "|", "|=", "||", "||=", "~", "break", "case", "continue", "delete", "do", "else", "finally", "instanceof", "return", "throw", "try", "typeof"]; var pattern = '(?:' + '(?:(?:^|[^0-9.])\\.{1,3})|' + '(?:(?:^|[^\\+])\\+)|' + '(?:(?:^|[^\\-])-)'; for (var i = 0; i < preceders.length; ++i) { var preceder = preceders[i]; if (isWordChar(preceder.charAt(0))) { pattern += '|\\b' + preceder; } else { pattern += '|' + preceder.replace(/([^=<>:&])/g, '\\$1'); } } pattern += '|^)\\s*$'; return new RegExp(pattern); } (); var pr_amp = /&/g; var pr_lt = //g; var pr_quot = /\"/g; function attribToHtml(str) { return str.replace(pr_amp, '&').replace(pr_lt, '<').replace(pr_gt, '>').replace(pr_quot, '"'); } function textToHtml(str) { return str.replace(pr_amp, '&').replace(pr_lt, '<').replace(pr_gt, '>'); } var pr_ltEnt = /</g; var pr_gtEnt = />/g; var pr_aposEnt = /'/g; var pr_quotEnt = /"/g; var pr_ampEnt = /&/g; var pr_nbspEnt = / /g; function htmlToText(html) { var pos = html.indexOf('&'); if (pos < 0) { return html; } for (--pos; (pos = html.indexOf('&#', pos + 1)) >= 0; ) { var end = html.indexOf(';', pos); if (end >= 0) { var num = html.substring(pos + 3, end); var radix = 10; if (num && num.charAt(0) === 'x') { num = num.substring(1); radix = 16; } var codePoint = parseInt(num, radix); if (!isNaN(codePoint)) { html = (html.substring(0, pos) + String.fromCharCode(codePoint) + html.substring(end + 1)); } } } return html.replace(pr_ltEnt, '<').replace(pr_gtEnt, '>').replace(pr_aposEnt, "'").replace(pr_quotEnt, '"').replace(pr_ampEnt, '&').replace(pr_nbspEnt, ' '); } function isRawContent(node) { return 'XMP' === node.tagName; } function normalizedHtml(node, out) { switch (node.nodeType) { case 1: var name = node.tagName.toLowerCase(); out.push('<', name); for (var i = 0; i < node.attributes.length; ++i) { var attr = node.attributes[i]; if (!attr.specified) { continue; } out.push(' '); normalizedHtml(attr, out); } out.push('>'); for (var child = node.firstChild; child; child = child.nextSibling) { normalizedHtml(child, out); } if (node.firstChild || !/^(?:br|link|img)$/.test(name)) { out.push('<\/', name, '>'); } break; case 2: out.push(node.name.toLowerCase(), '="', attribToHtml(node.value), '"'); break; case 3: case 4: out.push(textToHtml(node.nodeValue)); break; } } var PR_innerHtmlWorks = null; function getInnerHtml(node) { if (null === PR_innerHtmlWorks) { var testNode = document.createElement('PRE'); testNode.appendChild(document.createTextNode('\n')); PR_innerHtmlWorks = !/= 0; nSpaces -= SPACES.length) { out.push(SPACES.substring(0, nSpaces)); } pos = i + 1; break; case '\n': charInLine = 0; break; default: ++charInLine; } } if (!out) { return plainText; } out.push(plainText.substring(pos)); return out.join(''); }; } var pr_chunkPattern = /(?:[^<]+|||<\/?[a-zA-Z][^>]*>|<)/g; var pr_commentPrefix = /^|$)/, null], [PR_SOURCE, /^<\?[\s\S]*?(?:\?>|$)/, null], [PR_SOURCE, /^<%[\s\S]*?(?:%>|$)/, null], [PR_SOURCE, /^<(script|style|xmp)\b[^>]*>[\s\S]*?<\/\1\b[^>]*>/i, null], [PR_TAG, /^<\/?\w[^<>]*>/, null]]); var PR_SOURCE_CHUNK_PARTS = /^(<[^>]*>)([\s\S]*)(<\/[^>]*>)$/; function tokenizeMarkup(source) { var decorations = PR_MARKUP_LEXER(source); for (var i = 0; i < decorations.length; i += 2) { if (decorations[i + 1] === PR_SOURCE) { var start, end; start = decorations[i]; end = i + 2 < decorations.length ? decorations[i + 2] : source.length; var sourceChunk = source.substring(start, end); var match = sourceChunk.match(PR_SOURCE_CHUNK_PARTS); if (match) { decorations.splice(i, 2, start, PR_TAG, start + match[1].length, PR_SOURCE, start + match[1].length + (match[2] || '').length, PR_TAG); } } } return decorations; } var PR_TAG_LEXER = createSimpleLexer([[PR_ATTRIB_VALUE, /^\'[^\']*(?:\'|$)/, null, "'"], [PR_ATTRIB_VALUE, /^\"[^\"]*(?:\"|$)/, null, '"'], [PR_PUNCTUATION, /^[<>\/=]+/, null, '<>/=']], [[PR_TAG, /^[\w:\-]+/, /^= 2 && /^[\"\']/.test(attribValue) && attribValue.charAt(0) === attribValue.charAt(attribLen - 1)); var attribSource; var attribSourceStart; var attribSourceEnd; if (quoted) { attribSourceStart = start + 1; attribSourceEnd = end - 1; attribSource = attribValue; } else { attribSourceStart = start + 1; attribSourceEnd = end - 1; attribSource = attribValue.substring(1, attribValue.length - 1); } var attribSourceDecorations = decorateSource(attribSource); for (var j = 0, m = attribSourceDecorations.length; j < m; j += 2) { attribSourceDecorations[j] += attribSourceStart; } if (quoted) { attribSourceDecorations.push(attribSourceEnd, PR_ATTRIB_VALUE); spliceArrayInto(attribSourceDecorations, decorations, i + 2, 0); } else { spliceArrayInto(attribSourceDecorations, decorations, i, 2); } } nextValueIsSource = false; } } return decorations; } function decorateMarkup(sourceCode) { var decorations = tokenizeMarkup(sourceCode); decorations = splitTagAttributes(sourceCode, decorations); decorations = splitSourceNodes(sourceCode, decorations); decorations = splitSourceAttributes(sourceCode, decorations); return decorations; } function recombineTagsAndDecorations(sourceText, extractedTags, decorations) { var html = []; var outputIdx = 0; var openDecoration = null; var currentDecoration = null; var tagPos = 0; var decPos = 0; var tabExpander = makeTabExpander(PR_TAB_WIDTH); var adjacentSpaceRe = /([\r\n ]) /g; var startOrSpaceRe = /(^| ) /gm; var newlineRe = /\r\n?|\n/g; var trailingSpaceRe = /[ \r\n]$/; var lastWasSpace = true; function emitTextUpTo(sourceIdx) { if (sourceIdx > outputIdx) { if (openDecoration && openDecoration !== currentDecoration) { html.push(''); openDecoration = null; } if (!openDecoration && currentDecoration) { openDecoration = currentDecoration; html.push(''); } var htmlChunk = textToHtml(tabExpander(sourceText.substring(outputIdx, sourceIdx))).replace(lastWasSpace ? startOrSpaceRe : adjacentSpaceRe, '$1 '); lastWasSpace = trailingSpaceRe.test(htmlChunk); html.push(htmlChunk.replace(newlineRe, '
')); outputIdx = sourceIdx; } } while (true) { var outputTag; if (tagPos < extractedTags.length) { if (decPos < decorations.length) { outputTag = extractedTags[tagPos] <= decorations[decPos]; } else { outputTag = true; } } else { outputTag = false; } if (outputTag) { emitTextUpTo(extractedTags[tagPos]); if (openDecoration) { html.push('
'); openDecoration = null; } html.push(extractedTags[tagPos + 1]); tagPos += 2; } else if (decPos < decorations.length) { emitTextUpTo(decorations[decPos]); currentDecoration = decorations[decPos + 1]; decPos += 2; } else { break; } } emitTextUpTo(sourceText.length); if (openDecoration) { html.push(''); } return html.join(''); } var langHandlerRegistry = {}; function registerLangHandler(handler, fileExtensions) { for (var i = fileExtensions.length; --i >= 0; ) { var ext = fileExtensions[i]; if (!langHandlerRegistry.hasOwnProperty(ext)) { langHandlerRegistry[ext] = handler; } else if ('console' in window) { console.log('cannot override language handler %s', ext); } } } registerLangHandler(decorateSource, ['default-code']); registerLangHandler(decorateMarkup, ['default-markup', 'html', 'htm', 'xhtml', 'xml', 'xsl']); registerLangHandler(sourceDecorator({ keywords: CPP_KEYWORDS, hashComments: true, cStyleComments: true }), ['c', 'cc', 'cpp', 'cs', 'cxx', 'cyc']); registerLangHandler(sourceDecorator({ keywords: JAVA_KEYWORDS, cStyleComments: true }), ['java']); registerLangHandler(sourceDecorator({ keywords: SH_KEYWORDS, hashComments: true, multiLineStrings: true }), ['bsh', 'csh', 'sh']); registerLangHandler(sourceDecorator({ keywords: PYTHON_KEYWORDS, hashComments: true, multiLineStrings: true, tripleQuotedStrings: true }), ['cv', 'py']); registerLangHandler(sourceDecorator({ keywords: PERL_KEYWORDS, hashComments: true, multiLineStrings: true, regexLiterals: true }), ['perl', 'pl', 'pm']); registerLangHandler(sourceDecorator({ keywords: RUBY_KEYWORDS, hashComments: true, multiLineStrings: true, regexLiterals: true }), ['rb']); registerLangHandler(sourceDecorator({ keywords: JSCRIPT_KEYWORDS, cStyleComments: true, regexLiterals: true }), ['js']); function prettyPrintOne(sourceCodeHtml, opt_langExtension) { try { var sourceAndExtractedTags = extractTags(sourceCodeHtml); var source = sourceAndExtractedTags.source; var extractedTags = sourceAndExtractedTags.tags; if (!langHandlerRegistry.hasOwnProperty(opt_langExtension)) { opt_langExtension = /^\s*= 0) { var langExtension = cs.className.match(/\blang-(\w+)\b/); if (langExtension) { langExtension = langExtension[1]; } var nested = false; for (var p = cs.parentNode; p; p = p.parentNode) { if ((p.tagName === 'pre' || p.tagName === 'code' || p.tagName === 'xmp') && p.className && p.className.indexOf('prettyprint') >= 0) { nested = true; break; } } if (!nested) { var content = getInnerHtml(cs); content = content.replace(/(?:\r\n?|\n)$/, ''); var newContent = prettyPrintOne(content, langExtension); if (!isRawContent(cs)) { cs.innerHTML = newContent; } else { var pre = document.createElement('PRE'); for (var i = 0; i < cs.attributes.length; ++i) { var a = cs.attributes[i]; if (a.specified) { var aname = a.name.toLowerCase(); if (aname === 'class') { pre.className = a.value; } else { pre.setAttribute(a.name, a.value); } } } pre.innerHTML = newContent; cs.parentNode.replaceChild(pre, cs); cs = pre; } if (isIE6 && cs.tagName === 'PRE') { var lineBreaks = cs.getElementsByTagName('br'); for (var j = lineBreaks.length; --j >= 0; ) { var lineBreak = lineBreaks[j]; lineBreak.parentNode.replaceChild(document.createTextNode('\r\n'), lineBreak); } } } } } if (k < elements.length) { setTimeout(doWork, 250); } else if (opt_whenDone) { opt_whenDone(); } } doWork(); } window['PR_normalizedHtml'] = normalizedHtml; window['prettyPrintOne'] = prettyPrintOne; window['prettyPrint'] = prettyPrint; window['PR'] = { 'createSimpleLexer': createSimpleLexer, 'registerLangHandler': registerLangHandler, 'sourceDecorator': sourceDecorator, 'PR_ATTRIB_NAME': PR_ATTRIB_NAME, 'PR_ATTRIB_VALUE': PR_ATTRIB_VALUE, 'PR_COMMENT': PR_COMMENT, 'PR_DECLARATION': PR_DECLARATION, 'PR_KEYWORD': PR_KEYWORD, 'PR_LITERAL': PR_LITERAL, 'PR_NOCODE': PR_NOCODE, 'PR_PLAIN': PR_PLAIN, 'PR_PUNCTUATION': PR_PUNCTUATION, 'PR_SOURCE': PR_SOURCE, 'PR_STRING': PR_STRING, 'PR_TAG': PR_TAG, 'PR_TYPE': PR_TYPE }; })(); -- cgit v1.2.3-1-g7c22