From a9eef437702d5df7a2f97010e6798c689371808c Mon Sep 17 00:00:00 2001 From: hernani Date: Mon, 1 Mar 2010 16:55:04 +0000 Subject: [PATCH] Initial commit git-svn-id: http://svn.osqa.net/svnroot/osqa/trunk@4 0cfe37f9-358a-4d5e-be75-b63607b5c754 --- .idea/ant.xml | 7 + .idea/compiler.xml | 24 + .idea/copyright/profiles_settings.xml | 5 + .idea/encodings.xml | 7 + .idea/misc.xml | 52 + .idea/modules.xml | 9 + .idea/uiDesigner.xml | 125 + .idea/vcs.xml | 7 + .idea/workspace.xml | 654 +++ .project | 23 + .pydevproject | 10 + HOW_TO_DEBUG | 39 + INSTALL | 314 ++ LICENSE | 14 + PENDING | 28 + README | 6 + ROADMAP.rst | 32 + WISH_LIST | 10 + __init__.py | 0 context.py | 47 + cron/send_email_alerts | 4 + dos2unix.sh | 12 + forum/__init__.py | 1 + forum/admin.py | 74 + forum/auth.py | 498 +++ forum/authentication/__init__.py | 27 + forum/authentication/base.py | 40 + forum/authentication/forms.py | 31 + forum/const.py | 92 + forum/feed.py | 43 + forum/forms.py | 359 ++ forum/management/__init__.py | 3 + forum/management/commands/__init__.py | 0 forum/management/commands/base_command.py | 35 + .../management/commands/clean_award_badges.py | 59 + .../commands/message_to_everyone.py | 12 + .../management/commands/multi_award_badges.py | 348 ++ .../management/commands/once_award_badges.py | 350 ++ forum/management/commands/sample_command.py | 7 + .../management/commands/send_email_alerts.py | 192 + .../management/commands/subscribe_everyone.py | 32 + forum/middleware/__init__.py | 0 forum/middleware/anon_user.py | 35 + forum/middleware/cancel.py | 15 + forum/middleware/pagesize.py | 33 + forum/models/__init__.py | 343 ++ forum/models/answer.py | 134 + forum/models/base.py | 139 + forum/models/meta.py | 89 + forum/models/question.py | 336 ++ forum/models/repute.py | 109 + forum/models/tag.py | 85 + forum/models/user.py | 77 + forum/modules.py | 79 + forum/sitemap.py | 14 + forum/skins/README | 22 + forum/skins/__init__.py | 57 + forum/skins/common/media/README | 1 + .../media/images/blue-up-arrow-h18px.png | Bin 0 -> 593 bytes .../skins/default/media/images/box-arrow.gif | Bin 0 -> 69 bytes .../default/media/images/bullet_green.gif | Bin 0 -> 64 bytes forum/skins/default/media/images/cc-88x31.png | Bin 0 -> 5460 bytes forum/skins/default/media/images/cc-wiki.png | Bin 0 -> 2333 bytes .../default/media/images/close-small-dark.png | Bin 0 -> 226 bytes .../media/images/close-small-hover.png | Bin 0 -> 337 bytes .../default/media/images/close-small.png | Bin 0 -> 293 bytes forum/skins/default/media/images/dash.gif | Bin 0 -> 44 bytes .../media/images/djangomade124x25_grey.gif | Bin 0 -> 2035 bytes forum/skins/default/media/images/dot-g.gif | Bin 0 -> 61 bytes forum/skins/default/media/images/dot-list.gif | Bin 0 -> 56 bytes forum/skins/default/media/images/edit.png | Bin 0 -> 758 bytes .../media/images/expander-arrow-hide.gif | Bin 0 -> 126 bytes .../media/images/expander-arrow-show.gif | Bin 0 -> 135 bytes forum/skins/default/media/images/favicon.gif | Bin 0 -> 3918 bytes .../default/media/images/feed-icon-small.png | Bin 0 -> 689 bytes .../media/images/gray-up-arrow-h18px.png | Bin 0 -> 383 bytes forum/skins/default/media/images/grippie.png | Bin 0 -> 162 bytes .../skins/default/media/images/indicator.gif | Bin 0 -> 2545 bytes forum/skins/default/media/images/logo.gif | Bin 0 -> 2114 bytes forum/skins/default/media/images/logo.png | Bin 0 -> 2081 bytes forum/skins/default/media/images/logo1.png | Bin 0 -> 2752 bytes forum/skins/default/media/images/logo2.png | Bin 0 -> 2124 bytes forum/skins/default/media/images/medala.gif | Bin 0 -> 801 bytes .../skins/default/media/images/medala_on.gif | Bin 0 -> 957 bytes forum/skins/default/media/images/new.gif | Bin 0 -> 635 bytes forum/skins/default/media/images/nophoto.png | Bin 0 -> 696 bytes forum/skins/default/media/images/openid.gif | Bin 0 -> 910 bytes .../skins/default/media/images/openid/aol.gif | Bin 0 -> 2205 bytes .../default/media/images/openid/blogger.ico | Bin 0 -> 3638 bytes .../default/media/images/openid/claimid.ico | Bin 0 -> 3638 bytes .../default/media/images/openid/facebook.gif | Bin 0 -> 2075 bytes .../default/media/images/openid/flickr.ico | Bin 0 -> 1150 bytes .../default/media/images/openid/google.gif | Bin 0 -> 1596 bytes .../media/images/openid/livejournal.ico | Bin 0 -> 5222 bytes .../default/media/images/openid/myopenid.ico | Bin 0 -> 2862 bytes .../media/images/openid/openid-inputicon.gif | Bin 0 -> 237 bytes .../default/media/images/openid/openid.gif | Bin 0 -> 740 bytes .../media/images/openid/technorati.ico | Bin 0 -> 2294 bytes .../default/media/images/openid/twitter.png | Bin 0 -> 3130 bytes .../default/media/images/openid/verisign.ico | Bin 0 -> 4710 bytes .../default/media/images/openid/vidoop.ico | Bin 0 -> 1406 bytes .../default/media/images/openid/wordpress.ico | Bin 0 -> 1150 bytes .../default/media/images/openid/yahoo.gif | Bin 0 -> 1510 bytes forum/skins/default/media/images/quest-bg.gif | Bin 0 -> 294 bytes .../default/media/images/vote-accepted-on.png | Bin 0 -> 1124 bytes .../default/media/images/vote-accepted.png | Bin 0 -> 1058 bytes .../media/images/vote-arrow-down-on.png | Bin 0 -> 905 bytes .../default/media/images/vote-arrow-down.png | Bin 0 -> 876 bytes .../default/media/images/vote-arrow-up-on.png | Bin 0 -> 906 bytes .../default/media/images/vote-arrow-up.png | Bin 0 -> 843 bytes .../media/images/vote-favorite-off.png | Bin 0 -> 930 bytes .../default/media/images/vote-favorite-on.png | Bin 0 -> 1023 bytes .../media/jquery-openid/images/aol.gif | Bin 0 -> 2205 bytes .../media/jquery-openid/images/blogger-1.png | Bin 0 -> 432 bytes .../media/jquery-openid/images/blogger.ico | Bin 0 -> 3638 bytes .../media/jquery-openid/images/claimid-0.png | Bin 0 -> 629 bytes .../media/jquery-openid/images/claimid.ico | Bin 0 -> 3638 bytes .../media/jquery-openid/images/facebook.gif | Bin 0 -> 2075 bytes .../media/jquery-openid/images/flickr.ico | Bin 0 -> 1150 bytes .../media/jquery-openid/images/flickr.png | Bin 0 -> 426 bytes .../media/jquery-openid/images/google.gif | Bin 0 -> 1596 bytes .../jquery-openid/images/livejournal-1.png | Bin 0 -> 713 bytes .../jquery-openid/images/livejournal.ico | Bin 0 -> 5222 bytes .../media/jquery-openid/images/myopenid-2.png | Bin 0 -> 511 bytes .../media/jquery-openid/images/myopenid.ico | Bin 0 -> 2862 bytes .../jquery-openid/images/openid-inputicon.gif | Bin 0 -> 237 bytes .../media/jquery-openid/images/openid.gif | Bin 0 -> 740 bytes .../media/jquery-openid/images/openidico.png | Bin 0 -> 654 bytes .../jquery-openid/images/openidico16.png | Bin 0 -> 554 bytes .../jquery-openid/images/technorati-1.png | Bin 0 -> 606 bytes .../media/jquery-openid/images/technorati.ico | Bin 0 -> 2294 bytes .../media/jquery-openid/images/verisign-2.png | Bin 0 -> 859 bytes .../media/jquery-openid/images/verisign.ico | Bin 0 -> 4710 bytes .../media/jquery-openid/images/vidoop.ico | Bin 0 -> 1406 bytes .../media/jquery-openid/images/vidoop.png | Bin 0 -> 499 bytes .../media/jquery-openid/images/wordpress.ico | Bin 0 -> 1150 bytes .../media/jquery-openid/images/wordpress.png | Bin 0 -> 566 bytes .../media/jquery-openid/images/yahoo.gif | Bin 0 -> 1682 bytes .../media/jquery-openid/jquery.openid.js | 111 + .../default/media/jquery-openid/openid.css | 75 + .../default/media/js/com.cnprog.admin.js | 13 + .../default/media/js/com.cnprog.editor.js | 68 + .../skins/default/media/js/com.cnprog.i18n.js | 159 + .../skins/default/media/js/com.cnprog.post.js | 691 ++++ .../media/js/com.cnprog.tag_selector.js | 171 + .../default/media/js/com.cnprog.utils.js | 132 + forum/skins/default/media/js/compress.bat | 5 + forum/skins/default/media/js/excanvas.pack.js | 1 + forum/skins/default/media/js/flot-build.bat | 3 + forum/skins/default/media/js/jquery-1.2.6.js | 3549 +++++++++++++++++ .../default/media/js/jquery-1.2.6.min.js | 32 + .../default/media/js/jquery.ajaxfileupload.js | 195 + forum/skins/default/media/js/jquery.flot.js | 2421 +++++++++++ .../default/media/js/jquery.flot.pack.js | 1 + forum/skins/default/media/js/jquery.form.js | 654 +++ forum/skins/default/media/js/jquery.i18n.js | 133 + forum/skins/default/media/js/jquery.openid.js | 176 + .../default/media/js/jquery.validate.pack.js | 15 + forum/skins/default/media/js/se_hilite.js | 1 + forum/skins/default/media/js/se_hilite_src.js | 273 ++ .../media/js/wmd/images/wmd-buttons.png | Bin 0 -> 7465 bytes .../default/media/js/wmd/showdown-min.js | 1 + forum/skins/default/media/js/wmd/showdown.js | 1309 ++++++ forum/skins/default/media/js/wmd/wmd-min.js | 1 + .../skins/default/media/js/wmd/wmd-test.html | 158 + forum/skins/default/media/js/wmd/wmd.css | 129 + forum/skins/default/media/js/wmd/wmd.js | 2388 +++++++++++ .../default/media/js/yuicompressor-2.4.2.jar | Bin 0 -> 851219 bytes forum/skins/default/media/style/auth.css | 48 + forum/skins/default/media/style/default.css | 1754 ++++++++ .../media/style/jquery.autocomplete.css | 49 + forum/skins/default/media/style/openid.css | 45 + forum/skins/default/media/style/prettify.css | 27 + forum/skins/default/media/style/style.css | 2459 ++++++++++++ forum/skins/default/templates/404.html | 49 + forum/skins/default/templates/500.html | 35 + forum/skins/default/templates/about.html | 36 + .../default/templates/account_settings.html | 45 + .../skins/default/templates/answer_edit.html | 85 + .../default/templates/answer_edit_tips.html | 55 + forum/skins/default/templates/ask.html | 134 + .../default/templates/auth/complete.html | 95 + .../skins/default/templates/auth/signin.html | 161 + .../skins/default/templates/auth/signup.html | 32 + forum/skins/default/templates/badge.html | 37 + forum/skins/default/templates/badges.html | 76 + forum/skins/default/templates/base.html | 83 + .../skins/default/templates/base_content.html | 74 + forum/skins/default/templates/book.html | 152 + forum/skins/default/templates/changepw.html | 18 + forum/skins/default/templates/close.html | 36 + .../templates/edit_user_email_feeds_form.html | 4 + forum/skins/default/templates/faq.html | 146 + .../templates/fbconnect/xd_receiver.html | 10 + forum/skins/default/templates/feedback.html | 55 + .../default/templates/feedback_email.txt | 19 + .../templates/feeds/rss_description.html | 1 + .../default/templates/feeds/rss_title.html | 1 + forum/skins/default/templates/footer.html | 48 + forum/skins/default/templates/header.html | 65 + forum/skins/default/templates/index.html | 124 + forum/skins/default/templates/logout.html | 23 + forum/skins/default/templates/notarobot.html | 15 + forum/skins/default/templates/pagesize.html | 27 + forum/skins/default/templates/paginator.html | 38 + .../templates/post_contributor_info.html | 55 + forum/skins/default/templates/privacy.html | 42 + forum/skins/default/templates/question.html | 508 +++ .../default/templates/question_edit.html | 131 + .../default/templates/question_edit_tips.html | 53 + .../default/templates/question_retag.html | 106 + .../templates/question_summary_list_roll.html | 55 + forum/skins/default/templates/questions.html | 235 ++ forum/skins/default/templates/reopen.html | 37 + .../default/templates/revisions_answer.html | 83 + .../default/templates/revisions_question.html | 83 + .../skins/default/templates/tag_selector.html | 42 + forum/skins/default/templates/tags.html | 67 + forum/skins/default/templates/user.html | 39 + forum/skins/default/templates/user_edit.html | 95 + .../templates/user_email_subscriptions.html | 26 + .../default/templates/user_favorites.html | 8 + .../skins/default/templates/user_footer.html | 4 + forum/skins/default/templates/user_info.html | 116 + .../skins/default/templates/user_recent.html | 26 + .../default/templates/user_reputation.html | 42 + .../default/templates/user_responses.html | 23 + forum/skins/default/templates/user_stats.html | 138 + forum/skins/default/templates/user_tabs.html | 32 + forum/skins/default/templates/user_votes.html | 32 + forum/skins/default/templates/users.html | 73 + .../default/templates/users_questions.html | 66 + forum/templatetags/__init__.py | 0 forum/templatetags/extra_filters.py | 98 + forum/templatetags/extra_tags.py | 357 ++ forum/templatetags/smart_if.py | 401 ++ forum/upfiles/README | 2 + forum/urls.py | 114 + forum/user_messages/__init__.py | 36 + forum/user_messages/context_processors.py | 52 + forum/utils/__init__.py | 0 forum/utils/cache.py | 92 + forum/utils/decorators.py | 25 + forum/utils/diff.py | 66 + forum/utils/forms.py | 151 + forum/utils/html.py | 51 + forum/utils/lists.py | 86 + forum/utils/odict.py | 1399 +++++++ forum/views/README | 12 + forum/views/__init__.py | 6 + forum/views/auth.py | 212 + forum/views/commands.py | 335 ++ forum/views/meta.py | 91 + forum/views/readers.py | 588 +++ forum/views/users.py | 1009 +++++ forum/views/writers.py | 442 ++ forum_modules/__init__.py | 0 forum_modules/books/__init__.py | 3 + forum_modules/books/models.py | 63 + forum_modules/books/urls.py | 10 + forum_modules/books/views.py | 142 + forum_modules/facebookauth/__init__.py | 0 forum_modules/facebookauth/authentication.py | 85 + forum_modules/facebookauth/settings.py | 3 + .../facebookauth/templates/button.html | 38 + .../facebookauth/templates/xd_receiver.html | 1 + forum_modules/facebookauth/urls.py | 9 + forum_modules/facebookauth/views.py | 11 + forum_modules/localauth/__init__.py | 0 forum_modules/localauth/authentication.py | 18 + forum_modules/localauth/forms.py | 77 + .../localauth/templates/loginform.html | 31 + forum_modules/localauth/urls.py | 8 + forum_modules/localauth/views.py | 30 + forum_modules/oauthauth/__init__.py | 0 forum_modules/oauthauth/authentication.py | 41 + forum_modules/oauthauth/consumer.py | 87 + forum_modules/oauthauth/lib/__init__.py | 0 forum_modules/oauthauth/lib/oauth.py | 594 +++ forum_modules/oauthauth/settings.py | 3 + forum_modules/openidauth/__init__.py | 0 forum_modules/openidauth/authentication.py | 196 + forum_modules/openidauth/consumer.py | 112 + forum_modules/openidauth/models.py | 26 + forum_modules/openidauth/settings.py | 9 + forum_modules/openidauth/store.py | 79 + .../openidauth/templates/openidurl.html | 20 + forum_modules/pgfulltext/DISABLED | 0 forum_modules/pgfulltext/__init__.py | 9 + forum_modules/pgfulltext/handlers.py | 11 + forum_modules/pgfulltext/management.py | 29 + forum_modules/pgfulltext/pg_fts_install.sql | 38 + forum_modules/sphinxfulltext/DISABLED | 0 forum_modules/sphinxfulltext/__init__.py | 0 forum_modules/sphinxfulltext/dependencies.py | 2 + forum_modules/sphinxfulltext/handlers.py | 4 + forum_modules/sphinxfulltext/models.py | 10 + forum_modules/sphinxfulltext/settings.py | 5 + locale/en/LC_MESSAGES/django.mo | Bin 0 -> 26986 bytes locale/en/LC_MESSAGES/django.po | 3496 ++++++++++++++++ locale/es/LC_MESSAGES/django.mo | Bin 0 -> 49713 bytes locale/es/LC_MESSAGES/django.po | 2695 +++++++++++++ locale/zh_CN/LC_MESSAGES/django.mo | Bin 0 -> 37880 bytes locale/zh_CN/LC_MESSAGES/django.po | 2418 +++++++++++ log/django.osqa.log | 1957 +++++++++ manage.py | 11 + osqa.iml | 19 + osqa.wsgi.dist | 7 + rmpyc | 1 + settings.py | 101 + settings_local.py | 110 + settings_local.py.dist | 110 + sphinx/sphinx.conf | 127 + sql_scripts/091111_upgrade_evgeny.sql | 1 + sql_scripts/091208_upgrade_evgeny.sql | 1 + sql_scripts/091208_upgrade_evgeny_1.sql | 1 + sql_scripts/100108_upgrade_ef.sql | 4 + sql_scripts/badges.sql | 37 + sql_scripts/cnprog.xml | 1498 +++++++ sql_scripts/cnprog_new_install.sql | 811 ++++ sql_scripts/cnprog_new_install_2009_02_28.sql | 456 +++ sql_scripts/cnprog_new_install_2009_03_31.sql | 891 +++++ sql_scripts/cnprog_new_install_2009_04_07.sql | 24 + sql_scripts/cnprog_new_install_2009_04_09.sql | 904 +++++ sql_scripts/drop-all-tables.sh | 4 + sql_scripts/drop-auth.sql | 8 + sql_scripts/pg_fts_install.sql | 38 + sql_scripts/update_2009_01_13_001.sql | 62 + sql_scripts/update_2009_01_13_002.sql | 1 + sql_scripts/update_2009_01_18_001.sql | 62 + sql_scripts/update_2009_01_24.sql | 2 + sql_scripts/update_2009_01_25_001.sql | 2 + sql_scripts/update_2009_02_26_001.sql | 19 + sql_scripts/update_2009_04_10_001.sql | 3 + sql_scripts/update_2009_07_05_EF.sql | 3 + sql_scripts/update_2009_12_24_001.sql | 5 + sql_scripts/update_2009_12_27_001.sql | 3 + sql_scripts/update_2009_12_27_002.sql | 1 + sql_scripts/update_2010_01_23.sql | 9 + sql_scripts/update_2010_02_22.sql | 1 + urls.py | 11 + 341 files changed, 49786 insertions(+) create mode 100755 .idea/ant.xml create mode 100755 .idea/compiler.xml create mode 100755 .idea/copyright/profiles_settings.xml create mode 100755 .idea/encodings.xml create mode 100755 .idea/misc.xml create mode 100755 .idea/modules.xml create mode 100755 .idea/uiDesigner.xml create mode 100755 .idea/vcs.xml create mode 100755 .idea/workspace.xml create mode 100644 .project create mode 100644 .pydevproject create mode 100644 HOW_TO_DEBUG create mode 100644 INSTALL create mode 100644 LICENSE create mode 100644 PENDING create mode 100644 README create mode 100644 ROADMAP.rst create mode 100644 WISH_LIST create mode 100644 __init__.py create mode 100644 context.py create mode 100644 cron/send_email_alerts create mode 100644 dos2unix.sh create mode 100644 forum/__init__.py create mode 100644 forum/admin.py create mode 100644 forum/auth.py create mode 100755 forum/authentication/__init__.py create mode 100755 forum/authentication/base.py create mode 100755 forum/authentication/forms.py create mode 100644 forum/const.py create mode 100644 forum/feed.py create mode 100644 forum/forms.py create mode 100644 forum/management/__init__.py create mode 100644 forum/management/commands/__init__.py create mode 100644 forum/management/commands/base_command.py create mode 100644 forum/management/commands/clean_award_badges.py create mode 100644 forum/management/commands/message_to_everyone.py create mode 100644 forum/management/commands/multi_award_badges.py create mode 100644 forum/management/commands/once_award_badges.py create mode 100644 forum/management/commands/sample_command.py create mode 100644 forum/management/commands/send_email_alerts.py create mode 100644 forum/management/commands/subscribe_everyone.py create mode 100644 forum/middleware/__init__.py create mode 100644 forum/middleware/anon_user.py create mode 100644 forum/middleware/cancel.py create mode 100644 forum/middleware/pagesize.py create mode 100755 forum/models/__init__.py create mode 100755 forum/models/answer.py create mode 100755 forum/models/base.py create mode 100755 forum/models/meta.py create mode 100755 forum/models/question.py create mode 100755 forum/models/repute.py create mode 100755 forum/models/tag.py create mode 100755 forum/models/user.py create mode 100755 forum/modules.py create mode 100644 forum/sitemap.py create mode 100644 forum/skins/README create mode 100644 forum/skins/__init__.py create mode 100644 forum/skins/common/media/README create mode 100644 forum/skins/default/media/images/blue-up-arrow-h18px.png create mode 100644 forum/skins/default/media/images/box-arrow.gif create mode 100644 forum/skins/default/media/images/bullet_green.gif create mode 100644 forum/skins/default/media/images/cc-88x31.png create mode 100644 forum/skins/default/media/images/cc-wiki.png create mode 100644 forum/skins/default/media/images/close-small-dark.png create mode 100644 forum/skins/default/media/images/close-small-hover.png create mode 100644 forum/skins/default/media/images/close-small.png create mode 100644 forum/skins/default/media/images/dash.gif create mode 100644 forum/skins/default/media/images/djangomade124x25_grey.gif create mode 100644 forum/skins/default/media/images/dot-g.gif create mode 100644 forum/skins/default/media/images/dot-list.gif create mode 100644 forum/skins/default/media/images/edit.png create mode 100644 forum/skins/default/media/images/expander-arrow-hide.gif create mode 100644 forum/skins/default/media/images/expander-arrow-show.gif create mode 100644 forum/skins/default/media/images/favicon.gif create mode 100644 forum/skins/default/media/images/feed-icon-small.png create mode 100644 forum/skins/default/media/images/gray-up-arrow-h18px.png create mode 100644 forum/skins/default/media/images/grippie.png create mode 100644 forum/skins/default/media/images/indicator.gif create mode 100644 forum/skins/default/media/images/logo.gif create mode 100644 forum/skins/default/media/images/logo.png create mode 100644 forum/skins/default/media/images/logo1.png create mode 100644 forum/skins/default/media/images/logo2.png create mode 100644 forum/skins/default/media/images/medala.gif create mode 100644 forum/skins/default/media/images/medala_on.gif create mode 100644 forum/skins/default/media/images/new.gif create mode 100644 forum/skins/default/media/images/nophoto.png create mode 100644 forum/skins/default/media/images/openid.gif create mode 100644 forum/skins/default/media/images/openid/aol.gif create mode 100644 forum/skins/default/media/images/openid/blogger.ico create mode 100644 forum/skins/default/media/images/openid/claimid.ico create mode 100644 forum/skins/default/media/images/openid/facebook.gif create mode 100644 forum/skins/default/media/images/openid/flickr.ico create mode 100644 forum/skins/default/media/images/openid/google.gif create mode 100644 forum/skins/default/media/images/openid/livejournal.ico create mode 100644 forum/skins/default/media/images/openid/myopenid.ico create mode 100644 forum/skins/default/media/images/openid/openid-inputicon.gif create mode 100644 forum/skins/default/media/images/openid/openid.gif create mode 100644 forum/skins/default/media/images/openid/technorati.ico create mode 100755 forum/skins/default/media/images/openid/twitter.png create mode 100644 forum/skins/default/media/images/openid/verisign.ico create mode 100644 forum/skins/default/media/images/openid/vidoop.ico create mode 100644 forum/skins/default/media/images/openid/wordpress.ico create mode 100644 forum/skins/default/media/images/openid/yahoo.gif create mode 100644 forum/skins/default/media/images/quest-bg.gif create mode 100644 forum/skins/default/media/images/vote-accepted-on.png create mode 100644 forum/skins/default/media/images/vote-accepted.png create mode 100644 forum/skins/default/media/images/vote-arrow-down-on.png create mode 100644 forum/skins/default/media/images/vote-arrow-down.png create mode 100644 forum/skins/default/media/images/vote-arrow-up-on.png create mode 100644 forum/skins/default/media/images/vote-arrow-up.png create mode 100644 forum/skins/default/media/images/vote-favorite-off.png create mode 100644 forum/skins/default/media/images/vote-favorite-on.png create mode 100644 forum/skins/default/media/jquery-openid/images/aol.gif create mode 100644 forum/skins/default/media/jquery-openid/images/blogger-1.png create mode 100644 forum/skins/default/media/jquery-openid/images/blogger.ico create mode 100644 forum/skins/default/media/jquery-openid/images/claimid-0.png create mode 100644 forum/skins/default/media/jquery-openid/images/claimid.ico create mode 100644 forum/skins/default/media/jquery-openid/images/facebook.gif create mode 100644 forum/skins/default/media/jquery-openid/images/flickr.ico create mode 100644 forum/skins/default/media/jquery-openid/images/flickr.png create mode 100644 forum/skins/default/media/jquery-openid/images/google.gif create mode 100644 forum/skins/default/media/jquery-openid/images/livejournal-1.png create mode 100644 forum/skins/default/media/jquery-openid/images/livejournal.ico create mode 100644 forum/skins/default/media/jquery-openid/images/myopenid-2.png create mode 100644 forum/skins/default/media/jquery-openid/images/myopenid.ico create mode 100644 forum/skins/default/media/jquery-openid/images/openid-inputicon.gif create mode 100644 forum/skins/default/media/jquery-openid/images/openid.gif create mode 100644 forum/skins/default/media/jquery-openid/images/openidico.png create mode 100644 forum/skins/default/media/jquery-openid/images/openidico16.png create mode 100644 forum/skins/default/media/jquery-openid/images/technorati-1.png create mode 100644 forum/skins/default/media/jquery-openid/images/technorati.ico create mode 100644 forum/skins/default/media/jquery-openid/images/verisign-2.png create mode 100644 forum/skins/default/media/jquery-openid/images/verisign.ico create mode 100644 forum/skins/default/media/jquery-openid/images/vidoop.ico create mode 100644 forum/skins/default/media/jquery-openid/images/vidoop.png create mode 100644 forum/skins/default/media/jquery-openid/images/wordpress.ico create mode 100644 forum/skins/default/media/jquery-openid/images/wordpress.png create mode 100644 forum/skins/default/media/jquery-openid/images/yahoo.gif create mode 100644 forum/skins/default/media/jquery-openid/jquery.openid.js create mode 100644 forum/skins/default/media/jquery-openid/openid.css create mode 100644 forum/skins/default/media/js/com.cnprog.admin.js create mode 100644 forum/skins/default/media/js/com.cnprog.editor.js create mode 100644 forum/skins/default/media/js/com.cnprog.i18n.js create mode 100644 forum/skins/default/media/js/com.cnprog.post.js create mode 100644 forum/skins/default/media/js/com.cnprog.tag_selector.js create mode 100644 forum/skins/default/media/js/com.cnprog.utils.js create mode 100644 forum/skins/default/media/js/compress.bat create mode 100644 forum/skins/default/media/js/excanvas.pack.js create mode 100644 forum/skins/default/media/js/flot-build.bat create mode 100644 forum/skins/default/media/js/jquery-1.2.6.js create mode 100644 forum/skins/default/media/js/jquery-1.2.6.min.js create mode 100644 forum/skins/default/media/js/jquery.ajaxfileupload.js create mode 100644 forum/skins/default/media/js/jquery.flot.js create mode 100644 forum/skins/default/media/js/jquery.flot.pack.js create mode 100644 forum/skins/default/media/js/jquery.form.js create mode 100644 forum/skins/default/media/js/jquery.i18n.js create mode 100644 forum/skins/default/media/js/jquery.openid.js create mode 100644 forum/skins/default/media/js/jquery.validate.pack.js create mode 100644 forum/skins/default/media/js/se_hilite.js create mode 100644 forum/skins/default/media/js/se_hilite_src.js create mode 100644 forum/skins/default/media/js/wmd/images/wmd-buttons.png create mode 100644 forum/skins/default/media/js/wmd/showdown-min.js create mode 100644 forum/skins/default/media/js/wmd/showdown.js create mode 100644 forum/skins/default/media/js/wmd/wmd-min.js create mode 100644 forum/skins/default/media/js/wmd/wmd-test.html create mode 100644 forum/skins/default/media/js/wmd/wmd.css create mode 100644 forum/skins/default/media/js/wmd/wmd.js create mode 100644 forum/skins/default/media/js/yuicompressor-2.4.2.jar create mode 100755 forum/skins/default/media/style/auth.css create mode 100644 forum/skins/default/media/style/default.css create mode 100644 forum/skins/default/media/style/jquery.autocomplete.css create mode 100644 forum/skins/default/media/style/openid.css create mode 100644 forum/skins/default/media/style/prettify.css create mode 100644 forum/skins/default/media/style/style.css create mode 100644 forum/skins/default/templates/404.html create mode 100644 forum/skins/default/templates/500.html create mode 100644 forum/skins/default/templates/about.html create mode 100755 forum/skins/default/templates/account_settings.html create mode 100644 forum/skins/default/templates/answer_edit.html create mode 100644 forum/skins/default/templates/answer_edit_tips.html create mode 100644 forum/skins/default/templates/ask.html create mode 100755 forum/skins/default/templates/auth/complete.html create mode 100755 forum/skins/default/templates/auth/signin.html create mode 100755 forum/skins/default/templates/auth/signup.html create mode 100644 forum/skins/default/templates/badge.html create mode 100644 forum/skins/default/templates/badges.html create mode 100755 forum/skins/default/templates/base.html create mode 100644 forum/skins/default/templates/base_content.html create mode 100644 forum/skins/default/templates/book.html create mode 100755 forum/skins/default/templates/changepw.html create mode 100644 forum/skins/default/templates/close.html create mode 100644 forum/skins/default/templates/edit_user_email_feeds_form.html create mode 100644 forum/skins/default/templates/faq.html create mode 100755 forum/skins/default/templates/fbconnect/xd_receiver.html create mode 100644 forum/skins/default/templates/feedback.html create mode 100644 forum/skins/default/templates/feedback_email.txt create mode 100644 forum/skins/default/templates/feeds/rss_description.html create mode 100644 forum/skins/default/templates/feeds/rss_title.html create mode 100644 forum/skins/default/templates/footer.html create mode 100644 forum/skins/default/templates/header.html create mode 100755 forum/skins/default/templates/index.html create mode 100644 forum/skins/default/templates/logout.html create mode 100644 forum/skins/default/templates/notarobot.html create mode 100644 forum/skins/default/templates/pagesize.html create mode 100644 forum/skins/default/templates/paginator.html create mode 100644 forum/skins/default/templates/post_contributor_info.html create mode 100644 forum/skins/default/templates/privacy.html create mode 100644 forum/skins/default/templates/question.html create mode 100644 forum/skins/default/templates/question_edit.html create mode 100644 forum/skins/default/templates/question_edit_tips.html create mode 100644 forum/skins/default/templates/question_retag.html create mode 100644 forum/skins/default/templates/question_summary_list_roll.html create mode 100644 forum/skins/default/templates/questions.html create mode 100644 forum/skins/default/templates/reopen.html create mode 100644 forum/skins/default/templates/revisions_answer.html create mode 100644 forum/skins/default/templates/revisions_question.html create mode 100644 forum/skins/default/templates/tag_selector.html create mode 100644 forum/skins/default/templates/tags.html create mode 100644 forum/skins/default/templates/user.html create mode 100644 forum/skins/default/templates/user_edit.html create mode 100644 forum/skins/default/templates/user_email_subscriptions.html create mode 100644 forum/skins/default/templates/user_favorites.html create mode 100644 forum/skins/default/templates/user_footer.html create mode 100644 forum/skins/default/templates/user_info.html create mode 100644 forum/skins/default/templates/user_recent.html create mode 100644 forum/skins/default/templates/user_reputation.html create mode 100644 forum/skins/default/templates/user_responses.html create mode 100644 forum/skins/default/templates/user_stats.html create mode 100644 forum/skins/default/templates/user_tabs.html create mode 100644 forum/skins/default/templates/user_votes.html create mode 100644 forum/skins/default/templates/users.html create mode 100644 forum/skins/default/templates/users_questions.html create mode 100644 forum/templatetags/__init__.py create mode 100644 forum/templatetags/extra_filters.py create mode 100644 forum/templatetags/extra_tags.py create mode 100644 forum/templatetags/smart_if.py create mode 100644 forum/upfiles/README create mode 100644 forum/urls.py create mode 100644 forum/user_messages/__init__.py create mode 100644 forum/user_messages/context_processors.py create mode 100644 forum/utils/__init__.py create mode 100644 forum/utils/cache.py create mode 100644 forum/utils/decorators.py create mode 100644 forum/utils/diff.py create mode 100644 forum/utils/forms.py create mode 100644 forum/utils/html.py create mode 100644 forum/utils/lists.py create mode 100644 forum/utils/odict.py create mode 100644 forum/views/README create mode 100644 forum/views/__init__.py create mode 100755 forum/views/auth.py create mode 100644 forum/views/commands.py create mode 100644 forum/views/meta.py create mode 100644 forum/views/readers.py create mode 100644 forum/views/users.py create mode 100644 forum/views/writers.py create mode 100755 forum_modules/__init__.py create mode 100755 forum_modules/books/__init__.py create mode 100755 forum_modules/books/models.py create mode 100755 forum_modules/books/urls.py create mode 100755 forum_modules/books/views.py create mode 100755 forum_modules/facebookauth/__init__.py create mode 100755 forum_modules/facebookauth/authentication.py create mode 100755 forum_modules/facebookauth/settings.py create mode 100755 forum_modules/facebookauth/templates/button.html create mode 100755 forum_modules/facebookauth/templates/xd_receiver.html create mode 100755 forum_modules/facebookauth/urls.py create mode 100755 forum_modules/facebookauth/views.py create mode 100755 forum_modules/localauth/__init__.py create mode 100755 forum_modules/localauth/authentication.py create mode 100755 forum_modules/localauth/forms.py create mode 100755 forum_modules/localauth/templates/loginform.html create mode 100755 forum_modules/localauth/urls.py create mode 100755 forum_modules/localauth/views.py create mode 100755 forum_modules/oauthauth/__init__.py create mode 100755 forum_modules/oauthauth/authentication.py create mode 100755 forum_modules/oauthauth/consumer.py create mode 100755 forum_modules/oauthauth/lib/__init__.py create mode 100755 forum_modules/oauthauth/lib/oauth.py create mode 100755 forum_modules/oauthauth/settings.py create mode 100755 forum_modules/openidauth/__init__.py create mode 100755 forum_modules/openidauth/authentication.py create mode 100755 forum_modules/openidauth/consumer.py create mode 100755 forum_modules/openidauth/models.py create mode 100755 forum_modules/openidauth/settings.py create mode 100755 forum_modules/openidauth/store.py create mode 100755 forum_modules/openidauth/templates/openidurl.html create mode 100755 forum_modules/pgfulltext/DISABLED create mode 100755 forum_modules/pgfulltext/__init__.py create mode 100755 forum_modules/pgfulltext/handlers.py create mode 100755 forum_modules/pgfulltext/management.py create mode 100755 forum_modules/pgfulltext/pg_fts_install.sql create mode 100755 forum_modules/sphinxfulltext/DISABLED create mode 100755 forum_modules/sphinxfulltext/__init__.py create mode 100755 forum_modules/sphinxfulltext/dependencies.py create mode 100755 forum_modules/sphinxfulltext/handlers.py create mode 100755 forum_modules/sphinxfulltext/models.py create mode 100755 forum_modules/sphinxfulltext/settings.py create mode 100644 locale/en/LC_MESSAGES/django.mo create mode 100644 locale/en/LC_MESSAGES/django.po create mode 100644 locale/es/LC_MESSAGES/django.mo create mode 100644 locale/es/LC_MESSAGES/django.po create mode 100644 locale/zh_CN/LC_MESSAGES/django.mo create mode 100644 locale/zh_CN/LC_MESSAGES/django.po create mode 100755 log/django.osqa.log create mode 100644 manage.py create mode 100755 osqa.iml create mode 100644 osqa.wsgi.dist create mode 100755 rmpyc create mode 100755 settings.py create mode 100755 settings_local.py create mode 100755 settings_local.py.dist create mode 100644 sphinx/sphinx.conf create mode 100644 sql_scripts/091111_upgrade_evgeny.sql create mode 100644 sql_scripts/091208_upgrade_evgeny.sql create mode 100644 sql_scripts/091208_upgrade_evgeny_1.sql create mode 100644 sql_scripts/100108_upgrade_ef.sql create mode 100644 sql_scripts/badges.sql create mode 100644 sql_scripts/cnprog.xml create mode 100644 sql_scripts/cnprog_new_install.sql create mode 100644 sql_scripts/cnprog_new_install_2009_02_28.sql create mode 100644 sql_scripts/cnprog_new_install_2009_03_31.sql create mode 100644 sql_scripts/cnprog_new_install_2009_04_07.sql create mode 100644 sql_scripts/cnprog_new_install_2009_04_09.sql create mode 100644 sql_scripts/drop-all-tables.sh create mode 100644 sql_scripts/drop-auth.sql create mode 100644 sql_scripts/pg_fts_install.sql create mode 100644 sql_scripts/update_2009_01_13_001.sql create mode 100644 sql_scripts/update_2009_01_13_002.sql create mode 100644 sql_scripts/update_2009_01_18_001.sql create mode 100644 sql_scripts/update_2009_01_24.sql create mode 100644 sql_scripts/update_2009_01_25_001.sql create mode 100644 sql_scripts/update_2009_02_26_001.sql create mode 100644 sql_scripts/update_2009_04_10_001.sql create mode 100644 sql_scripts/update_2009_07_05_EF.sql create mode 100644 sql_scripts/update_2009_12_24_001.sql create mode 100644 sql_scripts/update_2009_12_27_001.sql create mode 100644 sql_scripts/update_2009_12_27_002.sql create mode 100755 sql_scripts/update_2010_01_23.sql create mode 100644 sql_scripts/update_2010_02_22.sql create mode 100644 urls.py diff --git a/.idea/ant.xml b/.idea/ant.xml new file mode 100755 index 0000000..db0112b --- /dev/null +++ b/.idea/ant.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100755 index 0000000..b9a1798 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,24 @@ + + + + + + diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml new file mode 100755 index 0000000..b385f01 --- /dev/null +++ b/.idea/copyright/profiles_settings.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100755 index 0000000..fa0a5f1 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100755 index 0000000..44294f2 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100755 index 0000000..9a64d92 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml new file mode 100755 index 0000000..1e7cce4 --- /dev/null +++ b/.idea/uiDesigner.xml @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100755 index 0000000..3848996 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100755 index 0000000..250800c --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,654 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + localhost + 5050 + + + + + + + + + + + 1266534782032 + 1266534782032 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1.6 + + + + + + + + + + + + + + diff --git a/.project b/.project new file mode 100644 index 0000000..8e56b00 --- /dev/null +++ b/.project @@ -0,0 +1,23 @@ + + + osqa + + + + + + org.eclipse.wst.jsdt.core.javascriptValidator + + + + + org.python.pydev.PyDevBuilder + + + + + + org.python.pydev.pythonNature + org.eclipse.wst.jsdt.core.jsNature + + diff --git a/.pydevproject b/.pydevproject new file mode 100644 index 0000000..f7f3fd1 --- /dev/null +++ b/.pydevproject @@ -0,0 +1,10 @@ + + + + +Default +python 2.6 + +/osqa + + diff --git a/HOW_TO_DEBUG b/HOW_TO_DEBUG new file mode 100644 index 0000000..ba36198 --- /dev/null +++ b/HOW_TO_DEBUG @@ -0,0 +1,39 @@ +1) LOGGING +Please remember that log files may contain plaintext passwords, etc. + +Please do not add print statements - at least do not commit them to git +because in some environments printing to stdout causes errors + +Instead use python logging this way: +-------------------------------- +#somewere on top of file +import logging + +#anywhere below +logging.debug('this maybe works') +logging.error('have big error!') +#or even +logging.debug('') #this will add time, line number, function and file record +#sometimes useful record for call tracing on its own +#etc - take a look at http://docs.python.org/library/logging.html +------------------------------- + +in OSQA logging is currently set up in settings_local.py.dist +please update it if you need - in older revs logging strings have less info + +messages of interest can be grepped out of the log file by module/file/function name +e.g. to take out all django_authopenid logs run: +>grep 'osqa\/django_authopenid' log/django.osqa.log | sed 's/^.*MSG: //' +in the example above 'sed' call truncates out a long prefix +and makes output look more meaningful + +2) DJANGO DEBUG TOOLBAR +osqa works with django debug toolbar +if debugging under apache server, check +that debug toolbar media is loaded correctly +if toolbar is enabled but you do not see it, possibly some Alias statement +in apache config is wrong in your VirtualHost or elsewhere + +3) If you discover new debugging techniques, please add here. +Possible areas to improve - at this point there is no SQL query logging, +as well as request data and http header. diff --git a/INSTALL b/INSTALL new file mode 100644 index 0000000..f70b3ec --- /dev/null +++ b/INSTALL @@ -0,0 +1,314 @@ +CONTENTS +------------------ +A. PREREQUISITES +B. INSTALLATION + 1. Settings file + 2. Database + 3. Running OSQA in the development server + 4. Installation under Apache/WSGI + 5. Full text search + 6. Email subscriptions + 7. Sitemap + 8. Miscellaneous +C. CONFIGURATION PARAMETERS (settings_local.py) +D. CUSTOMIZATION + + +A. PREREQUISITES +----------------------------------------------- +0. We recommend you to use python-setuptools to install pre-requirement libraries. +If you haven't installed it, please try to install it first. +e.g, sudo apt-get install python-setuptools + +1. Python2.5/2.6, MySQL, Django v1.0/1.1 +Note: email subscription sender job requires Django 1.1, everything else works with 1.0 +Make sure mysql for python provider has been installed. +sudo easy_install mysql-python + +2. Python-openid v2.2 +http://openidenabled.com/python-openid/ +sudo easy_install python-openid + +4. html5lib +http://code.google.com/p/html5lib/ +Used for HTML sanitizer +sudo easy_install html5lib + +5. Markdown2 +http://code.google.com/p/python-markdown2/ +sudo easy_install markdown2 + +6. Django Debug Toolbar +http://github.com/robhudson/django-debug-toolbar/tree/master + +7. djangosphinx (optional - for full text questions+answer+tag) +http://github.com/dcramer/django-sphinx/tree/master/djangosphinx + +8. sphinx search engine (optional, works together with djangosphinx) +http://sphinxsearch.com/downloads.html + +9. recaptcha_django +http://code.google.com/p/recaptcha-django/ + +10. python recaptcha module +http://code.google.com/p/recaptcha/ +Notice that you will need to register with recaptcha.net and receive +recaptcha public and private keys that need to be saved in your +settings_local.py file + +NOTES: django_authopenid is included into OSQA code +and is significantly modified. http://code.google.com/p/django-authopenid/ +no need to install this library + +B. INSTALLATION +----------------------------------------------- +0. Make sure you have all above python libraries installed. + + make osqa installation server-readable on Linux command might be: + chown -R yourlogin:apache /path/to/OSQA + + directories templates/upfiles and log must be server writable + + on Linux type chmod + chmod -R g+w /path/to/OSQA/upfiles + chmod -R g+w /path/to/log + + above it is assumed that webserver runs under group named "apache" + +1. Settings file + +Copy settings_local.py.dist to settings_local.py and +update all your settings. Check settings.py and update +it as well if necessory. +Section C explains configuration paramaters. + +2. Database + +Prepare your database by using the same database/account +configuration from above. +e.g, +create database osqa DEFAULT CHARACTER SET UTF8 COLLATE utf8_general_ci; +grant all on osqa.* to 'osqa'@'localhost'; +And then run "python manage.py syncdb" to synchronize your database. + +3. Running OSQA on the development server + +Run "python manage.py runserver" to startup django +development environment. +(Under Linux you can use command "python manage.py runserver `hostname -i`:8000", +where you can use any other available number for the port) + +you might want to have DEBUG=True in the beginning of settings.py +when using the test server + +4. Installation under Apache/WSGI + +4.1 Prepare wsgi script + +Make a file readable by your webserver with the following content: + +--------- +import os +import sys + +sys.path.insert(0,'/one/level/above') #insert to make sure that forum will be found +sys.path.append('/one/level/above/OSQA') #maybe this is not necessary +os.environ['DJANGO_SETTINGS_MODULE'] = 'OSQA.settings' +import django.core.handlers.wsgi +application = django.core.handlers.wsgi.WSGIHandler() +----------- + +insert method is used for path because if the forum directory name +is by accident the same as some other python module +you wull see strange errors - forum won't be found even though +it's in the python path. for example using name "test" is +not a good idea - as there is a module with such name + + +4.2 Configure webserver +Settings below are not perfect but may be a good starting point + +--------- +WSGISocketPrefix /path/to/socket/sock #must be readable and writable by apache +WSGIPythonHome /usr/local #must be readable by apache +WSGIPythonEggs /var/python/eggs #must be readable and writable by apache + +#NOTE: all urs below will need to be adjusted if +#settings.FORUM_SCRIPT_ALIAS !='' (e.g. = 'forum/') +#this allows "rooting" forum at http://example.com/forum, if you like + + ServerAdmin forum@example.com + DocumentRoot /path/to/osqa-site + ServerName example.com + + #run mod_wsgi process for django in daemon mode + #this allows avoiding confused timezone settings when + #another application runs in the same virtual host + WSGIDaemonProcess OSQA + WSGIProcessGroup OSQA + + #force all content to be served as static files + #otherwise django will be crunching images through itself wasting time + Alias /m/ /path/to/osqa-site/forum/skins/ + Alias /upfiles/ /path/to/osqa-site/forum/upfiles/ + + Order deny,allow + Allow from all + + + #this is your wsgi script described in the prev section + WSGIScriptAlias / /path/to/osqa-site/osqa.wsgi + + #this will force admin interface to work only + #through https (optional) + #"nimda" is the secret spelling of "admin" ;) + + RewriteEngine on + RewriteRule /nimda(.*)$ https://example.com/nimda$1 [L,R=301] + + CustomLog /var/log/httpd/OSQA/access_log common + ErrorLog /var/log/httpd/OSQA/error_log + +#(optional) run admin interface under https + + ServerAdmin forum@example.com + DocumentRoot /path/to/osqa-site + ServerName example.com + SSLEngine on + SSLCertificateFile /path/to/ssl-certificate/server.crt + SSLCertificateKeyFile /path/to/ssl-certificate/server.key + WSGIScriptAlias / /path/to/osqa-site/osqa.wsgi + CustomLog /var/log/httpd/OSQA/access_log common + ErrorLog /var/log/httpd/OSQA/error_log + DirectoryIndex index.html + +------------- + +5. Full text search (using sphinx search) + + Currently full text search works only with sphinx search engine + And builtin PostgreSQL (postgres only >= 8.3???) + + 5.1 Instructions for Sphinx search setup + Sphinx at this time supports only MySQL and PostgreSQL databases + to enable this, install sphinx search engine and djangosphinx + + configure sphinx, sample configuration can be found in + sphinx/sphinx.conf file usually goes somewhere in /etc tree + + build osqa index first time manually + + % indexer --config /path/to/sphinx.conf --index osqa + + setup cron job to rebuild index periodically with command + your crontab entry may be something like + + 0 9,15,21 * * * /usr/local/bin/indexer --config /etc/sphinx/sphinx.conf --all --rotate >/dev/null 2>&1 + adjust it as necessary this one will reindex three times a day at 9am 3pm and 9pm + + if your forum grows very big ( good luck with that :) you'll + need to two search indices one diff index and one main + please refer to online sphinx search documentation for the information + on the subject http://sphinxsearch.com/docs/ + + in settings_local.py set + USE_SPHINX_SEARCH=True + adjust other settings that have SPHINX_* prefix accordingly + remember that there must be trailing comma in parentheses for + SHPINX_SEARCH_INDICES tuple - particlarly with just one item! + + in settings.py look for INSTALLED_APPS + and uncomment #'djangosphinx', + + +6. Email subscriptions + + This function at the moment requires Django 1.1 + + edit paths in the file cron/send_email_alerts + set up a cron job to call cron/send_email_alerts once or twice a day + subscription sender may be tested manually in shell + by calling cron/send_email_alerts + +7. Sitemap +Sitemap will be available at /sitemap.xml +e.g yoursite.com/forum/sitemap.xml + +google will be pinged each time question, answer or +comment is saved or a question deleted + +for this to be useful - do register you sitemap with Google at +https://www.google.com/webmasters/tools/ + +8. Miscellaneous + +There are some demo scripts under sql_scripts folder, +including badges and test accounts for CNProg.com. You +don't need them to run your sample. + +C. CONFIGURATION PARAMETERS + +#the only parameter that needs to be touched in settings.py is +DEBUG=False #set to True to enable debug mode + +#all forum parameters are set in file settings_local.py + +LOG_FILENAME = 'osqa.log' #where logging messages should go +DATABASE_NAME = 'osqa' # Or path to database file if using sqlite3. +DATABASE_USER = '' # Not used with sqlite3. +DATABASE_PASSWORD = '' # Not used with sqlite3. +DATABASE_ENGINE = 'mysql' #mysql, etc +SERVER_EMAIL = '' +DEFAULT_FROM_EMAIL = '' +EMAIL_HOST_USER = '' +EMAIL_HOST_PASSWORD = '' #not necessary if mailserver is run on local machine +EMAIL_SUBJECT_PREFIX = '[OSQA] ' +EMAIL_HOST='osqa.com' +EMAIL_PORT='25' +EMAIL_USE_TLS=False +TIME_ZONE = 'America/Tijuana' +APP_TITLE = u'OSQA Q&A Forum' #title of your forum +APP_KEYWORDS = u'OSQA,forum,community' #keywords for search engines +APP_DESCRIPTION = u'Ask and answer questions.' #site description for searche engines +APP_INTRO = u'

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 OSQA 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://osqa.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 OSQA uses it's own login/password +EXTERNAL_LEGACY_LOGIN_HOST = 'login.osqa.com' +EXTERNAL_LEGACY_LOGIN_PORT = 80 +EXTERNAL_LEGACY_LOGIN_PROVIDER_NAME = 'OSQA' + +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 + "OSQA" in the msgstr lines +* templates/header.html and templates/footer.html may contain extra links +* templates/about.html - a place to explain for is your forum for +* templates/faq.html - put answers to users frequent questions +* templates/content/style/style.css - modify style sheet to add disctinctive look to your forum diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..803781c --- /dev/null +++ b/LICENSE @@ -0,0 +1,14 @@ +Copyright (C) 2009. Chen Gang + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . diff --git a/PENDING b/PENDING new file mode 100644 index 0000000..2931303 --- /dev/null +++ b/PENDING @@ -0,0 +1,28 @@ +There are two kinds of things that can be done: +refactorings (think of jogging in the morning, going to a spa, well make the code better :) +new features (go to law school, get a job, do something real) +Just a joke - pick yourself a task and work on it. + +==Refactoring== +* validate HTML +* set up loading of default settings from inside the /forum dir +* automatic dependency checking for modules +* propose how to rename directory forum --> osqa + without breaking things and keeping name of the project root + named the same way - osqa + +==New features== +Whoever wants - pick a feature from the WISH_LIST +add it here and start working on it +If you are not starting immediately - leave it on the wishlist :) + +==Notes== +1)after this is done most new suggested features + may be worked on easily since most of them + only require editing view functions and templates + + However, anyone can work on new features anyway - you'll + just have to probably copy-paste your code into + the branch undergoing refactoring which involves + splitting the files. Auto merging across split points + is harder or impossible. diff --git a/README b/README new file mode 100644 index 0000000..2a209b7 --- /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/ROADMAP.rst b/ROADMAP.rst new file mode 100644 index 0000000..42f2e8c --- /dev/null +++ b/ROADMAP.rst @@ -0,0 +1,32 @@ +This document is a map for our activities down the road - therefore ROADMAP. +ROADMAP does not specify deadlines - those belong to the PENDING file + +Intro +========= +ROADMAP aims to streamline activities of the OSQA open source project and +to minimize ad-hoc approaches of "big-picture" level. + +With one exception: under extreme time pressure improvised approaches are perfectly acceptable. + +Items in this document must be discussed in public via dev@osqa.net + +Architecture +============= + +Sub-systems +----------------- +* authentication system +* Q&A system + +Authentication system +------------------------- +* MUST authenticate people visiting the website via web browsers. +* Upon successful authentication must associates the visitor with + his/her Django system user account +* MUST allow multiple methods of authentication to the same account +* MUST support a method to recover lost authentication link by email +* MAY offer an option to "soft-validate" user's email (send a link + with a special key, so that user clicks and we know that email is valid) + "soft" - meaning that lack of validation won't block people + from using the site + diff --git a/WISH_LIST b/WISH_LIST new file mode 100644 index 0000000..6b10687 --- /dev/null +++ b/WISH_LIST @@ -0,0 +1,10 @@ +* The wonder bar (integrated the search / ask functionality) +* The authentication system ??? +* allow multiple logins to the same account +* more advanced templating/skinning system +* per-tag email subscriptions +* view for personalized news on the site +* a little flag popping when there are news +* drill-down mode for navigation by tags +* improved admin console +* sort out mess with profile - currently we patch django User diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/context.py b/context.py new file mode 100644 index 0000000..9e22550 --- /dev/null +++ b/context.py @@ -0,0 +1,47 @@ +from django.conf import settings +def application_settings(context): + my_settings = { + 'APP_TITLE' : settings.APP_TITLE, + 'APP_SHORT_NAME' : settings.APP_SHORT_NAME, + 'APP_URL' : settings.APP_URL, + 'APP_KEYWORDS' : settings.APP_KEYWORDS, + 'APP_DESCRIPTION' : settings.APP_DESCRIPTION, + 'APP_INTRO' : settings.APP_INTRO, + 'EMAIL_VALIDATION': settings.EMAIL_VALIDATION, + 'LANGUAGE_CODE': settings.LANGUAGE_CODE, + 'GOOGLE_SITEMAP_CODE':settings.GOOGLE_SITEMAP_CODE, + 'GOOGLE_ANALYTICS_KEY':settings.GOOGLE_ANALYTICS_KEY, + 'BOOKS_ON':settings.BOOKS_ON, + 'WIKI_ON':settings.WIKI_ON, + 'USE_EXTERNAL_LEGACY_LOGIN':settings.USE_EXTERNAL_LEGACY_LOGIN, + 'RESOURCE_REVISION':settings.RESOURCE_REVISION, + 'USE_SPHINX_SEARCH':settings.USE_SPHINX_SEARCH, + 'OSQA_SKIN':settings.OSQA_DEFAULT_SKIN, + } + return {'settings':my_settings} + +def auth_processor(request): + """ + Returns context variables required by apps that use Django's authentication + system. + + If there is no 'user' attribute in the request, uses AnonymousUser (from + django.contrib.auth). + """ + if hasattr(request, 'user'): + user = request.user + if user.is_authenticated(): + messages = user.message_set.all() + else: + messages = None + else: + from django.contrib.auth.models import AnonymousUser + user = AnonymousUser() + messages = None + + from django.core.context_processors import PermWrapper + return { + 'user': user, + 'messages': messages, + 'perms': PermWrapper(user), + } diff --git a/cron/send_email_alerts b/cron/send_email_alerts new file mode 100644 index 0000000..6358b59 --- /dev/null +++ b/cron/send_email_alerts @@ -0,0 +1,4 @@ +PYTHONPATH=/path/to/dir/above/forum +export PYTHONPATH +APP_ROOT=$PYTHONPATH/nmr-forum2 +/path/to/python $APP_ROOT/manage.py send_email_alerts diff --git a/dos2unix.sh b/dos2unix.sh new file mode 100644 index 0000000..2864426 --- /dev/null +++ b/dos2unix.sh @@ -0,0 +1,12 @@ +#please take care not to dos2unix anything in your .git directory +#because that will probably break your repo +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/forum/__init__.py b/forum/__init__.py new file mode 100644 index 0000000..85cd5d2 --- /dev/null +++ b/forum/__init__.py @@ -0,0 +1 @@ +__all__ = ['admin','auth','const','feed','forms','managers','models','sitemap','urls','views'] diff --git a/forum/admin.py b/forum/admin.py new file mode 100644 index 0000000..88643b9 --- /dev/null +++ b/forum/admin.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- + +from django.contrib import admin +from models import * + + +class AnonymousQuestionAdmin(admin.ModelAdmin): + """AnonymousQuestion admin class""" + +class QuestionAdmin(admin.ModelAdmin): + """Question admin class""" + +class TagAdmin(admin.ModelAdmin): + """Tag admin class""" + +class Answerdmin(admin.ModelAdmin): + """Answer admin class""" + +class CommentAdmin(admin.ModelAdmin): + """ admin class""" + +class VoteAdmin(admin.ModelAdmin): + """ admin class""" + +class FlaggedItemAdmin(admin.ModelAdmin): + """ admin class""" + +class FavoriteQuestionAdmin(admin.ModelAdmin): + """ admin class""" + +class QuestionRevisionAdmin(admin.ModelAdmin): + """ admin class""" + +class AnswerRevisionAdmin(admin.ModelAdmin): + """ admin class""" + +class AwardAdmin(admin.ModelAdmin): + """ admin class""" + +class BadgeAdmin(admin.ModelAdmin): + """ admin class""" + +class ReputeAdmin(admin.ModelAdmin): + """ admin class""" + +class ActivityAdmin(admin.ModelAdmin): + """ admin class""" + +#class BookAdmin(admin.ModelAdmin): +# """ admin class""" + +#class BookAuthorInfoAdmin(admin.ModelAdmin): +# """ admin class""" + +#class BookAuthorRssAdmin(admin.ModelAdmin): +# """ admin class""" + + +admin.site.register(Question, QuestionAdmin) +admin.site.register(Tag, TagAdmin) +admin.site.register(Answer, Answerdmin) +admin.site.register(Comment, CommentAdmin) +admin.site.register(Vote, VoteAdmin) +admin.site.register(FlaggedItem, FlaggedItemAdmin) +admin.site.register(FavoriteQuestion, FavoriteQuestionAdmin) +admin.site.register(QuestionRevision, QuestionRevisionAdmin) +admin.site.register(AnswerRevision, AnswerRevisionAdmin) +admin.site.register(Badge, BadgeAdmin) +admin.site.register(Award, AwardAdmin) +admin.site.register(Repute, ReputeAdmin) +admin.site.register(Activity, ActivityAdmin) +#admin.site.register(Book, BookAdmin) +#admin.site.register(BookAuthorInfo, BookAuthorInfoAdmin) +#admin.site.register(BookAuthorRss, BookAuthorRssAdmin) diff --git a/forum/auth.py b/forum/auth.py new file mode 100644 index 0000000..3533b9c --- /dev/null +++ b/forum/auth.py @@ -0,0 +1,498 @@ +""" +Authorisation related functions. + +The actions a User is authorised to perform are dependent on their reputation +and superuser status. +""" +import datetime +from django.contrib.contenttypes.models import ContentType +from django.utils.translation import ugettext as _ +from django.db import transaction +from models import Repute +from models import Question +from models import Answer +from const import TYPE_REPUTATION +import logging +question_type = ContentType.objects.get_for_model(Question) +answer_type = ContentType.objects.get_for_model(Answer) + +VOTE_UP = 15 +FLAG_OFFENSIVE = 15 +POST_IMAGES = 15 +LEAVE_COMMENTS = 50 +UPLOAD_FILES = 60 +VOTE_DOWN = 100 +CLOSE_OWN_QUESTIONS = 250 +RETAG_OTHER_QUESTIONS = 500 +REOPEN_OWN_QUESTIONS = 500 +EDIT_COMMUNITY_WIKI_POSTS = 750 +EDIT_OTHER_POSTS = 2000 +DELETE_COMMENTS = 2000 +VIEW_OFFENSIVE_FLAGS = 2000 +DISABLE_URL_NOFOLLOW = 2000 +CLOSE_OTHER_QUESTIONS = 3000 +LOCK_POSTS = 4000 + +VOTE_RULES = { + 'scope_votes_per_user_per_day' : 30, # how many votes of one user has everyday + 'scope_flags_per_user_per_day' : 5, # how many times user can flag posts everyday + 'scope_warn_votes_left' : 10, # start when to warn user how many votes left + 'scope_deny_unvote_days' : 1, # if 1 days passed, user can't cancel votes. + 'scope_flags_invisible_main_page' : 3, # post doesn't show on main page if has more than 3 offensive flags + 'scope_flags_delete_post' : 5, # post will be deleted if it has more than 5 offensive flags +} + +REPUTATION_RULES = { + 'initial_score' : 1, + 'scope_per_day_by_upvotes' : 200, + 'gain_by_upvoted' : 10, + 'gain_by_answer_accepted' : 15, + 'gain_by_accepting_answer' : 2, + 'gain_by_downvote_canceled' : 2, + 'gain_by_canceling_downvote' : 1, + 'lose_by_canceling_accepted_answer' : -2, + 'lose_by_accepted_answer_cancled' : -15, + 'lose_by_downvoted' : -2, + 'lose_by_flagged' : -2, + 'lose_by_downvoting' : -1, + 'lose_by_flagged_lastrevision_3_times': -30, + 'lose_by_flagged_lastrevision_5_times': -100, + 'lose_by_upvote_canceled' : -10, +} + +def can_moderate_users(user): + return user.is_superuser + +def can_vote_up(user): + """Determines if a User can vote Questions and Answers up.""" + return user.is_authenticated() and ( + user.reputation >= VOTE_UP or + user.is_superuser) + +def can_flag_offensive(user): + """Determines if a User can flag Questions and Answers as offensive.""" + return user.is_authenticated() and ( + user.reputation >= FLAG_OFFENSIVE or + user.is_superuser) + +def can_add_comments(user,subject): + """Determines if a User can add comments to Questions and Answers.""" + if user.is_authenticated(): + if user.id == subject.author.id: + return True + if user.reputation >= LEAVE_COMMENTS: + return True + if user.is_superuser: + return True + if isinstance(subject,Answer) and subject.question.author.id == user.id: + return True + return False + +def can_vote_down(user): + """Determines if a User can vote Questions and Answers down.""" + return user.is_authenticated() and ( + user.reputation >= VOTE_DOWN or + user.is_superuser) + +def can_retag_questions(user): + """Determines if a User can retag Questions.""" + return user.is_authenticated() and ( + RETAG_OTHER_QUESTIONS <= user.reputation < EDIT_OTHER_POSTS or + user.is_superuser) + +def can_edit_post(user, post): + """Determines if a User can edit the given Question or Answer.""" + return user.is_authenticated() and ( + user.id == post.author_id or + (post.wiki and user.reputation >= EDIT_COMMUNITY_WIKI_POSTS) or + user.reputation >= EDIT_OTHER_POSTS or + user.is_superuser) + +def can_delete_comment(user, comment): + """Determines if a User can delete the given Comment.""" + return user.is_authenticated() and ( + user.id == comment.user_id or + user.reputation >= DELETE_COMMENTS or + user.is_superuser) + +def can_view_offensive_flags(user): + """Determines if a User can view offensive flag counts.""" + return user.is_authenticated() and ( + user.reputation >= VIEW_OFFENSIVE_FLAGS or + user.is_superuser) + +def can_close_question(user, question): + """Determines if a User can close the given Question.""" + return user.is_authenticated() and ( + (user.id == question.author_id and + user.reputation >= CLOSE_OWN_QUESTIONS) or + user.reputation >= CLOSE_OTHER_QUESTIONS or + user.is_superuser) + +def can_lock_posts(user): + """Determines if a User can lock Questions or Answers.""" + return user.is_authenticated() and ( + user.reputation >= LOCK_POSTS or + user.is_superuser) + +def can_follow_url(user): + """Determines if the URL link can be followed by Google search engine.""" + return user.reputation >= DISABLE_URL_NOFOLLOW + +def can_accept_answer(user, question, answer): + return (user.is_authenticated() and + question.author != answer.author and + question.author == user) or user.is_superuser + +# now only support to reopen own question except superuser +def can_reopen_question(user, question): + return (user.is_authenticated() and + user.id == question.author_id and + user.reputation >= REOPEN_OWN_QUESTIONS) or user.is_superuser + +def can_delete_post(user, post): + if user.is_superuser: + return True + elif user.is_authenticated() and user == post.author: + if isinstance(post,Answer): + return True + elif isinstance(post,Question): + answers = post.answers.all() + for answer in answers: + if user != answer.author and answer.deleted == False: + return False + return True + else: + return False + else: + return False + +def can_view_deleted_post(user, post): + return user.is_superuser + +# user preferences view permissions +def is_user_self(request_user, target_user): + return (request_user.is_authenticated() and request_user == target_user) + +def can_view_user_votes(request_user, target_user): + return (request_user.is_authenticated() and request_user == target_user) + +def can_view_user_preferences(request_user, target_user): + return (request_user.is_authenticated() and request_user == target_user) + +def can_view_user_edit(request_user, target_user): + return (request_user.is_authenticated() and request_user == target_user) + +def can_upload_files(request_user): + return (request_user.is_authenticated() and request_user.reputation >= UPLOAD_FILES) or \ + request_user.is_superuser + +########################################### +## actions and reputation changes event +########################################### +def calculate_reputation(origin, offset): + result = int(origin) + int(offset) + if (result > 0): + return result + else: + return 1 + +@transaction.commit_on_success +def onFlaggedItem(item, post, user): + + item.save() + post.offensive_flag_count = post.offensive_flag_count + 1 + post.save() + + post.author.reputation = calculate_reputation(post.author.reputation, + int(REPUTATION_RULES['lose_by_flagged'])) + post.author.save() + + question = post + if ContentType.objects.get_for_model(post) == answer_type: + question = post.question + + reputation = Repute(user=post.author, + negative=int(REPUTATION_RULES['lose_by_flagged']), + question=question, reputed_at=datetime.datetime.now(), + reputation_type=-4, + reputation=post.author.reputation) + reputation.save() + + #todo: These should be updated to work on same revisions. + if post.offensive_flag_count == VOTE_RULES['scope_flags_invisible_main_page'] : + post.author.reputation = calculate_reputation(post.author.reputation, + int(REPUTATION_RULES['lose_by_flagged_lastrevision_3_times'])) + post.author.save() + + reputation = Repute(user=post.author, + negative=int(REPUTATION_RULES['lose_by_flagged_lastrevision_3_times']), + question=question, + reputed_at=datetime.datetime.now(), + reputation_type=-6, + reputation=post.author.reputation) + reputation.save() + + elif post.offensive_flag_count == VOTE_RULES['scope_flags_delete_post']: + post.author.reputation = calculate_reputation(post.author.reputation, + int(REPUTATION_RULES['lose_by_flagged_lastrevision_5_times'])) + post.author.save() + + reputation = Repute(user=post.author, + negative=int(REPUTATION_RULES['lose_by_flagged_lastrevision_5_times']), + question=question, + reputed_at=datetime.datetime.now(), + reputation_type=-7, + reputation=post.author.reputation) + reputation.save() + + post.deleted = True + #post.deleted_at = datetime.datetime.now() + #post.deleted_by = Admin + post.save() + + +@transaction.commit_on_success +def onAnswerAccept(answer, user): + answer.accepted = True + answer.accepted_at = datetime.datetime.now() + answer.question.answer_accepted = True + answer.save() + answer.question.save() + + answer.author.reputation = calculate_reputation(answer.author.reputation, + int(REPUTATION_RULES['gain_by_answer_accepted'])) + answer.author.save() + reputation = Repute(user=answer.author, + positive=int(REPUTATION_RULES['gain_by_answer_accepted']), + question=answer.question, + reputed_at=datetime.datetime.now(), + reputation_type=2, + reputation=answer.author.reputation) + reputation.save() + + user.reputation = calculate_reputation(user.reputation, + int(REPUTATION_RULES['gain_by_accepting_answer'])) + user.save() + reputation = Repute(user=user, + positive=int(REPUTATION_RULES['gain_by_accepting_answer']), + question=answer.question, + reputed_at=datetime.datetime.now(), + reputation_type=3, + reputation=user.reputation) + reputation.save() + +@transaction.commit_on_success +def onAnswerAcceptCanceled(answer, user): + answer.accepted = False + answer.accepted_at = None + answer.question.answer_accepted = False + answer.save() + answer.question.save() + + answer.author.reputation = calculate_reputation(answer.author.reputation, + int(REPUTATION_RULES['lose_by_accepted_answer_cancled'])) + answer.author.save() + reputation = Repute(user=answer.author, + negative=int(REPUTATION_RULES['lose_by_accepted_answer_cancled']), + question=answer.question, + reputed_at=datetime.datetime.now(), + reputation_type=-2, + reputation=answer.author.reputation) + reputation.save() + + user.reputation = calculate_reputation(user.reputation, + int(REPUTATION_RULES['lose_by_canceling_accepted_answer'])) + user.save() + reputation = Repute(user=user, + negative=int(REPUTATION_RULES['lose_by_canceling_accepted_answer']), + question=answer.question, + reputed_at=datetime.datetime.now(), + reputation_type=-1, + reputation=user.reputation) + reputation.save() + +@transaction.commit_on_success +def onUpVoted(vote, post, user): + vote.save() + + post.vote_up_count = int(post.vote_up_count) + 1 + post.score = int(post.score) + 1 + post.save() + + if not post.wiki: + author = post.author + if Repute.objects.get_reputation_by_upvoted_today(author) < int(REPUTATION_RULES['scope_per_day_by_upvotes']): + author.reputation = calculate_reputation(author.reputation, + int(REPUTATION_RULES['gain_by_upvoted'])) + author.save() + + question = post + if ContentType.objects.get_for_model(post) == answer_type: + question = post.question + + reputation = Repute(user=author, + positive=int(REPUTATION_RULES['gain_by_upvoted']), + question=question, + reputed_at=datetime.datetime.now(), + reputation_type=1, + reputation=author.reputation) + reputation.save() + +@transaction.commit_on_success +def onUpVotedCanceled(vote, post, user): + vote.delete() + + post.vote_up_count = int(post.vote_up_count) - 1 + if post.vote_up_count < 0: + post.vote_up_count = 0 + post.score = int(post.score) - 1 + post.save() + + if not post.wiki: + author = post.author + author.reputation = calculate_reputation(author.reputation, + int(REPUTATION_RULES['lose_by_upvote_canceled'])) + author.save() + + question = post + if ContentType.objects.get_for_model(post) == answer_type: + question = post.question + + reputation = Repute(user=author, + negative=int(REPUTATION_RULES['lose_by_upvote_canceled']), + question=question, + reputed_at=datetime.datetime.now(), + reputation_type=-8, + reputation=author.reputation) + reputation.save() + +@transaction.commit_on_success +def onDownVoted(vote, post, user): + vote.save() + + post.vote_down_count = int(post.vote_down_count) + 1 + post.score = int(post.score) - 1 + post.save() + + if not post.wiki: + author = post.author + author.reputation = calculate_reputation(author.reputation, + int(REPUTATION_RULES['lose_by_downvoted'])) + author.save() + + question = post + if ContentType.objects.get_for_model(post) == answer_type: + question = post.question + + reputation = Repute(user=author, + negative=int(REPUTATION_RULES['lose_by_downvoted']), + question=question, + reputed_at=datetime.datetime.now(), + reputation_type=-3, + reputation=author.reputation) + reputation.save() + + user.reputation = calculate_reputation(user.reputation, + int(REPUTATION_RULES['lose_by_downvoting'])) + user.save() + + reputation = Repute(user=user, + negative=int(REPUTATION_RULES['lose_by_downvoting']), + question=question, + reputed_at=datetime.datetime.now(), + reputation_type=-5, + reputation=user.reputation) + reputation.save() + +@transaction.commit_on_success +def onDownVotedCanceled(vote, post, user): + vote.delete() + + post.vote_down_count = int(post.vote_down_count) - 1 + if post.vote_down_count < 0: + post.vote_down_count = 0 + post.score = post.score + 1 + post.save() + + if not post.wiki: + author = post.author + author.reputation = calculate_reputation(author.reputation, + int(REPUTATION_RULES['gain_by_downvote_canceled'])) + author.save() + + question = post + if ContentType.objects.get_for_model(post) == answer_type: + question = post.question + + reputation = Repute(user=author, + positive=int(REPUTATION_RULES['gain_by_downvote_canceled']), + question=question, + reputed_at=datetime.datetime.now(), + reputation_type=4, + reputation=author.reputation) + reputation.save() + + user.reputation = calculate_reputation(user.reputation, + int(REPUTATION_RULES['gain_by_canceling_downvote'])) + user.save() + + reputation = Repute(user=user, + positive=int(REPUTATION_RULES['gain_by_canceling_downvote']), + question=question, + reputed_at=datetime.datetime.now(), + reputation_type=5, + reputation=user.reputation) + reputation.save() + +def onDeleteCanceled(post, user): + post.deleted = False + post.deleted_by = None + post.deleted_at = None + post.save() + logging.debug('now restoring something') + if isinstance(post,Answer): + logging.debug('updated answer count on undelete, have %d' % post.question.answer_count) + Question.objects.update_answer_count(post.question) + elif isinstance(post,Question): + for tag in list(post.tags.all()): + if tag.used_count == 1 and tag.deleted: + tag.deleted = False + tag.deleted_by = None + tag.deleted_at = None + tag.save() + +def onDeleted(post, user): + post.deleted = True + post.deleted_by = user + post.deleted_at = datetime.datetime.now() + post.save() + + if isinstance(post, Question): + for tag in list(post.tags.all()): + if tag.used_count == 1: + tag.deleted = True + tag.deleted_by = user + tag.deleted_at = datetime.datetime.now() + else: + tag.used_count = tag.used_count - 1 + tag.save() + + answers = post.answers.all() + if user == post.author: + if len(answers) > 0: + msg = _('Your question and all of it\'s answers have been deleted') + else: + msg = _('Your question has been deleted') + else: + if len(answers) > 0: + msg = _('The question and all of it\'s answers have been deleted') + else: + msg = _('The question has been deleted') + user.message_set.create(message=msg) + logging.debug('posted a message %s' % msg) + for answer in answers: + onDeleted(answer, user) + elif isinstance(post, Answer): + Question.objects.update_answer_count(post.question) + logging.debug('updated answer count to %d' % post.question.answer_count) diff --git a/forum/authentication/__init__.py b/forum/authentication/__init__.py new file mode 100755 index 0000000..e83ba87 --- /dev/null +++ b/forum/authentication/__init__.py @@ -0,0 +1,27 @@ +import re +from forum.modules import get_modules_script_classes +from forum.authentication.base import AuthenticationConsumer, ConsumerTemplateContext + +class ConsumerAndContext(): + def __init__(self, id, consumer, context): + self.id = id + self.consumer = consumer() + + context.id = id + self.context = context + +consumers = dict([ + (re.sub('AuthConsumer$', '', name).lower(), cls) for name, cls + in get_modules_script_classes('authentication', AuthenticationConsumer).items() + if not re.search('AbstractAuthConsumer$', name) + ]) + +contexts = dict([ + (re.sub('AuthContext$', '', name).lower(), cls) for name, cls + in get_modules_script_classes('authentication', ConsumerTemplateContext).items() + ]) + +AUTH_PROVIDERS = dict([ + (name, ConsumerAndContext(name, consumers[name], contexts[name])) for name in consumers.keys() + if name in contexts + ]) \ No newline at end of file diff --git a/forum/authentication/base.py b/forum/authentication/base.py new file mode 100755 index 0000000..995f7c9 --- /dev/null +++ b/forum/authentication/base.py @@ -0,0 +1,40 @@ + +class AuthenticationConsumer(object): + + def prepare_authentication_request(self, request, redirect_to): + raise NotImplementedError() + + def process_authentication_request(self, response): + raise NotImplementedError() + + def get_user_data(self, key): + raise NotImplementedError() + + +class ConsumerTemplateContext(object): + """ + Class that provides information about a certain authentication provider context in the signin page. + + class attributes: + + mode - one of BIGICON, SMALLICON, FORM + + human_name - the human readable name of the provider + + extra_js - some providers require us to load extra javascript on the signin page for them to work, + this is the place to add those files in the form of a list + + extra_css - same as extra_js but for css files + """ + mode = '' + weight = 500 + human_name = '' + extra_js = [] + extra_css = [] + show_to_logged_in_user = True + +class InvalidAuthentication(Exception): + def __init__(self, message): + self.message = message + + \ No newline at end of file diff --git a/forum/authentication/forms.py b/forum/authentication/forms.py new file mode 100755 index 0000000..0484134 --- /dev/null +++ b/forum/authentication/forms.py @@ -0,0 +1,31 @@ +from forum.utils.forms import NextUrlField, UserNameField, UserEmailField +from forum.models import EmailFeedSetting, Question +from django.contrib.contenttypes.models import ContentType +from django.utils.translation import ugettext as _ +from django import forms +from forum.forms import EditUserEmailFeedsForm +import logging + +class SimpleRegistrationForm(forms.Form): + next = NextUrlField() + username = UserNameField() + email = UserEmailField() + + +class SimpleEmailSubscribeForm(forms.Form): + SIMPLE_SUBSCRIBE_CHOICES = ( + ('y',_('okay, let\'s try!')), + ('n',_('no OSQA community email please, thanks')) + ) + subscribe = forms.ChoiceField(widget=forms.widgets.RadioSelect(), \ + error_messages={'required':_('please choose one of the options above')}, + choices=SIMPLE_SUBSCRIBE_CHOICES) + + def save(self,user=None): + EFF = EditUserEmailFeedsForm + if self.cleaned_data['subscribe'] == 'y': + email_settings_form = EFF() + logging.debug('%s wants to subscribe' % user.username) + else: + email_settings_form = EFF(initial=EFF.NO_EMAIL_INITIAL) + email_settings_form.save(user,save_unbound=True) diff --git a/forum/const.py b/forum/const.py new file mode 100644 index 0000000..76fd4a2 --- /dev/null +++ b/forum/const.py @@ -0,0 +1,92 @@ +# encoding:utf-8 +from django.utils.translation import ugettext as _ +""" +All constants could be used in other modules +For reasons that models, views can't have unicode text in this project, all unicode text go here. +""" +CLOSE_REASONS = ( + (1, _('duplicate question')), + (2, _('question is off-topic or not relevant')), + (3, _('too subjective and argumentative')), + (4, _('is not an answer to the question')), + (5, _('the question is answered, right answer was accepted')), + (6, _('problem is not reproducible or outdated')), + #(7, u'太局部、本地化的问题',) + (7, _('question contains offensive inappropriate, or malicious remarks')), + (8, _('spam or advertising')), +) + +TYPE_REPUTATION = ( + (1, 'gain_by_upvoted'), + (2, 'gain_by_answer_accepted'), + (3, 'gain_by_accepting_answer'), + (4, 'gain_by_downvote_canceled'), + (5, 'gain_by_canceling_downvote'), + (-1, 'lose_by_canceling_accepted_answer'), + (-2, 'lose_by_accepted_answer_cancled'), + (-3, 'lose_by_downvoted'), + (-4, 'lose_by_flagged'), + (-5, 'lose_by_downvoting'), + (-6, 'lose_by_flagged_lastrevision_3_times'), + (-7, 'lose_by_flagged_lastrevision_5_times'), + (-8, 'lose_by_upvote_canceled'), +) + +TYPE_ACTIVITY_ASK_QUESTION=1 +TYPE_ACTIVITY_ANSWER=2 +TYPE_ACTIVITY_COMMENT_QUESTION=3 +TYPE_ACTIVITY_COMMENT_ANSWER=4 +TYPE_ACTIVITY_UPDATE_QUESTION=5 +TYPE_ACTIVITY_UPDATE_ANSWER=6 +TYPE_ACTIVITY_PRIZE=7 +TYPE_ACTIVITY_MARK_ANSWER=8 +TYPE_ACTIVITY_VOTE_UP=9 +TYPE_ACTIVITY_VOTE_DOWN=10 +TYPE_ACTIVITY_CANCEL_VOTE=11 +TYPE_ACTIVITY_DELETE_QUESTION=12 +TYPE_ACTIVITY_DELETE_ANSWER=13 +TYPE_ACTIVITY_MARK_OFFENSIVE=14 +TYPE_ACTIVITY_UPDATE_TAGS=15 +TYPE_ACTIVITY_FAVORITE=16 +TYPE_ACTIVITY_USER_FULL_UPDATED = 17 +TYPE_ACTIVITY_QUESTION_EMAIL_UPDATE_SENT = 18 +#TYPE_ACTIVITY_EDIT_QUESTION=17 +#TYPE_ACTIVITY_EDIT_ANSWER=18 + +TYPE_ACTIVITY = ( + (TYPE_ACTIVITY_ASK_QUESTION, _('question')), + (TYPE_ACTIVITY_ANSWER, _('answer')), + (TYPE_ACTIVITY_COMMENT_QUESTION, _('commented question')), + (TYPE_ACTIVITY_COMMENT_ANSWER, _('commented answer')), + (TYPE_ACTIVITY_UPDATE_QUESTION, _('edited question')), + (TYPE_ACTIVITY_UPDATE_ANSWER, _('edited answer')), + (TYPE_ACTIVITY_PRIZE, _('received award')), + (TYPE_ACTIVITY_MARK_ANSWER, _('marked best answer')), + (TYPE_ACTIVITY_VOTE_UP, _('upvoted')), + (TYPE_ACTIVITY_VOTE_DOWN, _('downvoted')), + (TYPE_ACTIVITY_CANCEL_VOTE, _('canceled vote')), + (TYPE_ACTIVITY_DELETE_QUESTION, _('deleted question')), + (TYPE_ACTIVITY_DELETE_ANSWER, _('deleted answer')), + (TYPE_ACTIVITY_MARK_OFFENSIVE, _('marked offensive')), + (TYPE_ACTIVITY_UPDATE_TAGS, _('updated tags')), + (TYPE_ACTIVITY_FAVORITE, _('selected favorite')), + (TYPE_ACTIVITY_USER_FULL_UPDATED, _('completed user profile')), + (TYPE_ACTIVITY_QUESTION_EMAIL_UPDATE_SENT, _('email update sent to user')), +) + +TYPE_RESPONSE = { + 'QUESTION_ANSWERED' : 'question_answered', + 'QUESTION_COMMENTED': 'question_commented', + 'ANSWER_COMMENTED' : 'answer_commented', + 'ANSWER_ACCEPTED' : 'answer_accepted', +} + +CONST = { + 'closed' : _('[closed]'), + 'deleted' : _('[deleted]'), + 'default_version' : _('initial version'), + 'retagged' : _('retagged'), +} + +#how to filter questions by tags in email digests? +TAG_EMAIL_FILTER_CHOICES = (('ignored', _('exclude ignored tags')),('interesting',_('allow only selected tags'))) diff --git a/forum/feed.py b/forum/feed.py new file mode 100644 index 0000000..e4b929e --- /dev/null +++ b/forum/feed.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python +#encoding:utf-8 +#------------------------------------------------------------------------------- +# Name: Syndication feed class for subsribtion +# Purpose: +# +# Author: Mike +# +# Created: 29/01/2009 +# Copyright: (c) CNPROG.COM 2009 +# Licence: GPL V2 +#------------------------------------------------------------------------------- +from django.contrib.syndication.feeds import Feed, FeedDoesNotExist +from django.utils.translation import ugettext as _ +from models import Question +from django.conf import settings +class RssLastestQuestionsFeed(Feed): + title = settings.APP_TITLE + _(' - ')+ _('latest questions') + link = settings.APP_URL #+ '/' + _('question/') + description = settings.APP_DESCRIPTION + #ttl = 10 + copyright = settings.APP_COPYRIGHT + + def item_link(self, item): + return self.link + item.get_absolute_url() + + def item_author_name(self, item): + return item.author.username + + def item_author_link(self, item): + return item.author.get_profile_url() + + def item_pubdate(self, item): + return item.added_at + + def items(self, item): + return Question.objects.filter(deleted=False).order_by('-last_activity_at')[:30] + +def main(): + pass + +if __name__ == '__main__': + main() diff --git a/forum/forms.py b/forum/forms.py new file mode 100644 index 0000000..c157aa4 --- /dev/null +++ b/forum/forms.py @@ -0,0 +1,359 @@ +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.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.utils.safestring import mark_safe +from forum.utils.forms import NextUrlField, UserNameField, SetPasswordForm +from recaptcha_django import ReCaptchaField +from django.conf import settings +import logging + +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 NotARobotForm(forms.Form): + recaptcha = ReCaptchaField() + +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})) + if settings.EDITABLE_SCREEN_NAME: + 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) + logging.debug('initializing the form') + if settings.EDITABLE_SCREEN_NAME: + 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 ChangePasswordForm(SetPasswordForm): + """ change password form """ + oldpw = forms.CharField(widget=forms.PasswordInput(attrs={'class':'required'}), + label=mark_safe(_('Current password'))) + + def __init__(self, data=None, user=None, *args, **kwargs): + if user is None: + raise TypeError("Keyword argument 'user' must be supplied") + super(ChangePasswordForm, self).__init__(data, *args, **kwargs) + self.user = user + + def clean_oldpw(self): + """ test old password """ + if not self.user.check_password(self.cleaned_data['oldpw']): + raise forms.ValidationError(_("Old password is incorrect. \ + Please enter the correct password.")) + return self.cleaned_data['oldpw'] + +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,save_unbound=False): + """ + with save_unbound==True will bypass form validation and save initial values + """ + 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) + if save_unbound: + #just save initial values instead + if form_field in self.initial: + new_value = self.initial[form_field] + else: + new_value = self.fields[form_field].initial + else: + new_value = self.cleaned_data[form_field] + if s.frequency != new_value: + s.frequency = new_value + s.save() + changed = True + else: + if created: + s.save() + if form_field == 'individually_selected': + feed_type = ContentType.objects.get_for_model(Question) + user.followed_questions.clear() + return changed + diff --git a/forum/management/__init__.py b/forum/management/__init__.py new file mode 100644 index 0000000..8266592 --- /dev/null +++ b/forum/management/__init__.py @@ -0,0 +1,3 @@ +from forum.modules import get_modules_script + +get_modules_script('management') \ No newline at end of file diff --git a/forum/management/commands/__init__.py b/forum/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/forum/management/commands/base_command.py b/forum/management/commands/base_command.py new file mode 100644 index 0000000..c073bf7 --- /dev/null +++ b/forum/management/commands/base_command.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python +#encoding:utf-8 +#------------------------------------------------------------------------------- +# Name: Award badges command +# Purpose: This is a command file croning in background process regularly to +# query database and award badges for user's special acitivities. +# +# Author: Mike, Sailing +# +# Created: 22/01/2009 +# Copyright: (c) Mike 2009 +# Licence: GPL V2 +#------------------------------------------------------------------------------- + +from datetime import datetime, date +from django.core.management.base import NoArgsCommand +from django.db import connection +from django.shortcuts import get_object_or_404 +from django.contrib.contenttypes.models import ContentType + +from forum.models import * +from forum.const import * + +class BaseCommand(NoArgsCommand): + def update_activities_auditted(self, cursor, activity_ids): + # update processed rows to auditted + if len(activity_ids): + query = "UPDATE activity SET is_auditted = 1 WHERE id in (%s)"\ + % ','.join('%s' % item for item in activity_ids) + cursor.execute(query) + + + + + diff --git a/forum/management/commands/clean_award_badges.py b/forum/management/commands/clean_award_badges.py new file mode 100644 index 0000000..117e3a5 --- /dev/null +++ b/forum/management/commands/clean_award_badges.py @@ -0,0 +1,59 @@ +#------------------------------------------------------------------------------- +# Name: Award badges command +# Purpose: This is a command file croning in background process regularly to +# query database and award badges for user's special acitivities. +# +# Author: Mike +# +# Created: 18/01/2009 +# Copyright: (c) Mike 2009 +# Licence: GPL V2 +#------------------------------------------------------------------------------- +#!/usr/bin/env python +#encoding:utf-8 +from django.core.management.base import NoArgsCommand +from django.db import connection +from django.shortcuts import get_object_or_404 +from django.contrib.contenttypes.models import ContentType + +from forum.models import * + +class Command(NoArgsCommand): + def handle_noargs(self, **options): + try: + try: + self.clean_awards() + except Exception, e: + print e + finally: + connection.close() + + def clean_awards(self): + Award.objects.all().delete() + + award_type =ContentType.objects.get_for_model(Award) + Activity.objects.filter(content_type=award_type).delete() + + for user in User.objects.all(): + user.gold = 0 + user.silver = 0 + user.bronze = 0 + user.save() + + for badge in Badge.objects.all(): + badge.awarded_count = 0 + badge.save() + + query = "UPDATE activity SET is_auditted = 0" + cursor = connection.cursor() + try: + cursor.execute(query) + finally: + cursor.close() + connection.close() + +def main(): + pass + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/forum/management/commands/message_to_everyone.py b/forum/management/commands/message_to_everyone.py new file mode 100644 index 0000000..c020c17 --- /dev/null +++ b/forum/management/commands/message_to_everyone.py @@ -0,0 +1,12 @@ +from django.core.management.base import NoArgsCommand +from django.contrib.auth.models import User +import sys + +class Command(NoArgsCommand): + def handle_noargs(self, **options): + msg = None + if msg == None: + print 'to run this command, please first edit the file %s' % __file__ + sys.exit(1) + for u in User.objects.all(): + u.message_set.create(message = msg % u.username) diff --git a/forum/management/commands/multi_award_badges.py b/forum/management/commands/multi_award_badges.py new file mode 100644 index 0000000..6b330cf --- /dev/null +++ b/forum/management/commands/multi_award_badges.py @@ -0,0 +1,348 @@ +#!/usr/bin/env python +#encoding:utf-8 +#------------------------------------------------------------------------------- +# Name: Award badges command +# Purpose: This is a command file croning in background process regularly to +# query database and award badges for user's special acitivities. +# +# Author: Mike, Sailing +# +# Created: 22/01/2009 +# Copyright: (c) Mike 2009 +# Licence: GPL V2 +#------------------------------------------------------------------------------- + +from datetime import datetime, date +from django.core.management.base import NoArgsCommand +from django.db import connection +from django.shortcuts import get_object_or_404 +from django.contrib.contenttypes.models import ContentType + +from forum.models import * +from forum.const import * +from base_command import BaseCommand +""" +(1, '炼狱法师', 3, '炼狱法师', '删除自己有3个以上赞成票的帖子', 1, 0), +(2, '压力白领', 3, '压力白领', '删除自己有3个以上反对票的帖子', 1, 0), +(3, '优秀回答', 3, '优秀回答', '回答好评10次以上', 1, 0), +(4, '优秀问题', 3, '优秀问题', '问题好评10次以上', 1, 0), +(5, '评论家', 3, '评论家', '评论10次以上', 0, 0), +(6, '流行问题', 3, '流行问题', '问题的浏览量超过1000人次', 1, 0), +(7, '巡逻兵', 3, '巡逻兵', '第一次标记垃圾帖子', 0, 0), +(8, '清洁工', 3, '清洁工', '第一次撤销投票', 0, 0), +(9, '批评家', 3, '批评家', '第一次反对票', 0, 0), +(10, '小编', 3, '小编', '第一次编辑更新', 0, 0), +(11, '村长', 3, '村长', '第一次重新标签', 0, 0), +(12, '学者', 3, '学者', '第一次标记答案', 0, 0), +(13, '学生', 3, '学生', '第一次提问并且有一次以上赞成票', 0, 0), +(14, '支持者', 3, '支持者', '第一次赞成票', 0, 0), +(15, '教师', 3, '教师', '第一次回答问题并且得到一个以上赞成票', 0, 0), +(16, '自传作者', 3, '自传作者', '完整填写用户资料所有选项', 0, 0), +(17, '自学成才', 3, '自学成才', '回答自己的问题并且有3个以上赞成票', 1, 0), +(18, '最有价值回答', 1, '最有价值回答', '回答超过100次赞成票', 1, 0), +(19, '最有价值问题', 1, '最有价值问题', '问题超过100次赞成票', 1, 0), +(20, '万人迷', 1, '万人迷', '问题被100人以上收藏', 1, 0), +(21, '著名问题', 1, '著名问题', '问题的浏览量超过10000人次', 1, 0), +(22, 'alpha用户', 2, 'alpha用户', '内测期间的活跃用户', 0, 0), +(23, '极好回答', 2, '极好回答', '回答超过25次赞成票', 1, 0), +(24, '极好问题', 2, '极好问题', '问题超过25次赞成票', 1, 0), +(25, '受欢迎问题', 2, '受欢迎问题', '问题被25人以上收藏', 1, 0), +(26, '优秀市民', 2, '优秀市民', '投票300次以上', 0, 0), +(27, '编辑主任', 2, '编辑主任', '编辑了100个帖子', 0, 0), +(28, '通才', 2, '通才', '在多个标签领域活跃', 0, 0), +(29, '专家', 2, '专家', '在一个标签领域活跃出众', 0, 0), +(30, '老鸟', 2, '老鸟', '活跃超过一年的用户', 0, 0), +(31, '最受关注问题', 2, '最受关注问题', '问题的浏览量超过2500人次', 1, 0), +(32, '学问家', 2, '学问家', '第一次回答被投赞成票10次以上', 0, 0), +(33, 'beta用户', 2, 'beta用户', 'beta期间活跃参与', 0, 0), +(34, '导师', 2, '导师', '被指定为最佳答案并且赞成票40以上', 1, 0), +(35, '巫师', 2, '巫师', '在提问60天之后回答并且赞成票5次以上', 1, 0), +(36, '分类专家', 2, '分类专家', '创建的标签被50个以上问题使用', 1, 0); + + +TYPE_ACTIVITY_ASK_QUESTION=1 +TYPE_ACTIVITY_ANSWER=2 +TYPE_ACTIVITY_COMMENT_QUESTION=3 +TYPE_ACTIVITY_COMMENT_ANSWER=4 +TYPE_ACTIVITY_UPDATE_QUESTION=5 +TYPE_ACTIVITY_UPDATE_ANSWER=6 +TYPE_ACTIVITY_PRIZE=7 +TYPE_ACTIVITY_MARK_ANSWER=8 +TYPE_ACTIVITY_VOTE_UP=9 +TYPE_ACTIVITY_VOTE_DOWN=10 +TYPE_ACTIVITY_CANCEL_VOTE=11 +TYPE_ACTIVITY_DELETE_QUESTION=12 +TYPE_ACTIVITY_DELETE_ANSWER=13 +TYPE_ACTIVITY_MARK_OFFENSIVE=14 +TYPE_ACTIVITY_UPDATE_TAGS=15 +TYPE_ACTIVITY_FAVORITE=16 +TYPE_ACTIVITY_USER_FULL_UPDATED = 17 +""" + +class Command(BaseCommand): + def handle_noargs(self, **options): + try: + try: + self.delete_question_be_voted_up_3() + self.delete_answer_be_voted_up_3() + self.delete_question_be_vote_down_3() + self.delete_answer_be_voted_down_3() + self.answer_be_voted_up_10() + self.question_be_voted_up_10() + self.question_view_1000() + self.answer_self_question_be_voted_up_3() + self.answer_be_voted_up_100() + self.question_be_voted_up_100() + self.question_be_favorited_100() + self.question_view_10000() + self.answer_be_voted_up_25() + self.question_be_voted_up_25() + self.question_be_favorited_25() + self.question_view_2500() + self.answer_be_accepted_and_voted_up_40() + self.question_be_answered_after_60_days_and_be_voted_up_5() + self.created_tag_be_used_in_question_50() + except Exception, e: + print e + finally: + connection.close() + + def delete_question_be_voted_up_3(self): + """ + (1, '炼狱法师', 3, '炼狱法师', '删除自己有3个以上赞成票的帖子', 1, 0), + """ + query = "SELECT act.id, act.user_id, act.object_id FROM activity act, question q WHERE act.object_id = q.id AND\ + act.activity_type = %s AND\ + q.vote_up_count >=3 AND \ + act.is_auditted = 0" % (TYPE_ACTIVITY_DELETE_QUESTION) + self.__process_activities_badge(query, 1, Question) + + def delete_answer_be_voted_up_3(self): + """ + (1, '炼狱法师', 3, '炼狱法师', '删除自己有3个以上赞成票的帖子', 1, 0), + """ + query = "SELECT act.id, act.user_id, act.object_id FROM activity act, answer an WHERE act.object_id = an.id AND\ + act.activity_type = %s AND\ + an.vote_up_count >=3 AND \ + act.is_auditted = 0" % (TYPE_ACTIVITY_DELETE_ANSWER) + self.__process_activities_badge(query, 1, Answer) + + def delete_question_be_vote_down_3(self): + """ + (2, '压力白领', 3, '压力白领', '删除自己有3个以上反对票的帖子', 1, 0), + """ + query = "SELECT act.id, act.user_id, act.object_id FROM activity act, question q WHERE act.object_id = q.id AND\ + act.activity_type = %s AND\ + q.vote_down_count >=3 AND \ + act.is_auditted = 0" % (TYPE_ACTIVITY_DELETE_QUESTION) + content_type = ContentType.objects.get_for_model(Question) + self.__process_activities_badge(query, 2, Question) + + def delete_answer_be_voted_down_3(self): + """ + (2, '压力白领', 3, '压力白领', '删除自己有3个以上反对票的帖子', 1, 0), + """ + query = "SELECT act.id, act.user_id, act.object_id FROM activity act, answer an WHERE act.object_id = an.id AND\ + act.activity_type = %s AND\ + an.vote_down_count >=3 AND \ + act.is_auditted = 0" % (TYPE_ACTIVITY_DELETE_ANSWER) + self.__process_activities_badge(query, 2, Answer) + + def answer_be_voted_up_10(self): + """ + (3, '优秀回答', 3, '优秀回答', '回答好评10次以上', 1, 0), + """ + query = "SELECT act.id, act.user_id, act.object_id FROM \ + activity act, answer a WHERE act.object_id = a.id AND\ + act.activity_type = %s AND \ + a.vote_up_count >= 10 AND\ + act.is_auditted = 0" % (TYPE_ACTIVITY_ANSWER) + self.__process_activities_badge(query, 3, Answer) + + def question_be_voted_up_10(self): + """ + (4, '优秀问题', 3, '优秀问题', '问题好评10次以上', 1, 0), + """ + query = "SELECT act.id, act.user_id, act.object_id FROM \ + activity act, question q WHERE act.object_id = q.id AND\ + act.activity_type = %s AND \ + q.vote_up_count >= 10 AND\ + act.is_auditted = 0" % (TYPE_ACTIVITY_ASK_QUESTION) + self.__process_activities_badge(query, 4, Question) + + def question_view_1000(self): + """ + (6, '流行问题', 3, '流行问题', '问题的浏览量超过1000人次', 1, 0), + """ + query = "SELECT act.id, act.user_id, act.object_id FROM \ + activity act, question q WHERE act.activity_type = %s AND\ + act.object_id = q.id AND \ + q.view_count >= 1000 AND\ + act.object_id NOT IN \ + (SELECT object_id FROM award WHERE award.badge_id = %s)" % (TYPE_ACTIVITY_ASK_QUESTION, 6) + self.__process_activities_badge(query, 6, Question, False) + + def answer_self_question_be_voted_up_3(self): + """ + (17, '自学成才', 3, '自学成才', '回答自己的问题并且有3个以上赞成票', 1, 0), + """ + query = "SELECT act.id, act.user_id, act.object_id FROM \ + activity act, answer an WHERE act.activity_type = %s AND\ + act.object_id = an.id AND\ + an.vote_up_count >= 3 AND\ + act.user_id = (SELECT user_id FROM question q WHERE q.id = an.question_id) AND\ + act.object_id NOT IN \ + (SELECT object_id FROM award WHERE award.badge_id = %s)" % (TYPE_ACTIVITY_ANSWER, 17) + self.__process_activities_badge(query, 17, Question, False) + + def answer_be_voted_up_100(self): + """ + (18, '最有价值回答', 1, '最有价值回答', '回答超过100次赞成票', 1, 0), + """ + query = "SELECT an.id, an.author_id FROM answer an WHERE an.vote_up_count >= 100 AND an.id NOT IN \ + (SELECT object_id FROM award WHERE award.badge_id = %s)" % (18) + + self.__process_badge(query, 18, Answer) + + def question_be_voted_up_100(self): + """ + (19, '最有价值问题', 1, '最有价值问题', '问题超过100次赞成票', 1, 0), + """ + query = "SELECT q.id, q.author_id FROM question q WHERE q.vote_up_count >= 100 AND q.id NOT IN \ + (SELECT object_id FROM award WHERE award.badge_id = %s)" % (19) + + self.__process_badge(query, 19, Question) + + def question_be_favorited_100(self): + """ + (20, '万人迷', 1, '万人迷', '问题被100人以上收藏', 1, 0), + """ + query = "SELECT q.id, q.author_id FROM question q WHERE q.favourite_count >= 100 AND q.id NOT IN \ + (SELECT object_id FROM award WHERE award.badge_id = %s)" % (20) + + self.__process_badge(query, 20, Question) + + def question_view_10000(self): + """ + (21, '著名问题', 1, '著名问题', '问题的浏览量超过10000人次', 1, 0), + """ + query = "SELECT q.id, q.author_id FROM question q WHERE q.view_count >= 10000 AND q.id NOT IN \ + (SELECT object_id FROM award WHERE award.badge_id = %s)" % (21) + + self.__process_badge(query, 21, Question) + + def answer_be_voted_up_25(self): + """ + (23, '极好回答', 2, '极好回答', '回答超过25次赞成票', 1, 0), + """ + query = "SELECT a.id, a.author_id FROM answer a WHERE a.vote_up_count >= 25 AND a.id NOT IN \ + (SELECT object_id FROM award WHERE award.badge_id = %s)" % (23) + + self.__process_badge(query, 23, Answer) + + def question_be_voted_up_25(self): + """ + (24, '极好问题', 2, '极好问题', '问题超过25次赞成票', 1, 0), + """ + query = "SELECT q.id, q.author_id FROM question q WHERE q.vote_up_count >= 25 AND q.id NOT IN \ + (SELECT object_id FROM award WHERE award.badge_id = %s)" % (24) + + self.__process_badge(query, 24, Question) + + def question_be_favorited_25(self): + """ + (25, '受欢迎问题', 2, '受欢迎问题', '问题被25人以上收藏', 1, 0), + """ + query = "SELECT q.id, q.author_id FROM question q WHERE q.favourite_count >= 25 AND q.id NOT IN \ + (SELECT object_id FROM award WHERE award.badge_id = %s)" % (25) + + self.__process_badge(query, 25, Question) + + def question_view_2500(self): + """ + (31, '最受关注问题', 2, '最受关注问题', '问题的浏览量超过2500人次', 1, 0), + """ + query = "SELECT q.id, q.author_id FROM question q WHERE q.view_count >= 2500 AND q.id NOT IN \ + (SELECT object_id FROM award WHERE award.badge_id = %s)" % (31) + + self.__process_badge(query, 31, Question) + + def answer_be_accepted_and_voted_up_40(self): + """ + (34, '导师', 2, '导师', '被指定为最佳答案并且赞成票40以上', 1, 0), + """ + query = "SELECT a.id, a.author_id FROM answer a WHERE a.vote_up_count >= 40 AND\ + a.accepted = 1 AND\ + a.id NOT IN \ + (SELECT object_id FROM award WHERE award.badge_id = %s)" % (34) + + self.__process_badge(query, 34, Answer) + + def question_be_answered_after_60_days_and_be_voted_up_5(self): + """ + (35, '巫师', 2, '巫师', '在提问60天之后回答并且赞成票5次以上', 1, 0), + """ + query = "SELECT a.id, a.author_id FROM question q, answer a WHERE q.id = a.question_id AND\ + DATEDIFF(a.added_at, q.added_at) >= 60 AND\ + a.vote_up_count >= 5 AND \ + a.id NOT IN \ + (SELECT object_id FROM award WHERE award.badge_id = %s)" % (35) + + self.__process_badge(query, 35, Answer) + + def created_tag_be_used_in_question_50(self): + """ + (36, '分类专家', 2, '分类专家', '创建的标签被50个以上问题使用', 1, 0); + """ + query = "SELECT t.id, t.created_by_id FROM tag t, auth_user u WHERE t.created_by_id = u.id AND \ + t. used_count >= 50 AND \ + t.id NOT IN \ + (SELECT object_id FROM award WHERE award.badge_id = %s)" % (36) + + self.__process_badge(query, 36, Tag) + + def __process_activities_badge(self, query, badge, content_object, update_auditted=True): + content_type = ContentType.objects.get_for_model(content_object) + + cursor = connection.cursor() + try: + cursor.execute(query) + rows = cursor.fetchall() + + if update_auditted: + activity_ids = [] + badge = get_object_or_404(Badge, id=badge) + for row in rows: + activity_id = row[0] + user_id = row[1] + object_id = row[2] + + user = get_object_or_404(User, id=user_id) + award = Award(user=user, badge=badge, content_type=content_type, object_id=object_id) + award.save() + + if update_auditted: + activity_ids.append(activity_id) + + if update_auditted: + self.update_activities_auditted(cursor, activity_ids) + finally: + cursor.close() + + def __process_badge(self, query, badge, content_object): + content_type = ContentType.objects.get_for_model(Answer) + cursor = connection.cursor() + try: + cursor.execute(query) + rows = cursor.fetchall() + + badge = get_object_or_404(Badge, id=badge) + for row in rows: + object_id = row[0] + user_id = row[1] + + user = get_object_or_404(User, id=user_id) + award = Award(user=user, badge=badge, content_type=content_type, object_id=object_id) + award.save() + finally: + cursor.close() diff --git a/forum/management/commands/once_award_badges.py b/forum/management/commands/once_award_badges.py new file mode 100644 index 0000000..8c91334 --- /dev/null +++ b/forum/management/commands/once_award_badges.py @@ -0,0 +1,350 @@ +#!/usr/bin/env python +#encoding:utf-8 +#------------------------------------------------------------------------------- +# Name: Award badges command +# Purpose: This is a command file croning in background process regularly to +# query database and award badges for user's special acitivities. +# +# Author: Mike, Sailing +# +# Created: 18/01/2009 +# Copyright: (c) Mike 2009 +# Licence: GPL V2 +#------------------------------------------------------------------------------- + +from datetime import datetime, date +from django.db import connection +from django.shortcuts import get_object_or_404 +from django.contrib.contenttypes.models import ContentType + +from forum.models import * +from forum.const import * +from base_command import BaseCommand +""" +(1, '炼狱法师', 3, '炼狱法师', '删除自己有3个以上赞成票的帖子', 1, 0), +(2, '压力白领', 3, '压力白领', '删除自己有3个以上反对票的帖子', 1, 0), +(3, '优秀回答', 3, '优秀回答', '回答好评10次以上', 1, 0), +(4, '优秀问题', 3, '优秀问题', '问题好评10次以上', 1, 0), +(5, '评论家', 3, '评论家', '评论10次以上', 0, 0), +(6, '流行问题', 3, '流行问题', '问题的浏览量超过1000人次', 1, 0), +(7, '巡逻兵', 3, '巡逻兵', '第一次标记垃圾帖子', 0, 0), +(8, '清洁工', 3, '清洁工', '第一次撤销投票', 0, 0), +(9, '批评家', 3, '批评家', '第一次反对票', 0, 0), +(10, '小编', 3, '小编', '第一次编辑更新', 0, 0), +(11, '村长', 3, '村长', '第一次重新标签', 0, 0), +(12, '学者', 3, '学者', '第一次标记答案', 0, 0), +(13, '学生', 3, '学生', '第一次提问并且有一次以上赞成票', 0, 0), +(14, '支持者', 3, '支持者', '第一次赞成票', 0, 0), +(15, '教师', 3, '教师', '第一次回答问题并且得到一个以上赞成票', 0, 0), +(16, '自传作者', 3, '自传作者', '完整填写用户资料所有选项', 0, 0), +(17, '自学成才', 3, '自学成才', '回答自己的问题并且有3个以上赞成票', 1, 0), +(18, '最有价值回答', 1, '最有价值回答', '回答超过100次赞成票', 1, 0), +(19, '最有价值问题', 1, '最有价值问题', '问题超过100次赞成票', 1, 0), +(20, '万人迷', 1, '万人迷', '问题被100人以上收藏', 1, 0), +(21, '著名问题', 1, '著名问题', '问题的浏览量超过10000人次', 1, 0), +(22, 'alpha用户', 2, 'alpha用户', '内测期间的活跃用户', 0, 0), +(23, '极好回答', 2, '极好回答', '回答超过25次赞成票', 1, 0), +(24, '极好问题', 2, '极好问题', '问题超过25次赞成票', 1, 0), +(25, '受欢迎问题', 2, '受欢迎问题', '问题被25人以上收藏', 1, 0), +(26, '优秀市民', 2, '优秀市民', '投票300次以上', 0, 0), +(27, '编辑主任', 2, '编辑主任', '编辑了100个帖子', 0, 0), +(28, '通才', 2, '通才', '在多个标签领域活跃', 0, 0), +(29, '专家', 2, '专家', '在一个标签领域活跃出众', 0, 0), +(30, '老鸟', 2, '老鸟', '活跃超过一年的用户', 0, 0), +(31, '最受关注问题', 2, '最受关注问题', '问题的浏览量超过2500人次', 1, 0), +(32, '学问家', 2, '学问家', '第一次回答被投赞成票10次以上', 0, 0), +(33, 'beta用户', 2, 'beta用户', 'beta期间活跃参与', 0, 0), +(34, '导师', 2, '导师', '被指定为最佳答案并且赞成票40以上', 1, 0), +(35, '巫师', 2, '巫师', '在提问60天之后回答并且赞成票5次以上', 1, 0), +(36, '分类专家', 2, '分类专家', '创建的标签被50个以上问题使用', 1, 0); + + +TYPE_ACTIVITY_ASK_QUESTION=1 +TYPE_ACTIVITY_ANSWER=2 +TYPE_ACTIVITY_COMMENT_QUESTION=3 +TYPE_ACTIVITY_COMMENT_ANSWER=4 +TYPE_ACTIVITY_UPDATE_QUESTION=5 +TYPE_ACTIVITY_UPDATE_ANSWER=6 +TYPE_ACTIVITY_PRIZE=7 +TYPE_ACTIVITY_MARK_ANSWER=8 +TYPE_ACTIVITY_VOTE_UP=9 +TYPE_ACTIVITY_VOTE_DOWN=10 +TYPE_ACTIVITY_CANCEL_VOTE=11 +TYPE_ACTIVITY_DELETE_QUESTION=12 +TYPE_ACTIVITY_DELETE_ANSWER=13 +TYPE_ACTIVITY_MARK_OFFENSIVE=14 +TYPE_ACTIVITY_UPDATE_TAGS=15 +TYPE_ACTIVITY_FAVORITE=16 +TYPE_ACTIVITY_USER_FULL_UPDATED = 17 +""" + +BADGE_AWARD_TYPE_FIRST = { + TYPE_ACTIVITY_MARK_OFFENSIVE : 7, + TYPE_ACTIVITY_CANCEL_VOTE: 8, + TYPE_ACTIVITY_VOTE_DOWN : 9, + TYPE_ACTIVITY_UPDATE_QUESTION : 10, + TYPE_ACTIVITY_UPDATE_ANSWER : 10, + TYPE_ACTIVITY_UPDATE_TAGS : 11, + TYPE_ACTIVITY_MARK_ANSWER : 12, + TYPE_ACTIVITY_VOTE_UP : 14, + TYPE_ACTIVITY_USER_FULL_UPDATED: 16 + +} + +class Command(BaseCommand): + def handle_noargs(self, **options): + try: + try: + self.alpha_user() + self.beta_user() + self.first_type_award() + self.first_ask_be_voted() + self.first_answer_be_voted() + self.first_answer_be_voted_10() + self.vote_count_300() + self.edit_count_100() + self.comment_count_10() + except Exception, e: + print e + finally: + connection.close() + + def alpha_user(self): + """ + Before Jan 25, 2009(Chinese New Year Eve and enter into Beta for CNProg), every registered user + will be awarded the "Alpha" badge if he has any activities. + """ + alpha_end_date = date(2009, 1, 25) + if date.today() < alpha_end_date: + badge = get_object_or_404(Badge, id=22) + for user in User.objects.all(): + award = Award.objects.filter(user=user, badge=badge) + if award and not badge.multiple: + continue + activities = Activity.objects.filter(user=user) + if len(activities) > 0: + new_award = Award(user=user, badge=badge) + new_award.save() + + def beta_user(self): + """ + Before Feb 25, 2009, every registered user + will be awarded the "Beta" badge if he has any activities. + """ + beta_end_date = date(2009, 2, 25) + if date.today() < beta_end_date: + badge = get_object_or_404(Badge, id=33) + for user in User.objects.all(): + award = Award.objects.filter(user=user, badge=badge) + if award and not badge.multiple: + continue + activities = Activity.objects.filter(user=user) + if len(activities) > 0: + new_award = Award(user=user, badge=badge) + new_award.save() + + def first_type_award(self): + """ + This will award below badges for users first behaviors: + + (7, '巡逻兵', 3, '巡逻兵', '第一次标记垃圾帖子', 0, 0), + (8, '清洁工', 3, '清洁工', '第一次撤销投票', 0, 0), + (9, '批评家', 3, '批评家', '第一次反对票', 0, 0), + (10, '小编', 3, '小编', '第一次编辑更新', 0, 0), + (11, '村长', 3, '村长', '第一次重新标签', 0, 0), + (12, '学者', 3, '学者', '第一次标记答案', 0, 0), + (14, '支持者', 3, '支持者', '第一次赞成票', 0, 0), + (16, '自传作者', 3, '自传作者', '完整填写用户资料所有选项', 0, 0), + """ + activity_types = ','.join('%s' % item for item in BADGE_AWARD_TYPE_FIRST.keys()) + # ORDER BY user_id, activity_type + query = "SELECT id, user_id, activity_type, content_type_id, object_id FROM activity WHERE is_auditted = 0 AND activity_type IN (%s) ORDER BY user_id, activity_type" % activity_types + + cursor = connection.cursor() + try: + cursor.execute(query) + rows = cursor.fetchall() + # collect activity_id in current process + activity_ids = [] + last_user_id = 0 + last_activity_type = 0 + for row in rows: + activity_ids.append(row[0]) + user_id = row[1] + activity_type = row[2] + content_type_id = row[3] + object_id = row[4] + + # if the user and activity are same as the last, continue + if user_id == last_user_id and activity_type == last_activity_type: + continue; + + user = get_object_or_404(User, id=user_id) + badge = get_object_or_404(Badge, id=BADGE_AWARD_TYPE_FIRST[activity_type]) + content_type = get_object_or_404(ContentType, id=content_type_id) + + count = Award.objects.filter(user=user, badge=badge).count() + if count and not badge.multiple: + continue + else: + # new award + award = Award(user=user, badge=badge, content_type=content_type, object_id=object_id) + award.save() + + # set the current user_id and activity_type to last + last_user_id = user_id + last_activity_type = activity_type + + # update processed rows to auditted + self.update_activities_auditted(cursor, activity_ids) + finally: + cursor.close() + + def first_ask_be_voted(self): + """ + For user asked question and got first upvote, we award him following badge: + + (13, '学生', 3, '学生', '第一次提问并且有一次以上赞成票', 0, 0), + """ + query = "SELECT act.user_id, q.vote_up_count, act.object_id FROM " \ + "activity act, question q WHERE act.activity_type = %s AND " \ + "act.object_id = q.id AND " \ + "act.user_id NOT IN (SELECT distinct user_id FROM award WHERE badge_id = %s)" % (TYPE_ACTIVITY_ASK_QUESTION, 13) + cursor = connection.cursor() + try: + cursor.execute(query) + rows = cursor.fetchall() + + badge = get_object_or_404(Badge, id=13) + content_type = ContentType.objects.get_for_model(Question) + awarded_users = [] + for row in rows: + user_id = row[0] + vote_up_count = row[1] + object_id = row[2] + if vote_up_count > 0 and user_id not in awarded_users: + user = get_object_or_404(User, id=user_id) + award = Award(user=user, badge=badge, content_type=content_type, object_id=object_id) + award.save() + awarded_users.append(user_id) + finally: + cursor.close() + + def first_answer_be_voted(self): + """ + When user answerd questions and got first upvote, we award him following badge: + + (15, '教师', 3, '教师', '第一次回答问题并且得到一个以上赞成票', 0, 0), + """ + query = "SELECT act.user_id, a.vote_up_count, act.object_id FROM " \ + "activity act, answer a WHERE act.activity_type = %s AND " \ + "act.object_id = a.id AND " \ + "act.user_id NOT IN (SELECT distinct user_id FROM award WHERE badge_id = %s)" % (TYPE_ACTIVITY_ANSWER, 15) + cursor = connection.cursor() + try: + cursor.execute(query) + rows = cursor.fetchall() + + awarded_users = [] + badge = get_object_or_404(Badge, id=15) + content_type = ContentType.objects.get_for_model(Answer) + for row in rows: + user_id = row[0] + vote_up_count = row[1] + object_id = row[2] + if vote_up_count > 0 and user_id not in awarded_users: + user = get_object_or_404(User, id=user_id) + award = Award(user=user, badge=badge, content_type=content_type, object_id=object_id) + award.save() + awarded_users.append(user_id) + finally: + cursor.close() + + def first_answer_be_voted_10(self): + """ + (32, '学问家', 2, '学问家', '第一次回答被投赞成票10次以上', 0, 0) + """ + query = "SELECT act.user_id, act.object_id FROM " \ + "activity act, answer a WHERE act.object_id = a.id AND " \ + "act.activity_type = %s AND " \ + "a.vote_up_count >= 10 AND " \ + "act.user_id NOT IN (SELECT user_id FROM award WHERE badge_id = %s)" % (TYPE_ACTIVITY_ANSWER, 32) + cursor = connection.cursor() + try: + cursor.execute(query) + rows = cursor.fetchall() + + awarded_users = [] + badge = get_object_or_404(Badge, id=32) + content_type = ContentType.objects.get_for_model(Answer) + for row in rows: + user_id = row[0] + if user_id not in awarded_users: + user = get_object_or_404(User, id=user_id) + object_id = row[1] + award = Award(user=user, badge=badge, content_type=content_type, object_id=object_id) + award.save() + awarded_users.append(user_id) + finally: + cursor.close() + + def vote_count_300(self): + """ + (26, '优秀市民', 2, '优秀市民', '投票300次以上', 0, 0) + """ + query = "SELECT count(*) vote_count, user_id FROM activity WHERE " \ + "activity_type = %s OR " \ + "activity_type = %s AND " \ + "user_id NOT IN (SELECT user_id FROM award WHERE badge_id = %s) " \ + "GROUP BY user_id HAVING vote_count >= 300" % (TYPE_ACTIVITY_VOTE_UP, TYPE_ACTIVITY_VOTE_DOWN, 26) + + self.__award_for_count_num(query, 26) + + def edit_count_100(self): + """ + (27, '编辑主任', 2, '编辑主任', '编辑了100个帖子', 0, 0) + """ + query = "SELECT count(*) vote_count, user_id FROM activity WHERE " \ + "activity_type = %s OR " \ + "activity_type = %s AND " \ + "user_id NOT IN (SELECT user_id FROM award WHERE badge_id = %s) " \ + "GROUP BY user_id HAVING vote_count >= 100" % (TYPE_ACTIVITY_UPDATE_QUESTION, TYPE_ACTIVITY_UPDATE_ANSWER, 27) + + self.__award_for_count_num(query, 27) + + def comment_count_10(self): + """ + (5, '评论家', 3, '评论家', '评论10次以上', 0, 0), + """ + query = "SELECT count(*) vote_count, user_id FROM activity WHERE " \ + "activity_type = %s OR " \ + "activity_type = %s AND " \ + "user_id NOT IN (SELECT user_id FROM award WHERE badge_id = %s) " \ + "GROUP BY user_id HAVING vote_count >= 10" % (TYPE_ACTIVITY_COMMENT_QUESTION, TYPE_ACTIVITY_COMMENT_ANSWER, 5) + self.__award_for_count_num(query, 5) + + def __award_for_count_num(self, query, badge): + cursor = connection.cursor() + try: + cursor.execute(query) + rows = cursor.fetchall() + + awarded_users = [] + badge = get_object_or_404(Badge, id=badge) + for row in rows: + vote_count = row[0] + user_id = row[1] + + if user_id not in awarded_users: + user = get_object_or_404(User, id=user_id) + award = Award(user=user, badge=badge) + award.save() + awarded_users.append(user_id) + finally: + cursor.close() + +def main(): + pass + +if __name__ == '__main__': + main() diff --git a/forum/management/commands/sample_command.py b/forum/management/commands/sample_command.py new file mode 100644 index 0000000..55e6723 --- /dev/null +++ b/forum/management/commands/sample_command.py @@ -0,0 +1,7 @@ +from django.core.management.base import NoArgsCommand +from forum.models import Comment + +class Command(NoArgsCommand): + def handle_noargs(self, **options): + objs = Comment.objects.all() + print objs \ No newline at end of file diff --git a/forum/management/commands/send_email_alerts.py b/forum/management/commands/send_email_alerts.py new file mode 100644 index 0000000..26eb779 --- /dev/null +++ b/forum/management/commands/send_email_alerts.py @@ -0,0 +1,192 @@ +from django.core.management.base import NoArgsCommand +from django.db import connection +from django.db.models import Q, F +from forum.models import * +from forum import const +from django.core.mail import EmailMessage +from django.utils.translation import ugettext as _ +from django.utils.translation import ungettext +import datetime +from django.conf import settings +import logging +from forum.utils.odict import OrderedDict + +class Command(NoArgsCommand): + def handle_noargs(self,**options): + try: + try: + self.send_email_alerts() + except Exception, e: + print e + finally: + connection.close() + + def get_updated_questions_for_user(self,user): + q_sel = None + q_ask = None + q_ans = None + q_all = None + now = datetime.datetime.now() + Q_set1 = Question.objects.exclude( + last_activity_by=user, + ).exclude( + last_activity_at__lt=user.date_joined + ).filter( + Q(viewed__who=user,viewed__when__lt=F('last_activity_at')) | \ + ~Q(viewed__who=user) + ).exclude( + deleted=True + ).exclude( + closed=True + ) + + user_feeds = EmailFeedSetting.objects.filter(subscriber=user).exclude(frequency='n') + for feed in user_feeds: + cutoff_time = now - EmailFeedSetting.DELTA_TABLE[feed.frequency] + if feed.reported_at == None or feed.reported_at <= cutoff_time: + Q_set = Q_set1.exclude(last_activity_at__gt=cutoff_time)#report these excluded later + feed.reported_at = now + feed.save()#may not actually report anything, depending on filters below + if feed.feed_type == 'q_sel': + q_sel = Q_set.filter(followed_by=user) + q_sel.cutoff_time = cutoff_time #store cutoff time per query set + elif feed.feed_type == 'q_ask': + q_ask = Q_set.filter(author=user) + q_ask.cutoff_time = cutoff_time + elif feed.feed_type == 'q_ans': + q_ans = Q_set.filter(answers__author=user) + q_ans.cutoff_time = cutoff_time + elif feed.feed_type == 'q_all': + if user.tag_filter_setting == 'ignored': + ignored_tags = Tag.objects.filter(user_selections__reason='bad',user_selections__user=user) + q_all = Q_set.exclude( tags__in=ignored_tags ) + else: + selected_tags = Tag.objects.filter(user_selections__reason='good',user_selections__user=user) + q_all = Q_set.filter( tags__in=selected_tags ) + q_all.cutoff_time = cutoff_time + #build list in this order + q_list = OrderedDict() + def extend_question_list(src, dst): + """src is a query set with questions + or an empty list + dst - is an ordered dictionary + """ + if src is None: + return #will not do anything if subscription of this type is not used + cutoff_time = src.cutoff_time + for q in src: + if q in dst: + if cutoff_time < dst[q]['cutoff_time']: + dst[q]['cutoff_time'] = cutoff_time + else: + #initialise a questions metadata dictionary to use for email reporting + dst[q] = {'cutoff_time':cutoff_time} + + extend_question_list(q_sel, q_list) + extend_question_list(q_ask, q_list) + extend_question_list(q_ans, q_list) + extend_question_list(q_all, q_list) + + ctype = ContentType.objects.get_for_model(Question) + EMAIL_UPDATE_ACTIVITY = const.TYPE_ACTIVITY_QUESTION_EMAIL_UPDATE_SENT + for q, meta_data in q_list.items(): + #todo use Activity, but first start keeping more Activity records + #act = Activity.objects.filter(content_type=ctype, object_id=q.id) + #because currently activity is not fully recorded to through + #revision records to see what kind modifications were done on + #the questions and answers + try: + update_info = Activity.objects.get(content_type=ctype, + object_id=q.id, + activity_type=EMAIL_UPDATE_ACTIVITY) + emailed_at = update_info.active_at + except Activity.DoesNotExist: + update_info = Activity(user=user, content_object=q, activity_type=EMAIL_UPDATE_ACTIVITY) + emailed_at = datetime.datetime(1970,1,1)#long time ago + except Activity.MultipleObjectsReturned: + raise Exception('server error - multiple question email activities found per user-question pair') + + q_rev = QuestionRevision.objects.filter(question=q,\ + revised_at__lt=cutoff_time,\ + revised_at__gt=emailed_at) + q_rev = q_rev.exclude(author=user) + meta_data['q_rev'] = len(q_rev) + if len(q_rev) > 0 and q.added_at == q_rev[0].revised_at: + meta_data['q_rev'] = 0 + meta_data['new_q'] = True + else: + meta_data['new_q'] = False + + new_ans = Answer.objects.filter(question=q,\ + added_at__lt=cutoff_time,\ + added_at__gt=emailed_at) + new_ans = new_ans.exclude(author=user) + meta_data['new_ans'] = len(new_ans) + ans_rev = AnswerRevision.objects.filter(answer__question=q,\ + revised_at__lt=cutoff_time,\ + revised_at__gt=emailed_at) + ans_rev = ans_rev.exclude(author=user) + meta_data['ans_rev'] = len(ans_rev) + if len(q_rev) == 0 and len(new_ans) == 0 and len(ans_rev) == 0: + meta_data['nothing_new'] = True + else: + meta_data['nothing_new'] = False + update_info.active_at = now + update_info.save() #save question email update activity + return q_list + + def __action_count(self,string,number,output): + if number > 0: + output.append(_(string) % {'num':number}) + + def send_email_alerts(self): + + #todo: move this to template + for user in User.objects.all(): + q_list = self.get_updated_questions_for_user(user) + num_q = 0 + num_moot = 0 + for meta_data in q_list.values(): + if meta_data['nothing_new'] == False: + num_q += 1 + else: + num_moot += 1 + if num_q > 0: + url_prefix = settings.APP_URL + subject = _('email update message subject') + print 'have %d updated questions for %s' % (num_q, user.username) + text = ungettext('%(name)s, this is an update message header for a question', + '%(name)s, this is an update message header for %(num)d questions',num_q) \ + % {'num':num_q, 'name':user.username} + + text += '
    ' + for q, meta_data in q_list.items(): + act_list = [] + if meta_data['nothing_new']: + continue + else: + if meta_data['new_q']: + act_list.append(_('new question')) + self.__action_count('%(num)d rev', meta_data['q_rev'],act_list) + self.__action_count('%(num)d ans', meta_data['new_ans'],act_list) + self.__action_count('%(num)d ans rev',meta_data['ans_rev'],act_list) + act_token = ', '.join(act_list) + text += '
  • %s (%s)
  • ' \ + % (url_prefix + q.get_absolute_url(), q.title, act_token) + text += '
' + if num_moot > 0: + text += '

' + text += ungettext('There is also one question which was recently '\ + +'updated but you might not have seen its latest version.', + 'There are also %(num)d more questions which were recently updated '\ + +'but you might not have seen their latest version.',num_moot) \ + % {'num':num_moot,} + text += _('Perhaps you could look up previously sent forum reminders in your mailbox.') + text += '

' + + link = url_prefix + user.get_profile_url() + '?sort=email_subscriptions' + text += _('go to %(link)s to change frequency of email updates or %(email)s administrator') \ + % {'link':link, 'email':settings.ADMINS[0][1]} + msg = EmailMessage(subject, text, settings.DEFAULT_FROM_EMAIL, [user.email]) + msg.content_subtype = 'html' + msg.send() diff --git a/forum/management/commands/subscribe_everyone.py b/forum/management/commands/subscribe_everyone.py new file mode 100644 index 0000000..c79528f --- /dev/null +++ b/forum/management/commands/subscribe_everyone.py @@ -0,0 +1,32 @@ +from django.core.management.base import NoArgsCommand +from django.db import connection +from django.db.models import Q, F +from forum.models import * +from django.core.mail import EmailMessage +from django.utils.translation import ugettext as _ +from django.utils.translation import ungettext +import datetime +from django.conf import settings + +class Command(NoArgsCommand): + def handle_noargs(self,**options): + try: + try: + self.subscribe_everyone() + except Exception, e: + print e + finally: + connection.close() + + def subscribe_everyone(self): + + feed_type_info = EmailFeedSetting.FEED_TYPES + for user in User.objects.all(): + for feed_type in feed_type_info: + try: + feed_setting = EmailFeedSetting.objects.get(subscriber=user,feed_type = feed_type[0]) + except EmailFeedSetting.DoesNotExist: + feed_setting = EmailFeedSetting(subscriber=user,feed_type=feed_type[0]) + feed_setting.frequency = 'w' + feed_setting.reported_at = None + feed_setting.save() diff --git a/forum/middleware/__init__.py b/forum/middleware/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/forum/middleware/anon_user.py b/forum/middleware/anon_user.py new file mode 100644 index 0000000..866734d --- /dev/null +++ b/forum/middleware/anon_user.py @@ -0,0 +1,35 @@ +from django.http import HttpResponseRedirect +from forum.utils.forms import get_next_url +from django.utils.translation import ugettext as _ +from forum.user_messages import create_message, get_and_delete_messages +from django.conf import settings +from django.core.urlresolvers import reverse +import logging + +class AnonymousMessageManager(object): + def __init__(self,request): + self.request = request + def create(self,message=''): + create_message(self.request,message) + def get_and_delete(self): + messages = get_and_delete_messages(self.request) + return messages + +def dummy_deepcopy(*arg): + """this is necessary to prevent deepcopy() on anonymous user object + that now contains reference to request, which cannot be deepcopied + """ + return None + +class ConnectToSessionMessagesMiddleware(object): + def process_request(self, request): + if not request.user.is_authenticated(): + request.user.__deepcopy__ = dummy_deepcopy #plug on deepcopy which may be called by django db "driver" + request.user.message_set = AnonymousMessageManager(request) #here request is linked to anon user + request.user.get_and_delete_messages = request.user.message_set.get_and_delete + + #also set the first greeting one time per session only + if 'greeting_set' not in request.session: + request.session['greeting_set'] = True + msg = _('First time here? Check out the FAQ!') % reverse('faq') + request.user.message_set.create(message=msg) diff --git a/forum/middleware/cancel.py b/forum/middleware/cancel.py new file mode 100644 index 0000000..15a4371 --- /dev/null +++ b/forum/middleware/cancel.py @@ -0,0 +1,15 @@ +from django.http import HttpResponseRedirect +from forum.utils.forms import get_next_url +import logging +class CancelActionMiddleware(object): + def process_view(self, request, view_func, view_args, view_kwargs): + if 'cancel' in request.REQUEST: + #todo use session messages for the anonymous users + try: + msg = getattr(view_func,'CANCEL_MESSAGE') + except AttributeError: + msg = 'action canceled' + request.user.message_set.create(message=msg) + return HttpResponseRedirect(get_next_url(request)) + else: + return None diff --git a/forum/middleware/pagesize.py b/forum/middleware/pagesize.py new file mode 100644 index 0000000..f6e6fcf --- /dev/null +++ b/forum/middleware/pagesize.py @@ -0,0 +1,33 @@ +# used in questions +QUESTIONS_PAGE_SIZE = 10 +class QuestionsPageSizeMiddleware(object): + def process_request(self, request): + # Set flag to False by default. If it is equal to True, then need to be saved. + pagesize_changed = False + # get pagesize from session, if failed then get default value + user_page_size = request.session.get("pagesize", QUESTIONS_PAGE_SIZE) + # set pagesize equal to logon user specified value in database + if request.user.is_authenticated() and request.user.questions_per_page > 0: + user_page_size = request.user.questions_per_page + + try: + # get new pagesize from UI selection + pagesize = int(request.GET.get('pagesize', user_page_size)) + if pagesize <> user_page_size: + pagesize_changed = True + + except ValueError: + pagesize = user_page_size + + # save this pagesize to user database + if pagesize_changed: + if request.user.is_authenticated(): + user = request.user + user.questions_per_page = pagesize + user.save() + # put pagesize into session + request.session["pagesize"] = pagesize + + def process_exception(self,request,exception): + import logging + logging.debug('have exception %s' % str(exception)) diff --git a/forum/models/__init__.py b/forum/models/__init__.py new file mode 100755 index 0000000..12a0239 --- /dev/null +++ b/forum/models/__init__.py @@ -0,0 +1,343 @@ +from question import Question ,QuestionRevision, QuestionView, AnonymousQuestion, FavoriteQuestion +from answer import Answer, AnonymousAnswer, AnswerRevision +from tag import Tag, MarkedTag +from meta import Vote, Comment, FlaggedItem +from user import Activity, AnonymousEmail, EmailFeedSetting, AuthKeyUserAssociation +from repute import Badge, Award, Repute + +from base import * + +# User extend properties +QUESTIONS_PER_PAGE_CHOICES = ( + (10, u'10'), + (30, u'30'), + (50, u'50'), +) + +def user_is_username_taken(cls,username): + try: + cls.objects.get(username=username) + return True + except cls.MultipleObjectsReturned: + return True + except cls.DoesNotExist: + return False + +def user_get_q_sel_email_feed_frequency(self): + #print 'looking for frequency for user %s' % self + try: + feed_setting = EmailFeedSetting.objects.get(subscriber=self,feed_type='q_sel') + except Exception, e: + #print 'have error %s' % e.message + raise e + #print 'have freq=%s' % feed_setting.frequency + return feed_setting.frequency + +User.add_to_class('is_approved', models.BooleanField(default=False)) +User.add_to_class('email_isvalid', models.BooleanField(default=False)) +User.add_to_class('email_key', models.CharField(max_length=32, null=True)) +User.add_to_class('reputation', models.PositiveIntegerField(default=1)) +User.add_to_class('gravatar', models.CharField(max_length=32)) + +#User.add_to_class('favorite_questions', +# models.ManyToManyField(Question, through=FavoriteQuestion, +# related_name='favorited_by')) + +#User.add_to_class('badges', models.ManyToManyField(Badge, through=Award, +# related_name='awarded_to')) +User.add_to_class('gold', models.SmallIntegerField(default=0)) +User.add_to_class('silver', models.SmallIntegerField(default=0)) +User.add_to_class('bronze', models.SmallIntegerField(default=0)) +User.add_to_class('questions_per_page', + models.SmallIntegerField(choices=QUESTIONS_PER_PAGE_CHOICES, default=10)) +User.add_to_class('last_seen', + models.DateTimeField(default=datetime.datetime.now)) +User.add_to_class('real_name', models.CharField(max_length=100, blank=True)) +User.add_to_class('website', models.URLField(max_length=200, blank=True)) +User.add_to_class('location', models.CharField(max_length=100, blank=True)) +User.add_to_class('date_of_birth', models.DateField(null=True, blank=True)) +User.add_to_class('about', models.TextField(blank=True)) +User.add_to_class('is_username_taken',classmethod(user_is_username_taken)) +User.add_to_class('get_q_sel_email_feed_frequency',user_get_q_sel_email_feed_frequency) +User.add_to_class('hide_ignored_questions', models.BooleanField(default=False)) +User.add_to_class('tag_filter_setting', + models.CharField( + max_length=16, + choices=TAG_EMAIL_FILTER_CHOICES, + default='ignored' + ) + ) + +# custom signal +tags_updated = django.dispatch.Signal(providing_args=["question"]) +edit_question_or_answer = django.dispatch.Signal(providing_args=["instance", "modified_by"]) +delete_post_or_answer = django.dispatch.Signal(providing_args=["instance", "deleted_by"]) +mark_offensive = django.dispatch.Signal(providing_args=["instance", "mark_by"]) +user_updated = django.dispatch.Signal(providing_args=["instance", "updated_by"]) +user_logged_in = django.dispatch.Signal(providing_args=["session"]) + + +def get_messages(self): + messages = [] + for m in self.message_set.all(): + messages.append(m.message) + return messages + +def delete_messages(self): + self.message_set.all().delete() + +def get_profile_url(self): + """Returns the URL for this User's profile.""" + return '%s%s/' % (reverse('user', args=[self.id]), slugify(self.username)) + +def get_profile_link(self): + profile_link = u'%s' % (self.get_profile_url(),self.username) + logging.debug('in get profile link %s' % profile_link) + return mark_safe(profile_link) + +User.add_to_class('get_profile_url', get_profile_url) +User.add_to_class('get_profile_link', get_profile_link) +User.add_to_class('get_messages', get_messages) +User.add_to_class('delete_messages', delete_messages) + +def calculate_gravatar_hash(instance, **kwargs): + """Calculates a User's gravatar hash from their email address.""" + if kwargs.get('raw', False): + return + instance.gravatar = hashlib.md5(instance.email).hexdigest() + +def record_ask_event(instance, created, **kwargs): + if created: + activity = Activity(user=instance.author, active_at=instance.added_at, content_object=instance, activity_type=TYPE_ACTIVITY_ASK_QUESTION) + activity.save() + +def record_answer_event(instance, created, **kwargs): + if created: + activity = Activity(user=instance.author, active_at=instance.added_at, content_object=instance, activity_type=TYPE_ACTIVITY_ANSWER) + activity.save() + +def record_comment_event(instance, created, **kwargs): + if created: + from django.contrib.contenttypes.models import ContentType + question_type = ContentType.objects.get_for_model(Question) + question_type_id = question_type.id + if (instance.content_type_id == question_type_id): + type = TYPE_ACTIVITY_COMMENT_QUESTION + else: + type = TYPE_ACTIVITY_COMMENT_ANSWER + activity = Activity(user=instance.user, active_at=instance.added_at, content_object=instance, activity_type=type) + activity.save() + +def record_revision_question_event(instance, created, **kwargs): + if created and instance.revision <> 1: + activity = Activity(user=instance.author, active_at=instance.revised_at, content_object=instance, activity_type=TYPE_ACTIVITY_UPDATE_QUESTION) + activity.save() + +def record_revision_answer_event(instance, created, **kwargs): + if created and instance.revision <> 1: + activity = Activity(user=instance.author, active_at=instance.revised_at, content_object=instance, activity_type=TYPE_ACTIVITY_UPDATE_ANSWER) + activity.save() + +def record_award_event(instance, created, **kwargs): + """ + After we awarded a badge to user, we need to record this activity and notify user. + We also recaculate awarded_count of this badge and user information. + """ + if created: + activity = Activity(user=instance.user, active_at=instance.awarded_at, content_object=instance, + activity_type=TYPE_ACTIVITY_PRIZE) + activity.save() + + instance.badge.awarded_count += 1 + instance.badge.save() + + if instance.badge.type == Badge.GOLD: + instance.user.gold += 1 + if instance.badge.type == Badge.SILVER: + instance.user.silver += 1 + if instance.badge.type == Badge.BRONZE: + instance.user.bronze += 1 + instance.user.save() + +def notify_award_message(instance, created, **kwargs): + """ + Notify users when they have been awarded badges by using Django message. + """ + if created: + user = instance.user + user.message_set.create(message=u"Congratulations, you have received a badge '%s'" % instance.badge.name) + +def record_answer_accepted(instance, created, **kwargs): + """ + when answer is accepted, we record this for question author - who accepted it. + """ + if not created and instance.accepted: + activity = Activity(user=instance.question.author, active_at=datetime.datetime.now(), \ + content_object=instance, activity_type=TYPE_ACTIVITY_MARK_ANSWER) + activity.save() + +def update_last_seen(instance, created, **kwargs): + """ + when user has activities, we update 'last_seen' time stamp for him + """ + user = instance.user + user.last_seen = datetime.datetime.now() + user.save() + +def record_vote(instance, created, **kwargs): + """ + when user have voted + """ + if created: + if instance.vote == 1: + vote_type = TYPE_ACTIVITY_VOTE_UP + else: + vote_type = TYPE_ACTIVITY_VOTE_DOWN + + activity = Activity(user=instance.user, active_at=instance.voted_at, content_object=instance, activity_type=vote_type) + activity.save() + +def record_cancel_vote(instance, **kwargs): + """ + when user canceled vote, the vote will be deleted. + """ + activity = Activity(user=instance.user, active_at=datetime.datetime.now(), content_object=instance, activity_type=TYPE_ACTIVITY_CANCEL_VOTE) + activity.save() + +def record_delete_question(instance, delete_by, **kwargs): + """ + when user deleted the question + """ + if instance.__class__ == "Question": + activity_type = TYPE_ACTIVITY_DELETE_QUESTION + else: + activity_type = TYPE_ACTIVITY_DELETE_ANSWER + + activity = Activity(user=delete_by, active_at=datetime.datetime.now(), content_object=instance, activity_type=activity_type) + activity.save() + +def record_mark_offensive(instance, mark_by, **kwargs): + activity = Activity(user=mark_by, active_at=datetime.datetime.now(), content_object=instance, activity_type=TYPE_ACTIVITY_MARK_OFFENSIVE) + activity.save() + +def record_update_tags(question, **kwargs): + """ + when user updated tags of the question + """ + activity = Activity(user=question.author, active_at=datetime.datetime.now(), content_object=question, activity_type=TYPE_ACTIVITY_UPDATE_TAGS) + activity.save() + +def record_favorite_question(instance, created, **kwargs): + """ + when user add the question in him favorite questions list. + """ + if created: + activity = Activity(user=instance.user, active_at=datetime.datetime.now(), content_object=instance, activity_type=TYPE_ACTIVITY_FAVORITE) + activity.save() + +def record_user_full_updated(instance, **kwargs): + activity = Activity(user=instance, active_at=datetime.datetime.now(), content_object=instance, activity_type=TYPE_ACTIVITY_USER_FULL_UPDATED) + activity.save() + +def post_stored_anonymous_content(sender,user,session_key,signal,*args,**kwargs): + aq_list = AnonymousQuestion.objects.filter(session_key = session_key) + aa_list = AnonymousAnswer.objects.filter(session_key = session_key) + import settings + if settings.EMAIL_VALIDATION == 'on':#add user to the record + for aq in aq_list: + aq.author = user + aq.save() + for aa in aa_list: + aa.author = user + aa.save() + #maybe add pending posts message? + else: #just publish the questions + for aq in aq_list: + aq.publish(user) + for aa in aa_list: + aa.publish(user) + +#signal for User modle save changes + +pre_save.connect(calculate_gravatar_hash, sender=User) +post_save.connect(record_ask_event, sender=Question) +post_save.connect(record_answer_event, sender=Answer) +post_save.connect(record_comment_event, sender=Comment) +post_save.connect(record_revision_question_event, sender=QuestionRevision) +post_save.connect(record_revision_answer_event, sender=AnswerRevision) +post_save.connect(record_award_event, sender=Award) +post_save.connect(notify_award_message, sender=Award) +post_save.connect(record_answer_accepted, sender=Answer) +post_save.connect(update_last_seen, sender=Activity) +post_save.connect(record_vote, sender=Vote) +post_delete.connect(record_cancel_vote, sender=Vote) +delete_post_or_answer.connect(record_delete_question, sender=Question) +delete_post_or_answer.connect(record_delete_question, sender=Answer) +mark_offensive.connect(record_mark_offensive, sender=Question) +mark_offensive.connect(record_mark_offensive, sender=Answer) +tags_updated.connect(record_update_tags, sender=Question) +post_save.connect(record_favorite_question, sender=FavoriteQuestion) +user_updated.connect(record_user_full_updated, sender=User) +user_logged_in.connect(post_stored_anonymous_content) + +Question = Question +QuestionRevision = QuestionRevision +QuestionView = QuestionView +FavoriteQuestion = FavoriteQuestion +AnonymousQuestion = AnonymousQuestion + +Answer = Answer +AnswerRevision = AnswerRevision +AnonymousAnswer = AnonymousAnswer + +Tag = Tag +Comment = Comment +Vote = Vote +FlaggedItem = FlaggedItem +MarkedTag = MarkedTag + +Badge = Badge +Award = Award +Repute = Repute + +Activity = Activity +EmailFeedSetting = EmailFeedSetting +AnonymousEmail = AnonymousEmail +AuthKeyUserAssociation = AuthKeyUserAssociation + +__all__ = [ + 'Question', + 'QuestionRevision', + 'QuestionView', + 'FavoriteQuestion', + 'AnonymousQuestion', + + 'Answer', + 'AnswerRevision', + 'AnonymousAnswer', + + 'Tag', + 'Comment', + 'Vote', + 'FlaggedItem', + 'MarkedTag', + + 'Badge', + 'Award', + 'Repute', + + 'Activity', + 'EmailFeedSetting', + 'AnonymousEmail', + 'AuthKeyUserAssociation', + + 'User' + ] + + +from forum.modules import get_modules_script_classes + +for k, v in get_modules_script_classes('models', models.Model).items(): + if not k in __all__: + __all__.append(k) + exec "%s = v" % k \ No newline at end of file diff --git a/forum/models/answer.py b/forum/models/answer.py new file mode 100755 index 0000000..14199de --- /dev/null +++ b/forum/models/answer.py @@ -0,0 +1,134 @@ +from base import * + +from question import Question + +class AnswerManager(models.Manager): + @staticmethod + def create_new(cls, question=None, author=None, added_at=None, wiki=False, text='', email_notify=False): + answer = Answer( + question = question, + author = author, + added_at = added_at, + wiki = wiki, + html = text + ) + if answer.wiki: + answer.last_edited_by = answer.author + answer.last_edited_at = added_at + answer.wikified_at = added_at + + answer.save() + + #update question data + question.last_activity_at = added_at + question.last_activity_by = author + question.save() + Question.objects.update_answer_count(question) + + AnswerRevision.objects.create( + answer = answer, + revision = 1, + author = author, + revised_at = added_at, + summary = CONST['default_version'], + text = text + ) + + #set notification/delete + if email_notify: + if author not in question.followed_by.all(): + question.followed_by.add(author) + else: + #not sure if this is necessary. ajax should take care of this... + try: + question.followed_by.remove(author) + except: + pass + + #GET_ANSWERS_FROM_USER_QUESTIONS = u'SELECT answer.* FROM answer INNER JOIN question ON answer.question_id = question.id WHERE question.author_id =%s AND answer.author_id <> %s' + def get_answers_from_question(self, question, user=None): + """ + Retrieves visibile answers for the given question. Delete answers + are only visibile to the person who deleted them. + """ + + if user is None or not user.is_authenticated(): + return self.filter(question=question, deleted=False) + else: + return self.filter(models.Q(question=question), + models.Q(deleted=False) | models.Q(deleted_by=user)) + + #todo: I think this method is not being used anymore, I'll just comment it for now + #def get_answers_from_questions(self, user_id): + # """ + # Retrieves visibile answers for the given question. Which are not included own answers + # """ + # cursor = connection.cursor() + # cursor.execute(self.GET_ANSWERS_FROM_USER_QUESTIONS, [user_id, user_id]) + # return cursor.fetchall() + +class Answer(Content, DeletableContent): + question = models.ForeignKey('Question', related_name='answers') + accepted = models.BooleanField(default=False) + accepted_at = models.DateTimeField(null=True, blank=True) + + objects = AnswerManager() + + class Meta(Content.Meta): + db_table = u'answer' + + def get_user_vote(self, user): + if user.__class__.__name__ == "AnonymousUser": + return None + + votes = self.votes.filter(user=user) + if votes and votes.count() > 0: + return votes[0] + else: + return None + + def get_latest_revision(self): + return self.revisions.all()[0] + + def get_question_title(self): + return self.question.title + + def get_absolute_url(self): + return '%s%s#%s' % (reverse('question', args=[self.question.id]), django_urlquote(slugify(self.question.title)), self.id) + + def __unicode__(self): + return self.html + + +class AnswerRevision(ContentRevision): + """A revision of an Answer.""" + answer = models.ForeignKey('Answer', related_name='revisions') + + def get_absolute_url(self): + return reverse('answer_revisions', kwargs={'id':self.answer.id}) + + def get_question_title(self): + return self.answer.question.title + + class Meta(ContentRevision.Meta): + db_table = u'answer_revision' + ordering = ('-revision',) + + def save(self, **kwargs): + """Looks up the next available revision number if not set.""" + if not self.revision: + self.revision = AnswerRevision.objects.filter( + answer=self.answer).values_list('revision', + flat=True)[0] + 1 + super(AnswerRevision, self).save(**kwargs) + +class AnonymousAnswer(AnonymousContent): + question = models.ForeignKey('Question', related_name='anonymous_answers') + + def publish(self,user): + added_at = datetime.datetime.now() + #print user.id + AnswerManager.create_new(question=self.question,wiki=self.wiki, + added_at=added_at,text=self.text, + author=user) + self.delete() diff --git a/forum/models/base.py b/forum/models/base.py new file mode 100755 index 0000000..2c28a47 --- /dev/null +++ b/forum/models/base.py @@ -0,0 +1,139 @@ +import datetime +import hashlib +from urllib import quote_plus, urlencode +from django.db import models, IntegrityError, connection, transaction +from django.utils.http import urlquote as django_urlquote +from django.utils.html import strip_tags +from django.core.urlresolvers import reverse +from django.contrib.auth.models import User +from django.contrib.contenttypes import generic +from django.contrib.contenttypes.models import ContentType +from django.template.defaultfilters import slugify +from django.db.models.signals import post_delete, post_save, pre_save +from django.utils.translation import ugettext as _ +from django.utils.safestring import mark_safe +from django.contrib.sitemaps import ping_google +import django.dispatch +from django.conf import settings +import logging + +if settings.USE_SPHINX_SEARCH == True: + from djangosphinx.models import SphinxSearch + +from forum.const import * + +class MetaContent(models.Model): + """ + Base class for Vote, Comment and FlaggedItem + """ + content_type = models.ForeignKey(ContentType) + object_id = models.PositiveIntegerField() + content_object = generic.GenericForeignKey('content_type', 'object_id') + user = models.ForeignKey(User, related_name='%(class)ss') + + class Meta: + abstract = True + app_label = 'forum' + + +class DeletableContent(models.Model): + deleted = models.BooleanField(default=False) + deleted_at = models.DateTimeField(null=True, blank=True) + deleted_by = models.ForeignKey(User, null=True, blank=True, related_name='deleted_%(class)ss') + + class Meta: + abstract = True + app_label = 'forum' + + +class ContentRevision(models.Model): + """ + Base class for QuestionRevision and AnswerRevision + """ + revision = models.PositiveIntegerField() + author = models.ForeignKey(User, related_name='%(class)ss') + revised_at = models.DateTimeField() + summary = models.CharField(max_length=300, blank=True) + text = models.TextField() + + class Meta: + abstract = True + app_label = 'forum' + + +class AnonymousContent(models.Model): + """ + Base class for AnonymousQuestion and AnonymousAnswer + """ + session_key = models.CharField(max_length=40) #session id for anonymous questions + wiki = models.BooleanField(default=False) + added_at = models.DateTimeField(default=datetime.datetime.now) + ip_addr = models.IPAddressField(max_length=21) #allow high port numbers + author = models.ForeignKey(User,null=True) + text = models.TextField() + summary = models.CharField(max_length=180) + + class Meta: + abstract = True + app_label = 'forum' + + +from meta import Comment, Vote, FlaggedItem + +class Content(models.Model): + """ + Base class for Question and Answer + """ + author = models.ForeignKey(User, related_name='%(class)ss') + added_at = models.DateTimeField(default=datetime.datetime.now) + + wiki = models.BooleanField(default=False) + wikified_at = models.DateTimeField(null=True, blank=True) + + locked = models.BooleanField(default=False) + locked_by = models.ForeignKey(User, null=True, blank=True, related_name='locked_%(class)ss') + locked_at = models.DateTimeField(null=True, blank=True) + + score = models.IntegerField(default=0) + vote_up_count = models.IntegerField(default=0) + vote_down_count = models.IntegerField(default=0) + + comment_count = models.PositiveIntegerField(default=0) + offensive_flag_count = models.SmallIntegerField(default=0) + + last_edited_at = models.DateTimeField(null=True, blank=True) + last_edited_by = models.ForeignKey(User, null=True, blank=True, related_name='last_edited_%(class)ss') + + html = models.TextField() + comments = generic.GenericRelation(Comment) + votes = generic.GenericRelation(Vote) + flagged_items = generic.GenericRelation(FlaggedItem) + + class Meta: + abstract = True + app_label = 'forum' + + def save(self,**kwargs): + super(Content,self).save(**kwargs) + try: + ping_google() + except Exception: + logging.debug('problem pinging google did you register you sitemap with google?') + + def get_object_comments(self): + comments = self.comments.all().order_by('id') + return comments + + def post_get_last_update_info(self): + when = self.added_at + who = self.author + if self.last_edited_at and self.last_edited_at > when: + when = self.last_edited_at + who = self.last_edited_by + comments = self.comments.all() + if len(comments) > 0: + for c in comments: + if c.added_at > when: + when = c.added_at + who = c.user + return when, who \ No newline at end of file diff --git a/forum/models/meta.py b/forum/models/meta.py new file mode 100755 index 0000000..3dfd3e8 --- /dev/null +++ b/forum/models/meta.py @@ -0,0 +1,89 @@ +from base import * + +class VoteManager(models.Manager): + def get_up_vote_count_from_user(self, user): + if user is not None: + return self.filter(user=user, vote=1).count() + else: + return 0 + + def get_down_vote_count_from_user(self, user): + if user is not None: + return self.filter(user=user, vote=-1).count() + else: + return 0 + + def get_votes_count_today_from_user(self, user): + if user is not None: + today = datetime.date.today() + return self.filter(user=user, voted_at__range=(today, today + datetime.timedelta(1))).count() + + else: + return 0 + + +class Vote(MetaContent): + VOTE_UP = +1 + VOTE_DOWN = -1 + VOTE_CHOICES = ( + (VOTE_UP, u'Up'), + (VOTE_DOWN, u'Down'), + ) + + vote = models.SmallIntegerField(choices=VOTE_CHOICES) + voted_at = models.DateTimeField(default=datetime.datetime.now) + + objects = VoteManager() + + class Meta(MetaContent.Meta): + unique_together = ('content_type', 'object_id', 'user') + db_table = u'vote' + + def __unicode__(self): + return '[%s] voted at %s: %s' %(self.user, self.voted_at, self.vote) + + def is_upvote(self): + return self.vote == self.VOTE_UP + + def is_downvote(self): + return self.vote == self.VOTE_DOWN + + +class FlaggedItemManager(models.Manager): + def get_flagged_items_count_today(self, user): + if user is not None: + today = datetime.date.today() + return self.filter(user=user, flagged_at__range=(today, today + datetime.timedelta(1))).count() + else: + return 0 + +class FlaggedItem(MetaContent): + """A flag on a Question or Answer indicating offensive content.""" + flagged_at = models.DateTimeField(default=datetime.datetime.now) + + objects = FlaggedItemManager() + + class Meta(MetaContent.Meta): + unique_together = ('content_type', 'object_id', 'user') + db_table = u'flagged_item' + + def __unicode__(self): + return '[%s] flagged at %s' %(self.user, self.flagged_at) + +class Comment(MetaContent): + comment = models.CharField(max_length=300) + added_at = models.DateTimeField(default=datetime.datetime.now) + + class Meta(MetaContent.Meta): + ordering = ('-added_at',) + db_table = u'comment' + + def save(self,**kwargs): + super(Comment,self).save(**kwargs) + try: + ping_google() + except Exception: + logging.debug('problem pinging google did you register you sitemap with google?') + + def __unicode__(self): + return self.comment \ No newline at end of file diff --git a/forum/models/question.py b/forum/models/question.py new file mode 100755 index 0000000..f916e65 --- /dev/null +++ b/forum/models/question.py @@ -0,0 +1,336 @@ +from base import * +from tag import Tag + +class QuestionManager(models.Manager): + @staticmethod + def create_new(cls, title=None,author=None,added_at=None, wiki=False,tagnames=None,summary=None, text=None): + question = Question( + title = title, + author = author, + added_at = added_at, + last_activity_at = added_at, + last_activity_by = author, + wiki = wiki, + tagnames = tagnames, + html = text, + summary = summary + ) + if question.wiki: + question.last_edited_by = question.author + question.last_edited_at = added_at + question.wikified_at = added_at + + question.save() + + # create the first revision + QuestionRevision.objects.create( + question = question, + revision = 1, + title = question.title, + author = author, + revised_at = added_at, + tagnames = question.tagnames, + summary = CONST['default_version'], + text = text + ) + return question + + def update_tags(self, question, tagnames, user): + """ + Updates Tag associations for a question to match the given + tagname string. + + Returns ``True`` if tag usage counts were updated as a result, + ``False`` otherwise. + """ + + current_tags = list(question.tags.all()) + current_tagnames = set(t.name for t in current_tags) + updated_tagnames = set(t for t in tagnames.split(' ') if t) + modified_tags = [] + + removed_tags = [t for t in current_tags + if t.name not in updated_tagnames] + if removed_tags: + modified_tags.extend(removed_tags) + question.tags.remove(*removed_tags) + + added_tagnames = updated_tagnames - current_tagnames + if added_tagnames: + added_tags = Tag.objects.get_or_create_multiple(added_tagnames, + user) + modified_tags.extend(added_tags) + question.tags.add(*added_tags) + + if modified_tags: + Tag.objects.update_use_counts(modified_tags) + return True + + return False + + def update_answer_count(self, question): + """ + Executes an UPDATE query to update denormalised data with the + number of answers the given question has. + """ + + # for some reasons, this Answer class failed to be imported, + # although we have imported all classes from models on top. + from answer import Answer + self.filter(id=question.id).update( + answer_count=Answer.objects.get_answers_from_question(question).filter(deleted=False).count()) + + def update_view_count(self, question): + """ + update counter+1 when user browse question page + """ + self.filter(id=question.id).update(view_count = question.view_count + 1) + + def update_favorite_count(self, question): + """ + update favourite_count for given question + """ + self.filter(id=question.id).update(favourite_count = FavoriteQuestion.objects.filter(question=question).count()) + + def get_similar_questions(self, question): + """ + Get 10 similar questions for given one. + This will search the same tag list for give question(by exactly same string) first. + Questions with the individual tags will be added to list if above questions are not full. + """ + #print datetime.datetime.now() + questions = list(self.filter(tagnames = question.tagnames, deleted=False).all()) + + tags_list = question.tags.all() + for tag in tags_list: + extend_questions = self.filter(tags__id = tag.id, deleted=False)[:50] + for item in extend_questions: + if item not in questions and len(questions) < 10: + questions.append(item) + + #print datetime.datetime.now() + return questions + +class Question(Content, DeletableContent): + title = models.CharField(max_length=300) + tags = models.ManyToManyField('Tag', related_name='questions') + answer_accepted = models.BooleanField(default=False) + closed = models.BooleanField(default=False) + closed_by = models.ForeignKey(User, null=True, blank=True, related_name='closed_questions') + closed_at = models.DateTimeField(null=True, blank=True) + close_reason = models.SmallIntegerField(choices=CLOSE_REASONS, null=True, blank=True) + followed_by = models.ManyToManyField(User, related_name='followed_questions') + + # Denormalised data + answer_count = models.PositiveIntegerField(default=0) + view_count = models.PositiveIntegerField(default=0) + favourite_count = models.PositiveIntegerField(default=0) + last_activity_at = models.DateTimeField(default=datetime.datetime.now) + last_activity_by = models.ForeignKey(User, related_name='last_active_in_questions') + tagnames = models.CharField(max_length=125) + summary = models.CharField(max_length=180) + + favorited_by = models.ManyToManyField(User, through='FavoriteQuestion', related_name='favorite_questions') + + objects = QuestionManager() + + class Meta(Content.Meta): + db_table = u'question' + + def delete(self): + super(Question, self).delete() + try: + ping_google() + except Exception: + logging.debug('problem pinging google did you register you sitemap with google?') + + def save(self, **kwargs): + """ + Overridden to manually manage addition of tags when the object + is first saved. + + This is required as we're using ``tagnames`` as the sole means of + adding and editing tags. + """ + initial_addition = (self.id is None) + + super(Question, self).save(**kwargs) + + if initial_addition: + tags = Tag.objects.get_or_create_multiple(self.tagname_list(), + self.author) + self.tags.add(*tags) + Tag.objects.update_use_counts(tags) + + def tagname_list(self): + """Creates a list of Tag names from the ``tagnames`` attribute.""" + return [name for name in self.tagnames.split(u' ')] + + def tagname_meta_generator(self): + return u','.join([unicode(tag) for tag in self.tagname_list()]) + + def get_absolute_url(self): + return '%s%s' % (reverse('question', args=[self.id]), django_urlquote(slugify(self.title))) + + def has_favorite_by_user(self, user): + if not user.is_authenticated(): + return False + + return FavoriteQuestion.objects.filter(question=self, user=user).count() > 0 + + def get_answer_count_by_user(self, user_id): + from answer import Answer + query_set = Answer.objects.filter(author__id=user_id) + return query_set.filter(question=self).count() + + def get_question_title(self): + if self.closed: + attr = CONST['closed'] + elif self.deleted: + attr = CONST['deleted'] + else: + attr = None + if attr is not None: + return u'%s %s' % (self.title, attr) + else: + return self.title + + def get_revision_url(self): + return reverse('question_revisions', args=[self.id]) + + def get_latest_revision(self): + return self.revisions.all()[0] + + def get_last_update_info(self): + when, who = self.post_get_last_update_info() + + answers = self.answers.all() + if len(answers) > 0: + for a in answers: + a_when, a_who = a.post_get_last_update_info() + if a_when > when: + when = a_when + who = a_who + + return when, who + + def get_update_summary(self,last_reported_at=None,recipient_email=''): + edited = False + if self.last_edited_at and self.last_edited_at > last_reported_at: + if self.last_edited_by.email != recipient_email: + edited = True + comments = [] + for comment in self.comments.all(): + if comment.added_at > last_reported_at and comment.user.email != recipient_email: + comments.append(comment) + new_answers = [] + answer_comments = [] + modified_answers = [] + commented_answers = [] + import sets + commented_answers = sets.Set([]) + for answer in self.answers.all(): + if (answer.added_at > last_reported_at and answer.author.email != recipient_email): + new_answers.append(answer) + if (answer.last_edited_at + and answer.last_edited_at > last_reported_at + and answer.last_edited_by.email != recipient_email): + modified_answers.append(answer) + for comment in answer.comments.all(): + if comment.added_at > last_reported_at and comment.user.email != recipient_email: + commented_answers.add(answer) + answer_comments.append(comment) + + #create the report + if edited or new_answers or modified_answers or answer_comments: + out = [] + if edited: + out.append(_('%(author)s modified the question') % {'author':self.last_edited_by.username}) + if new_answers: + names = sets.Set(map(lambda x: x.author.username,new_answers)) + people = ', '.join(names) + out.append(_('%(people)s posted %(new_answer_count)s new answers') \ + % {'new_answer_count':len(new_answers),'people':people}) + if comments: + names = sets.Set(map(lambda x: x.user.username,comments)) + people = ', '.join(names) + out.append(_('%(people)s commented the question') % {'people':people}) + if answer_comments: + names = sets.Set(map(lambda x: x.user.username,answer_comments)) + people = ', '.join(names) + if len(commented_answers) > 1: + out.append(_('%(people)s commented answers') % {'people':people}) + else: + out.append(_('%(people)s commented an answer') % {'people':people}) + url = settings.APP_URL + self.get_absolute_url() + retval = '%s:
\n' % (url,self.title) + out = map(lambda x: '
  • ' + x + '
  • ',out) + retval += '
      ' + '\n'.join(out) + '

    \n' + return retval + else: + return None + + def __unicode__(self): + return self.title + + +class QuestionView(models.Model): + question = models.ForeignKey(Question, related_name='viewed') + who = models.ForeignKey(User, related_name='question_views') + when = models.DateTimeField() + + class Meta: + app_label = 'forum' + +class FavoriteQuestion(models.Model): + """A favorite Question of a User.""" + question = models.ForeignKey(Question) + user = models.ForeignKey(User, related_name='user_favorite_questions') + added_at = models.DateTimeField(default=datetime.datetime.now) + + class Meta: + app_label = 'forum' + db_table = u'favorite_question' + def __unicode__(self): + return '[%s] favorited at %s' %(self.user, self.added_at) + +class QuestionRevision(ContentRevision): + """A revision of a Question.""" + question = models.ForeignKey(Question, related_name='revisions') + title = models.CharField(max_length=300) + tagnames = models.CharField(max_length=125) + + class Meta(ContentRevision.Meta): + db_table = u'question_revision' + ordering = ('-revision',) + + def get_question_title(self): + return self.question.title + + def get_absolute_url(self): + #print 'in QuestionRevision.get_absolute_url()' + return reverse('question_revisions', args=[self.question.id]) + + def save(self, **kwargs): + """Looks up the next available revision number.""" + if not self.revision: + self.revision = QuestionRevision.objects.filter( + question=self.question).values_list('revision', + flat=True)[0] + 1 + super(QuestionRevision, self).save(**kwargs) + + def __unicode__(self): + return u'revision %s of %s' % (self.revision, self.title) + +class AnonymousQuestion(AnonymousContent): + title = models.CharField(max_length=300) + tagnames = models.CharField(max_length=125) + + def publish(self,user): + added_at = datetime.datetime.now() + QuestionManager.create_new(title=self.title, author=user, added_at=added_at, + wiki=self.wiki, tagnames=self.tagnames, + summary=self.summary, text=self.text) + self.delete() + +from answer import Answer, AnswerManager diff --git a/forum/models/repute.py b/forum/models/repute.py new file mode 100755 index 0000000..a47ce47 --- /dev/null +++ b/forum/models/repute.py @@ -0,0 +1,109 @@ +from base import * + +from django.utils.translation import ugettext as _ + +class Badge(models.Model): + """Awarded for notable actions performed on the site by Users.""" + GOLD = 1 + SILVER = 2 + BRONZE = 3 + TYPE_CHOICES = ( + (GOLD, _('gold')), + (SILVER, _('silver')), + (BRONZE, _('bronze')), + ) + + name = models.CharField(max_length=50) + type = models.SmallIntegerField(choices=TYPE_CHOICES) + slug = models.SlugField(max_length=50, blank=True) + description = models.CharField(max_length=300) + multiple = models.BooleanField(default=False) + # Denormalised data + awarded_count = models.PositiveIntegerField(default=0) + + awarded_to = models.ManyToManyField(User, through='Award', related_name='badges') + + class Meta: + app_label = 'forum' + db_table = u'badge' + ordering = ('name',) + unique_together = ('name', 'type') + + def __unicode__(self): + return u'%s: %s' % (self.get_type_display(), self.name) + + def save(self, **kwargs): + if not self.slug: + self.slug = self.name#slugify(self.name) + super(Badge, self).save(**kwargs) + + def get_absolute_url(self): + return '%s%s/' % (reverse('badge', args=[self.id]), self.slug) + +class AwardManager(models.Manager): + def get_recent_awards(self): + awards = super(AwardManager, self).extra( + select={'badge_id': 'badge.id', 'badge_name':'badge.name', + 'badge_description': 'badge.description', 'badge_type': 'badge.type', + 'user_id': 'auth_user.id', 'user_name': 'auth_user.username' + }, + tables=['award', 'badge', 'auth_user'], + order_by=['-awarded_at'], + where=['auth_user.id=award.user_id AND badge_id=badge.id'], + ).values('badge_id', 'badge_name', 'badge_description', 'badge_type', 'user_id', 'user_name') + return awards + +class Award(models.Model): + """The awarding of a Badge to a User.""" + user = models.ForeignKey(User, related_name='award_user') + badge = models.ForeignKey('Badge', related_name='award_badge') + content_type = models.ForeignKey(ContentType) + object_id = models.PositiveIntegerField() + content_object = generic.GenericForeignKey('content_type', 'object_id') + awarded_at = models.DateTimeField(default=datetime.datetime.now) + notified = models.BooleanField(default=False) + + objects = AwardManager() + + def __unicode__(self): + return u'[%s] is awarded a badge [%s] at %s' % (self.user.username, self.badge.name, self.awarded_at) + + class Meta: + app_label = 'forum' + db_table = u'award' + +class ReputeManager(models.Manager): + def get_reputation_by_upvoted_today(self, user): + """ + For one user in one day, he can only earn rep till certain score (ep. +200) + by upvoted(also substracted from upvoted canceled). This is because we need + to prohibit gaming system by upvoting/cancel again and again. + """ + if user is not None: + today = datetime.date.today() + sums = self.filter(models.Q(reputation_type=1) | models.Q(reputation_type=-8), + user=user, reputed_at__range=(today, today + datetime.timedelta(1))). \ + agregate(models.Sum('positive'), models.Sum('negative')) + + return sums['positive__sum'] + sums['negative__sum'] + else: + return 0 + +class Repute(models.Model): + """The reputation histories for user""" + user = models.ForeignKey(User) + positive = models.SmallIntegerField(default=0) + negative = models.SmallIntegerField(default=0) + question = models.ForeignKey('Question') + reputed_at = models.DateTimeField(default=datetime.datetime.now) + reputation_type = models.SmallIntegerField(choices=TYPE_REPUTATION) + reputation = models.IntegerField(default=1) + + objects = ReputeManager() + + def __unicode__(self): + return u'[%s]\' reputation changed at %s' % (self.user.username, self.reputed_at) + + class Meta: + app_label = 'forum' + db_table = u'repute' diff --git a/forum/models/tag.py b/forum/models/tag.py new file mode 100755 index 0000000..28b9e57 --- /dev/null +++ b/forum/models/tag.py @@ -0,0 +1,85 @@ +from base import * + +from django.utils.translation import ugettext as _ + +class TagManager(models.Manager): + UPDATE_USED_COUNTS_QUERY = ( + 'UPDATE tag ' + 'SET used_count = (' + 'SELECT COUNT(*) FROM question_tags ' + 'INNER JOIN question ON question_id=question.id ' + 'WHERE tag_id = tag.id AND question.deleted=False' + ') ' + 'WHERE id IN (%s)') + + def get_valid_tags(self, page_size): + tags = self.all().filter(deleted=False).exclude(used_count=0).order_by("-id")[:page_size] + return tags + + def get_or_create_multiple(self, names, user): + """ + Fetches a list of Tags with the given names, creating any Tags + which don't exist when necesssary. + """ + tags = list(self.filter(name__in=names)) + #Set all these tag visible + for tag in tags: + if tag.deleted: + tag.deleted = False + tag.deleted_by = None + tag.deleted_at = None + tag.save() + + if len(tags) < len(names): + existing_names = set(tag.name for tag in tags) + new_names = [name for name in names if name not in existing_names] + tags.extend([self.create(name=name, created_by=user) + for name in new_names if self.filter(name=name).count() == 0 and len(name.strip()) > 0]) + + return tags + + def update_use_counts(self, tags): + """Updates the given Tags with their current use counts.""" + if not tags: + return + cursor = connection.cursor() + query = self.UPDATE_USED_COUNTS_QUERY % ','.join(['%s'] * len(tags)) + cursor.execute(query, [tag.id for tag in tags]) + transaction.commit_unless_managed() + + def get_tags_by_questions(self, questions): + question_ids = [] + for question in questions: + question_ids.append(question.id) + + question_ids_str = ','.join([str(id) for id in question_ids]) + related_tags = self.extra( + tables=['tag', 'question_tags'], + where=["tag.id = question_tags.tag_id AND question_tags.question_id IN (" + question_ids_str + ")"] + ).distinct() + + return related_tags + +class Tag(DeletableContent): + name = models.CharField(max_length=255, unique=True) + created_by = models.ForeignKey(User, related_name='created_tags') + # Denormalised data + used_count = models.PositiveIntegerField(default=0) + + objects = TagManager() + + class Meta(DeletableContent.Meta): + db_table = u'tag' + ordering = ('-used_count', 'name') + + def __unicode__(self): + return self.name + +class MarkedTag(models.Model): + TAG_MARK_REASONS = (('good',_('interesting')),('bad',_('ignored'))) + tag = models.ForeignKey('Tag', related_name='user_selections') + user = models.ForeignKey(User, related_name='tag_selections') + reason = models.CharField(max_length=16, choices=TAG_MARK_REASONS) + + class Meta: + app_label = 'forum' \ No newline at end of file diff --git a/forum/models/user.py b/forum/models/user.py new file mode 100755 index 0000000..9502416 --- /dev/null +++ b/forum/models/user.py @@ -0,0 +1,77 @@ +from base import * + +from django.utils.translation import ugettext as _ + +class Activity(models.Model): + """ + We keep some history data for user activities + """ + user = models.ForeignKey(User) + activity_type = models.SmallIntegerField(choices=TYPE_ACTIVITY) + active_at = models.DateTimeField(default=datetime.datetime.now) + content_type = models.ForeignKey(ContentType) + object_id = models.PositiveIntegerField() + content_object = generic.GenericForeignKey('content_type', 'object_id') + is_auditted = models.BooleanField(default=False) + + def __unicode__(self): + return u'[%s] was active at %s' % (self.user.username, self.active_at) + + class Meta: + app_label = 'forum' + db_table = u'activity' + +class EmailFeedSetting(models.Model): + DELTA_TABLE = { + 'w':datetime.timedelta(7), + 'd':datetime.timedelta(1), + 'n':datetime.timedelta(-1), + } + FEED_TYPES = ( + ('q_all',_('Entire forum')), + ('q_ask',_('Questions that I asked')), + ('q_ans',_('Questions that I answered')), + ('q_sel',_('Individually selected questions')), + ) + UPDATE_FREQUENCY = ( + ('w',_('Weekly')), + ('d',_('Daily')), + ('n',_('No email')), + ) + subscriber = models.ForeignKey(User) + feed_type = models.CharField(max_length=16,choices=FEED_TYPES) + frequency = models.CharField(max_length=8,choices=UPDATE_FREQUENCY,default='n') + added_at = models.DateTimeField(auto_now_add=True) + reported_at = models.DateTimeField(null=True) + + def save(self,*args,**kwargs): + type = self.feed_type + subscriber = self.subscriber + similar = self.__class__.objects.filter(feed_type=type,subscriber=subscriber).exclude(pk=self.id) + if len(similar) > 0: + raise IntegrityError('email feed setting already exists') + super(EmailFeedSetting,self).save(*args,**kwargs) + + class Meta: + app_label = 'forum' + +class AnonymousEmail(models.Model): + #validation key, if used + key = models.CharField(max_length=32) + email = models.EmailField(null=False,unique=True) + isvalid = models.BooleanField(default=False) + + class Meta: + app_label = 'forum' + + +class AuthKeyUserAssociation(models.Model): + key = models.CharField(max_length=256,null=False,unique=True) + provider = models.CharField(max_length=64) + user = models.ForeignKey(User) + added_at = models.DateTimeField(default=datetime.datetime.now) + + class Meta: + app_label = 'forum' + + \ No newline at end of file diff --git a/forum/modules.py b/forum/modules.py new file mode 100755 index 0000000..9c07233 --- /dev/null +++ b/forum/modules.py @@ -0,0 +1,79 @@ +import os +import types +import re + +from django.template import Template, TemplateDoesNotExist + +MODULES_PACKAGE = 'forum_modules' + +MODULES_FOLDER = os.path.join(os.path.dirname(__file__), '../' + MODULES_PACKAGE) + +MODULE_LIST = [ + __import__('forum_modules.%s' % f, globals(), locals(), ['forum_modules']) + for f in os.listdir(MODULES_FOLDER) + if os.path.isdir(os.path.join(MODULES_FOLDER, f)) and + os.path.exists(os.path.join(MODULES_FOLDER, "%s/__init__.py" % f)) and + not os.path.exists(os.path.join(MODULES_FOLDER, "%s/DISABLED" % f)) +] + +def get_modules_script(script_name): + all = [] + + for m in MODULE_LIST: + try: + all.append(__import__('%s.%s' % (m.__name__, script_name), globals(), locals(), [m.__name__])) + except Exception, e: + #print script_name + ":" + str(e) + pass + + return all + +def get_modules_script_classes(script_name, base_class): + scripts = get_modules_script(script_name) + all_classes = {} + + for script in scripts: + all_classes.update(dict([ + (n, c) for (n, c) in [(n, getattr(script, n)) for n in dir(script)] + if isinstance(c, (type, types.ClassType)) and issubclass(c, base_class) + ])) + + return all_classes + +def get_all_handlers(name): + handler_files = get_modules_script('handlers') + + return [ + h for h in [ + getattr(f, name) for f in handler_files + if hasattr(f, name) + ] + + if callable(h) + ] + +def get_handler(name, default): + all = get_all_handlers(name) + print(len(all)) + return len(all) and all[0] or default + +module_template_re = re.compile('^modules\/(\w+)\/(.*)$') + +def module_templates_loader(name, dirs=None): + result = module_template_re.search(name) + + if result is not None: + file_name = os.path.join(MODULES_FOLDER, result.group(1), 'templates', result.group(2)) + + if os.path.exists(file_name): + try: + f = open(file_name, 'r') + source = f.read() + f.close() + return (source, file_name) + except: + pass + + raise TemplateDoesNotExist, name + +module_templates_loader.is_usable = True \ No newline at end of file diff --git a/forum/sitemap.py b/forum/sitemap.py new file mode 100644 index 0000000..c0c60b5 --- /dev/null +++ b/forum/sitemap.py @@ -0,0 +1,14 @@ +from django.contrib.sitemaps import Sitemap +from forum.models import Question + +class QuestionsSitemap(Sitemap): + changefreq = 'daily' + priority = 0.5 + def items(self): + return Question.objects.exclude(deleted=True) + + def lastmod(self, obj): + return obj.last_activity_at + + def location(self, obj): + return obj.get_absolute_url() diff --git a/forum/skins/README b/forum/skins/README new file mode 100644 index 0000000..5565fa8 --- /dev/null +++ b/forum/skins/README @@ -0,0 +1,22 @@ +this directory contains available skins + +1) default - default skin with templates +2) common - this directory is to media directory common to all or many templates + +to create a new skin just create another directory under skins/ +and start populating it with the directory structure as in +default/templates - templates must be named the same way + +NO NEED TO CREATE ALL TEMPLATES/MEDIA FILES AT ONCE + +templates are resolved in the following way: +* check in skin named as in settings.OSQA_DEFAULT_SKIN +* then skin named 'default' + +media is resolved with one extra option +* settings.OSQA_DEFAULT_SKIN +* 'default' +* 'common' + +media does not have to be composed of files named the same way as in default skin +whatever media you link to from your templates - will be in operation diff --git a/forum/skins/__init__.py b/forum/skins/__init__.py new file mode 100644 index 0000000..be6bd4f --- /dev/null +++ b/forum/skins/__init__.py @@ -0,0 +1,57 @@ +from django.conf import settings +from django.template import loader +from django.template.loaders import filesystem +from django.http import HttpResponse +import os.path +import logging + +#module for skinning osqa +#at this point skin can be changed only in settings file +#via OSQA_DEFAULT_SKIN variable + +#note - Django template loaders use method django.utils._os.safe_join +#to work on unicode file paths +#here it is ignored because it is assumed that we won't use unicode paths + +def load_template_source(name, dirs=None): + try: + tname = os.path.join(settings.OSQA_DEFAULT_SKIN,'templates',name) + return filesystem.load_template_source(tname,dirs) + except: + tname = os.path.join('default','templates',name) + return filesystem.load_template_source(tname,dirs) +load_template_source.is_usable = True + +def find_media_source(url): + """returns url prefixed with the skin name + of the first skin that contains the file + directories are searched in this order: + settings.OSQA_DEFAULT_SKIN, then 'default', then 'commmon' + if file is not found - returns None + and logs an error message + """ + while url[0] == '/': url = url[1:] + d = os.path.dirname + n = os.path.normpath + j = os.path.join + f = os.path.isfile + skins = n(j(d(d(__file__)),'skins')) + try: + media = os.path.join(skins, settings.OSQA_DEFAULT_SKIN, url) + assert(f(media)) + use_skin = settings.OSQA_DEFAULT_SKIN + except: + try: + media = j(skins, 'default', url) + assert(f(media)) + use_skin = 'default' + except: + media = j(skins, 'common', url) + try: + assert(f(media)) + use_skin = 'common' + except: + logging.error('could not find media for %s' % url) + use_skin = '' + return None + return use_skin + '/' + url diff --git a/forum/skins/common/media/README b/forum/skins/common/media/README new file mode 100644 index 0000000..3376e75 --- /dev/null +++ b/forum/skins/common/media/README @@ -0,0 +1 @@ +directory for media common to all or many templates diff --git a/forum/skins/default/media/images/blue-up-arrow-h18px.png b/forum/skins/default/media/images/blue-up-arrow-h18px.png new file mode 100644 index 0000000000000000000000000000000000000000..e1f29e86334ce72d2d28989a133571d7bf53a94e GIT binary patch literal 593 zcmV-X0{6iqad+$V|KR(Q{q;Ok;ua!jjQtuI1z7)O( zySE1i5k-TnUMhU^!Fth&L`=(4hnCB&mew0>atrF^~@9muPo$nmJ5BNLwb>CS+ z;4aaxR`*WD1Hfmk^?b25_^vW#;gMP0a=;-lw8uKYF+TucXlk2wNsBS@}pFD ztaavBN$@-;DyO5Dqi0!d9a~=U7;O0Ayiuq>#Jyi<@Hb@35~1ra$v5dmgDzqB>^+=uOM7b5+>fY}+kwy1R`cV__*wFuQ!Bi@#VE~BJd f7UnWh{5Sjn&bP0xmAf5500000NkvXXu0mjfU!M$- literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/images/box-arrow.gif b/forum/skins/default/media/images/box-arrow.gif new file mode 100644 index 0000000000000000000000000000000000000000..89dcf5b3dd40fac0e6afb0b1a7ff899a059f923f GIT binary patch literal 69 zcmZ?wbhEHbWM@!dXkcJ?_wL=lfBzJJvM@6+Ff!;c00Bsbfk~#Pf92`7{EJz*0#eqW WoE7iinDij`X5pe+YFj-S8LR>Nkr%B1 literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/images/bullet_green.gif b/forum/skins/default/media/images/bullet_green.gif new file mode 100644 index 0000000000000000000000000000000000000000..fa530910f9dc11fadaa2314f72bd98f29df39daf GIT binary patch literal 64 zcmZ?wbhEHbKLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=00004XF*Lt006O$eEU(80000WV@Og>004R=004l4008;_004mL004C` z008P>0026e000+nl3&F}000VaNklJyRN8J52lge3Dk<@*>AXrMmGP=H z$s9P2&*V2vPMX-1L}oIXPMP_nlH^n>#fZ%$`c@)Y4M0h*N)5H*Dy5UhPr>W;VqjnZ zld(w!&=SoOtX;bn<{y|*_V7bUrc~hhgft*Y9vryzSY}M06iG7y={dwn-rQt#5xb>+`RC^f6v^z4(Q|d8B-!MDTk(XP{Q8r-@ZB!XmXA&5bSa9P_XS zTCEm>AV9CzqqMXX0MOIZgGeL-kH>?Fi3wI$un4HD--X4uEk+`dKs=E^A`wR-kx-fp zO5tksDt`IIQ;5Z45Cj2ErxSX;UeVb|B!b%7T6jDjXfztMI$CGF)V0_(RPU_$!r;hu zn9lZ|rQW`C)aUP`!JwZmydNaNB2aqrl2)swmX;QZMx!*Bbai!+R;y+0iVQ_`zOR=C zgM;K7?4#bkbL8zkOJ~GxdiQVd(CO||bmE=kWH1;A09mb81)hvE^BFSsTgU&bNJWOC ztT-;GleTW!d{a21qod^O>my%ZAB~QV=7j@*cmPnPZLnIUf?B22T%*y5Kp=qT=4NQM z+BwaG)oMjJ9A;B#Bs79{#}S1s=b1+$c_V(GAE8hPf*_!N_wIR3kkx91AP5)f1LhU0p{MZ^CXrv4!x zo%i*U_q?RV@4WXHG89YN8xDu_#t(4No=U&Jo}T#mV+4R& zT3YDP!9xUqj=uF)UN~}pe0_Zq-ks~{XWMO5zpIW`EYs79WqNWsojGx`Xt1Qkj%OUq z-gb3$&8xN9M61=p>-Dm>jzf;LFP+w4F6~qEJVKX3FqurVY@E$zV_I&r*%W1Zy&fi$ zNwKnrLK03{Ss5PLxDjJxV>r{@jTOuE`0wO3Y}U!qMyXh{a+E1ug;r z%7vfc5AII1wI9abrbYmOyR$1Njx4Ng@pzP#D|w7a2#pJ(xpq`?(9P7 z(xtp`WNCd?Qn=s$@fCLONB7?k0Eoq6`1s0|oH&eNS)06GFC(YAx|&rIMG+Q@1!l7u zX0sV~yFF)FM@NUkUdmeRJ?qWTUJe{TP4H(Sciwdu+S(6ebaWK;yXsK#z1#8j(W5AP za099~Z(%lcF41T-aJgK#O|t|qzx;C6I-h)UeNGH%qrq|DISvB@1Com+2#gp}6k#@- z6^%qugeZ!ruC9i~VwqKT;lc%Ub#*Z;x7&?@fdOd;m?l;tmt%fqw#7`=T&vYOt2|qq znfGF`7|wKeqpkfgte0#kdvF5)px@_1zt4xl!a{J1)N|r2Qmav4SBKjcFJ|}peLj|7 zreBt`KN%V&|pt!ggq9`H~i6}ZI575lK?1PG;2!p|(*jdfQ`Nd<8qjleYth#+Asy1)I zhnFL$sH{X=`(adV+m;v3*w`3anwqh7(`K~p+mF_L`_a_U007v%XHU-lCm%bWIiI-6)J@3jZHeAiPP48 z7`7cdaOL0s!au`dxSUSxZE8eCWhLwf_M>FY>UnW&J9fafV}~@*ot^0F?8Nm?u46;l zL)cQaHD`YmCgSsyb?eu`P;7w5^H-xjdsS!VZ-~n8|ENOLdaWM?V#dG2)HJFr9tW0|%YqQ(!#>B(~ zg25n`FJBJ3-OkG7ZE|K==5(HpkFp;{CmLHCq0wmIcDrG*SXi6PC|e{FfyH7$M@I)V z8Vz<<@0|5AdwyG=6=&D(I=ueo8#jfsdd+H7R92#*vJ!^k;=FKJu*W|x zk_klrGl7HlgUpvS8jUa-jnHbf5Jiy<`g}o}Yq4u+JLq6Rn$Hu51$+93`cY6&0DF@i zo?{+pwOT}@QH5vVa=BPA$7C|0xw)AQfXn4#bu-HVVEOXph(sc|?}zu{h_fBnr=}2} zL9iS*P9;<$H0nVR0kLsr+|JUhsq9(&;-H5o|Zng_MaPR;g z-0%RV;!~K4$B~G~5l_q@I7^9PX4ba;*Y)WDdvcoO zj3Xnq$kMHKWW*MC0MyXjK>pzY3SJD*qd$FA5m5^S0t2x}BJ@2C6#Q+Tk{q)lBUdp@e%wwXN`OJIs z;mp@QAFOP37K$-TgJr3Cx*Ap6s@PGF=0 zojBKXZow~eGp1oalQ0+lLI*8hj5!IG<=)bJv7u}O?pS>XzM;{iKb@L3Fhqo_u^5I1 zhj9Aj=>-$}S!wXi+Ti)tginp&ujD2+8btKz>$5D*Z6fPgSCFq)s4ARr)MU|_7VtMKsfouZtnuBfQ3r*Lp^p{JlwP*A0- zq@1Cfpr)U+x3i3qjFXs>m70`}la833m$0y~v9++Cqn)6jpqHGMz`(%8$i=R+t&Nh5 zq^hHon3I;9m5!5*kCcwBvaFPulefCIqo|^rpqjY6xQ2>_tFEfP!M(q~zmb-Z!NkF^ zwXdY8ql%A-qNbsRiG-}Lte>Bs;NakWfqamakWo`mhl_^E%*n~k$!@r-RpNfu(v9z$Sv#yYkkeQyCudlC)kBZUM(1?tO zq^G09#>2L{waw7X)7H|dtf|k^&Z@1dwYap%%g3Rnpm}|HoS>VDj);<%k)^1lkd%*< znUoe67OAhPkB^U=pqrYWnXa;~tgx)LxwV*_my3^z@9*!Vs-!|hLJJEE8XFqAy}820 z!LYNi*4fs!xwVOnh@YgMczkyP0|LLozM`n1$;-&Gx3G12b)27@&e6@Ks-%;clc1!Z zl9iELU0PIERKmr=g^Gl;xU$R6%ErmX(9_Soz`eA&vvhZJw79gjyR?6Tex|CWKte!! ze|o5^sL;^RDk~`}EGmwYjcRUc3=9mnytX_&Jifucp{1cyRZ^m+qKAxzv9+<2my*1| zySTi!USM6Wv8@CI1gx&B#K*);PD|9+)XdM!frWsAg@J;Ef}NwCwYjvIo0pN4k*ct% zkd=>?nw0?o0Y*qh4h{~2hk=ffjhdaB($&(6jfskliU$Y>VrF8;%Ewe!Q_RoHvbC|Y zx3QF$lY@wYo1U6AHZ_uzl9rj3L`6i<)6kimm}F>Vnw^=Ko0g)bp~T3;Y;J7I%*u?A zi_X!`p`)OSkc!gP(Qa{V)Yj9fuBnickDZ{LGc+@`xV4UvjvpW&CnzUB|c9N&VVzJZykO{A@7ep{vsL1vFKYgqeXubk1PL(42z&9igG8PKEBsFPIbL zzuPQqj4CqrEMY-TigvwMdElbc@~G}?X^61YtXXhr(%*jbi-!-Jue-W#b9KF5yD6eD zG(SjF64G7tA(Ghdlk_}`JVPm!JQBK@x7?q+>*R5;X>8YkBG!65q%bTk1~iJr2WW=JJ~jRjbon3xmwrNDO*a zj8qu;_Rc+*E`y`Yo;3ev)#}vR9aBP(_;`r}kZjp=;#qK<=jP3C*uK?uX;3q;pvPI9 zfqm`VvjJda^Fw`W#GE*aSdf@^{sBhL;jbH-(rWWdmQch)^7j?@j>9PVXg+opnq?<$ zEB*U?*A_OWtovol6lT zk{gZzmk$IR0h<8$b?RJydzC)N#DxPSfrKCd?xt?Cku|+AVihEG6-7Upu?vBKBf$Cu zhrwm5!fP=j1=+9M2bUr7S=dntD*$k zBIB_rQk#DTXz~UC*U81i#hzSTXaQUffIxw70&o!(96pZ0_PpRzc?`yu*-L}+fPRpx z$4E|w)*YmXYFXy2v5~I|H2aWvTgm>B09zIx#^D0y#h~+1G?8bn96<=Clh#zQq3_~? z(ikgw8p*gm$AYvao&D-r&E%LRNc1YXa-738Jk2 zgDjl@emcqSE0v0AqEYKv-zpnfBN4j2HasE2AdIx$F&GZy z)l8%aPwjyua;BpyiV6i6DrDnH0Z9Ke)lQ^BUk4_hlmRHx`M?e%`LmSk^hmTio+U&2 z$k)xyZ8$@a{i%TcQR1~oD)CpW%|rS~C=~i08|KF0*u``t>8lCP!SCZjqWlHYe#aCHq+7okbzZ<)Gb1){Qx_Tj|xVU(lHGjqzmFVb&=VEhn z65E0GTai~TiOk;xxGNv*6zP;~$ zL=pRI=o}0zK|=?`(YY`y#vshVBMqwZlP@K6uJqQv$DA@VwuNcj!QOze1Hx?ViXp=^ zM(qw5GE49{VI~IDqX;;t;^6i>%F@`WJ_EYme@8^MqmJhE400000NkvXXu0mjf D3>(Ui literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/images/close-small-dark.png b/forum/skins/default/media/images/close-small-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..280c1fc74e47c0e7d1c68d6f356eb22eeba7a2de GIT binary patch literal 226 zcmeAS@N?(olHy`uVBq!ia0vp^d?3ui3?$#C89V|~1_3@Hu3NY7ShRS_jG43c?mKYe z;M6!dxb8-^+kP+MX_sAsp9}6ArM>V3CjzZ7^A^)I5Q~ t!yzC*Q0L$Zl?Eqn$0Lca5`Jx9W=NmTc;}pLNdZtBgQu&X%Q~loCIHsxKFr1QCT0q{1{MYeDE)e-c@Ne1ia=5ZCX&{{Hy&_tW!N_ZKfc zpObUM#pTWAs~3xk4w#ty`t$F>lBGXhy}DLgdpr1Q#zqRd1{MYe;g^zIfZEtgJbhi+Z*Z~+S_nO6;N1-r z;wE)e-c@Ne1ia=5Z4(qXDwR1Wb5`Fd-okU zaq{%lYd1c8`uyw9U!Xizkhgxu4WI-^NswRge+Xc>*St3pD5mS_;uunKD_NtRfsJRE z&YnFcvL4forc1XpAK{H{IrvG{>z#&4h|Ds+W&FE$GCZx(SSr0JY6i#_Pgg&ebxsLQ E08gA+X#fBK literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/images/dash.gif b/forum/skins/default/media/images/dash.gif new file mode 100644 index 0000000000000000000000000000000000000000..d1ddc507fe00bd654fce38ac8552793aa18c9966 GIT binary patch literal 44 wcmZ?wbhEHbWMN=rXkcLY|NsBAY10&cvM{hS{AbW%00NK<0~50k11p0y023w&(*OVf literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/images/djangomade124x25_grey.gif b/forum/skins/default/media/images/djangomade124x25_grey.gif new file mode 100644 index 0000000000000000000000000000000000000000..d34bb311615b1378a672a828c7a7916490cd882b GIT binary patch literal 2035 zcmcJOSyz(>0zk8Y5oHk(5d(s_FbFhAqNt4L16&YNCyID%iyA#v9i~-V)T#$7C{?Rq zJuXmNkX;Zk3t36XzAs-E$W8(YK|~TDELE}gFw-9~_w_#9r@MFGo{ViLN+D5@_t6k? zub_>}Y9`ZKyBS?Q+zuw^CB28p=9m6l=^h>P&(87W>M^fRs?m>(j#(W;9ErkUvGq!o zR>z=aV9@3qVhSZRzIguQM;cEA4PH>{`)!UPyVLdOr%&oW6O}KX^iRvR21>7B_K!bg zpuy#NEl_HdeI}XK;2M6dGg;OB7NH7sx`ycjiOOJXqcVp_-!Mh8P9}%M;d7*l9dn^o^Yc8JiXo6#2L{zfi^DzK#p>;|ITX6Se!BxS zS=nOw+`D&V4qvF$^jqyzo~TDG>y;|lVi{d1<;qn?yF=A))|stRQ2&y_<|)*2U7tjw zV~V8YUSW?!VYCi#Byx?(%9SZ)x_-xyo5JBc-5#D?H9GEn-bvP4>_a1?Mw^2xQ-LN+ zBe7ec(5U;2FS?oT(KkAyXb zH`<+SiGsov4vvg6M6xClb-?9rrO;i&BX+lk$mV$`rZ`gN>v8YfiAm6G<101ow4OKP zUbVqQ?BTjSuXPq%Yd5pISJ261cQQEjo#Yk@y{?1ASAq*s-`cubDGW-lki_O|%{D4etT9^j7JEC5N#je{GL^~hpbMnVVNY!b z@p%VHZ?SpC-cHTTm>h#mB$`;Qbq9D5VJ`EU_H44c4@Fu%*g{g$2m} zjre&46atBcB>!Fb`w0kg0fIg2-$QN?-a;-U9a~C|Sx_l#$$ zbyds`pGKe0LOZIh;hTXQH~7?2Jo0 zAKTtHp2*){xDdLxDX)5Bq$d9S)rRYTt~R1BZu8%arq9oL7S0X{cVZ83Md|CWuc5Ww zgHJrm4n{311;Egli>`(nMilUQIqb4)5Q?cl#}=HzL03S&F5J5ZvF3I&1iAmphxib7 z=~(%y)r?(&s;zbaOiB%VcyqB)a3LW*6`!N2T5$*vtPDAu4Lr&i{1t{hUIbJv$v&)4 zc!CfB!4I<5zFha<24-Hg2z@rDMTRf&8K%(xp|Jl;8yyb7(z29dO~7 z0?>d2d_u(#9&?g)BczhRg5UXUk{R}HU+hrKk846$t{)2uvfo5}`X_+FT_r3*q@K(Q zIFy3^Nem&SdwmdC?f@$3a69--AQ=DME>cj0|A&0oy86<nZyuyLVuwr{IGXQux+DRXY@2U6(<)cxduLt8LR@#Wa`O=WAre>^=26DJK^^h~Zenh_Qpc_eE|a>K3SC}>hGS%%baA>Rzm6~eB6s0c`oTN0US zfK?FULsI-Xr|IOSn-WhZ6RhFiUYWj6SlS#V&y{WdZMyPn->wo=`JS}ytYDdcZ9-8+ z#g^jc^%g~Q?0bcL=f~+^%9ggo{bjwMjFH13QK!~qEei?la)%F0L$*X*t*SoddGxEM zWy4mba{slN#|^%bl*nZdHaOy*)Xwd`S5&UbXN;^SSGFdf*{A-}K2y!otWP}=A9U0{ sv!XO(598i6?6_>^sSNjGbB$7Sdi(Nfq+XETwht`+l3E>&L_&7_7sn(m+5i9m literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/images/dot-g.gif b/forum/skins/default/media/images/dot-g.gif new file mode 100644 index 0000000000000000000000000000000000000000..5d6bb28e56377b0eeb80899222aa3290ec2a3a95 GIT binary patch literal 61 zcmZ?wbhEHb1;oomohA-cw8MruKF$f7>W?=Zo!hivofRLHNli}wd9fr@0yBS{oImf`r!T=CJ zObo<=KYvmfSa>QJq}1IQ{xNkh{QGMS5(Wq$!Uiw`?fv_wh=H9yhCxi_BLl18R|YAy zzYGlQ%?$tlTLT0TVFP~usbb(1@&ju6$nf>=e}=c;7#aR?|7H-C1iI?46+i&7;IWyL z;n$xU20l?+1~CPo=06M!Uw{_>X8zCc{xt`~yO&=Xm{>Lg1Q0F*xPh8m83e@j8ARki zGJO8U!0_!K1H&(%gWtd9WO)AQ6T^RoY6eEe8vp@>#en~UKxelz2uZ60@n?olzZn_6 z{|9LXI{!5n!^+`mIW?f>FASf4GBSJvI{OzR z1H&<(HAFtjse0v3qvG*Lj!k<6yW@2D$#+OV00*L7y zBZtO!W_$i{sO4^#Rs-O?>`EBV-VT&i$$;vm@)9CGk^eM*?-}6i0Jdne+|V2 zxHvw3_{Z?+(@RDM_T7w3%nJ!=1_6KoVp+N8l7OGV`-jpW1oz1C@m=Tm^ZxZW7V#J0 oz$F3z1Q6r@|Nj{%0RRC80Nya-9-e3d82|tP07*qoM6N<$g1t^w0{{R3 literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/images/expander-arrow-hide.gif b/forum/skins/default/media/images/expander-arrow-hide.gif new file mode 100644 index 0000000000000000000000000000000000000000..feb6a6187c2742ea8e516244f139e7946ed757fb GIT binary patch literal 126 zcmZ?wbhEHb6k!l$Xl7tYOG_&#C^&ZP*n$NM9zTBE+}u2E+O#ug&iw!Xp8*?C{K>-1 zz`((v1Cj)p!N8&|aME-2UW?afcmGe&$YV~Jr(qS6`f6R+>scREmz$PdzB_la&4mZ% LT2lR!I2fz}g2F4- literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/images/expander-arrow-show.gif b/forum/skins/default/media/images/expander-arrow-show.gif new file mode 100644 index 0000000000000000000000000000000000000000..6825c56ee42f0184d66c0fe954d7fc4b6f05e850 GIT binary patch literal 135 zcmZ?wbhEHb6k!l$Xl7tYOG|5RZr;3k^RZ*c9zTA(V8Mcdf`WVZ?*0G&p8*?C{K>-1 zz`((v1Cj)p!N6i7aME-2(x+KmpRPH4k6ma{w6t6)bw%IW-N%f4X5Wr&{_cHgU%a-) Votr)GNqpOrH0{qn(_mt-1^|9hF$Dks literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/images/favicon.gif b/forum/skins/default/media/images/favicon.gif new file mode 100644 index 0000000000000000000000000000000000000000..910c26660ca2088729309bd9286403237c68f020 GIT binary patch literal 3918 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 literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/images/feed-icon-small.png b/forum/skins/default/media/images/feed-icon-small.png new file mode 100644 index 0000000000000000000000000000000000000000..b3c949d2244f2c0c81d65e74719af2a1b56d06a3 GIT binary patch literal 689 zcmV;i0#5yjP)(tky!*UETcH-TCU7SrqEjJM#?B`_A)!p7(kFf9-P@=@15kkTkGK zgFusyy#KECqZzRdBLb=P?$(kUP;>kYTDeG&{|a+iOiRbI6nbQ)j#7bOf>iF=C+|_py<&Fo1F5cC*iEM?zZGC{ejNg4LWYp=S$L6Qaby6y zp$+F`250{%tU{Lg$5*ROH}y!1UKJS4*xqd7P(Y3JQF?lrnf?yerr%&6yGXLG1ur*B z{$&R1@Oj)yl@%rY5rh?j(j10Yz_DBs`AKFU_QnB;)(aqQmGi&ieOS|21^NP9UMpa< zU&p!f6RZ6Owp^X!EXA=0SbN&h?CrQK%Q3(=YBqqHD^9ZUM0Hxt-6-KT;>lf@j?Z+v zHm(}`>85I&E<7e}oz?6UwjAogowzGO8kSN7+2`b^$Az9L{K5*ko87EV45LT-`_##3 z>d3AGh@>=mbg34|6}+-gT9N+6Dr@44VEl44O&{&|w=qpbzC#iWMKa?5)>tI+KLQK@ Xq0QFqn(9Yl00000NkvXXu0mjfZ8tBj>^LJ3~~S>i&IO88=qln{V-q zOM40!3-puuch|@DVB6cUq=Rp^(V|(yIunMk|nMY zCBgY=CFO}lsSJ)O`AMk?p1FzXsX?iUDV2pMQ*D5X*aCb)TzBu@{r~^}iVZzqfg(&L zL4Lvi8J=!8@B;EgJY5_^DsCku9AK6xWM-3k!OUU6z#qV1KKathOF(%BPgg&ebxsLQ E0Ke)mAOHXW literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/images/indicator.gif b/forum/skins/default/media/images/indicator.gif new file mode 100644 index 0000000000000000000000000000000000000000..1c72ebb554be018511ae972c3f2361dff02dce02 GIT binary patch literal 2545 zcma*pX;2es8VB%~zPr=ibVMCx-JQ^BhLDAsK)^**h(ZDp9YGuzZ%~j!}+w%FI;|aC7){7CdVvG)P{bng1y9Te*f}~*`1kQl$jwb z$tlW~rRS!X?#xfm_&6tTdp_`cjgYwbRFLNdoJCN$S-yhg`ZnC-yvedRSmOh%;Y`Gl6bY$Z-}#C=#F4%9!I1b zWQ~f+9P?;vhCxWwlwl=lrWG|7IYo;{jjmzJ5R9?f>n%-d@>kLINUc z4wM5dAO;kq<$}Dk{2-u0$I6@2N}&cUx9nmV1dYc8jfC}%=F9WCg^OQK9C6poh#2!A z3^EU*UFZvS^)?bu3T?J;@Ahb~%I?+@4!l5!*TjC}GIslNan-RCrrd~PdHYnNLJk+m&`$Y+NV(e>CCu%R#_8GqY4cv#j`#uRWdsg9DxWy(?oOvgCU}&@jy%c!H&-Q zqXJxajAtmQRoRa9V-RFXXh-bK*;Fum{BjpkYQGX~i@OZ^Dx0n&H}kvGKqQ?w(6iGXu_g08T|_hp#ZvFzIwKF*a=oMJ~3UGAjZ?g}GOxm44td zXoyYrU*I=y*vHv89hkYH(v5R#wc)BC3dZJKb3K)f>zaM3%JP(mpecViP0eKKYf3zy z->jx_mc?mCtPEvCQ?uppk?eLJt}_IR7giW%Jr)RyI!+E-voIs*lXI*z`GQc_&D#X( z{6G};HPYj6O|$lXxBJeDaweqa{4L=tOZCjTI^&UOxXg})LRG_cr^B9Rqt(i5ORbQX zq`_xCRsH>xEYY%&*Nyi#{S_JZNlTm#K56`RI%7^amom;*h90Si&g1CfaFV3D|a!`3Y-GKKbL*KSbl z>I96`TR@CqPJl(>QqB~RvK~-U)`e`l4LIqj+IU^~yyIe*|BRVB>4Bup%j{tLdKz4j zY^<8P8m~GRGz*yv0&-RJE+-keJ+%m3wNeopzsltWd->eWmBVwUr)pX` zK~CD<;~Z*Uy3W`3+MrEYxm5qYQ!z%YI;y7DTG`UVH0;@{M{!B&id_}3DBQ?zsotuR zEGLdRx25nLm%-wjlnEi;-aN_1S7???rO~WgA67jjr&(vRa3y$u#kqJbeKnw z{!T!1li9>M+sJ6AUe+*9d}2uGjhzd z|L1Rtp8uTGYyZoQ*`DS^m2dw-X{a)l+3m?ncvn^+O>)hdd3(hMtlhkRGns{<8c0I! zDDjpmwtj?@!6kA|iu3q+Ai;@JR+ zfk+ln&YFC{4bhK6IxVgLs4W%^8Lk`qzWU*L>yq0A3;l}{!wKZ!ue)C)SKI)9dl1hl zhIRLV@8E}rwvE{gX(}$f6x*k)_`*Ijt1=EU-Ls6-(phomeQBgtUs z5Xz~Cd*nE)Ac!0i4ep}Z1AugMB(&F?)#CU{Qc{Sp^vKsdL}vRB30H+Bbzrn`M##H3 z{W8dc_mDroEE+p8_}mnJtzZ4!RNe)zhB)Ds;S57nYSJxtek>^~&(7B+N5MPf2+2xx z5Dl&4X|c@f{Kd|z1r+N|$DmsoVp*3yOdxT^J^-VAk)Z@$4^XrPrFP-Co+MXZ+KJ(W z{JNYvraLLWA;&tRhIKOvhW|HC|L-dLvAUF(MG0(Nl?4tB{RzN7I(}Cb%hwN{crFC8 zji#aJElKvDFV+&VI1V?oUMA>*kto0^;3W8FQBSZ|{ z$v~TqE=(8DZa^i$^oht&h};P1N&wMXorKh*Z68gPV&ouy>%f36Oqkwemyeas$Qbz# zV?7Jy%o7KY6^I=P@eCji%W`o5sf(5hySYo9$l4e2`(hIV_?=H-#R6}0$WVA|*(K@3 z=5?@RlcLh(meW%A4)hGzcvEpm(_w?>zhL*i&s9$2>r zAtk{8Cia|+Y+V!uX9BtpXoF%lswuRKsM!pSs!?yhlCy!269K0|b M?FSZn2B>%I-}ej|s{jB1 literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/images/logo.gif b/forum/skins/default/media/images/logo.gif new file mode 100644 index 0000000000000000000000000000000000000000..ab690de2a1c9679f225d80560cf5e06f3ed3cab0 GIT binary patch literal 2114 zcmb7<`#;kQ1INFc?}kmzHeo{yGuL*6N$J$gW^>CWm!Vv8cjzehrOw<6kC^+A;}XMS z9hWqvT#jUODI~OxD7lnMhwgPeub%(l`TX$y?frVaKMZ?2s{=tJfE6$U{MhR1YFJoU z4je8LiFS5&h(zLFDJkW6ypodA(yd#)QBgD+O_H5mT3RZVNZ!4BCvQ8_ z_=gUO#bT*c8W$IbL?S&rJmy@t@grT|{Qq zh0K_oTq=kM3b=(hHB}l|Sa{x!2he3@YU}FjC zB2wPg7;jqo?5x@bucWDJKv=WFQdv1#(Jmiiu3QehM=d1M%>Y4%J=mrPWvvHdlx41Y zW{_!hs0(#@wV*PUiDa7^E4ZQ3{n41+MvK%~K&xm6t$i^2870Ve4Dr-LN8G>8!a$)k z#3;u#TMRXp#cJ*{wMpP_nt9h$(PgvvlCQ>%z1`Gcc+knd#Ox>?ik#ISUP7 z#GoF#j`(()T+*;pki7QOd2ZU0?Y^JHaexWK{?0W{_Z}wUF4j{TvesPlwUA3#aZB+{ z7)YI}-H*93NW2`4MCuAPrZFhsV^1KID+PK(qmxx9HT5C{j_CnlC~n(<81SRAjmOZ zuX!fW#(kJC!;9E#B$#R1gk4Z3$`g2l&BG|F95wx6RDusfYIzZrf6-9!GW!ob- z5RpcTnp9Vz>-kOzC5~=XgNdh_RbUiRA=>DONAs;-F0M!f=HIj;uSn5mhkNdLM+7b5 zttN!evt6#TvEIC`GlR*a77ZMsw_@D)ox!xX7K_MBKlj;Xn&yVn*X$4(SGL98H(cy5 zf&^ht)zxu~uYy^YePFgdeCt_N^KETM`kCuam-B|;BBo=y&wza0g25i{{z1s9;c@Wp ze&a9Xi9#Ln)oD&b(J~Q9+94y}IMmm!fo9$8Q7O&LW-g-snULrAq*>Yf|&p0Y!q^tJn8k z?tt)jCuQEOXsK&=#eAh|voSsiUN2D@OH%{qg&)3si-LPzC+cf{8u&i1dX~zyBg*eu z-bm{Ad)$|uB+vu0aLRKWOl$|hrg5OXRto?t> zPs;`j#YI+Ta_ntCcl=0Gh^xqT($7(jnEs-ezc#2&^Ib?Ke5DdG))S zm~`)wK)`^@k;ex?RTinBr$R8jl4C5Hx|F5vo~7Fvlqbk2V~kOpuBSdP8*NKXa5^7XcD8 z4QWsdI1r7C`5UFWdm`=0ARYoS9W+*=jQTz@E6d4j+#8f)PaQ1c(kmPH2B$~@riysB z$$I-rbHGrePNSaPD|@pR$87%nr|^CQ@#pJ3qd^@(#WO=IiuPXGuTau6W^wK!GvRWYySSZdh?2JAdxm} z4@L^i%LZu%cDL5Y!_|r^6v_Ot5!AE>=`9+=W)GB#c ztC+B+x0S*yDC=ru#=O9JeRGiAsbOSn}iGDZgXt#=z(6UXyd@4U( z9p+K1+}PylDVYqeCpV~jZunedKO8Wcsk6P?$beXTjnyac40o+ME*)eE_+<1U&jvr7 z+Dy~YW2K+-em&vy#b^9N_x0_KjWC80YkmdO^^qoUe<5Hc8J!dr=&hPb4JX4f7B{LM z>bdE^oQoKI)chgd_$2z3TiqSyiUj&8t0wh@7R^~vNn3ykwMuQi;n{m$UugJo7o*wY z%44O6^@w_RU$I_(C>NM#?PUynb#!n{pa@KQ2Ee)H7 io>#?{QxAKS722~IPR}q^pN0&Mw&!{uAI1ZK^?v~_GK-J^ literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/images/logo.png b/forum/skins/default/media/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..6a250e35b34cbe9113e3a62d17eba05d05c83888 GIT binary patch literal 2081 zcmV++2;TRJP)00003b3#c}2nYz< z;ZNWI003-IOjJbx008gr?~jjw(EZ*psMAVX6$2C~zj0000MbVXQnLvm$dbZKvHAXI5>WdJcU zFEThUFgb%jHT(bo2Lee%K~!jg?OBPEqc{vThnq756B@|1A-n(ok1N~qO=!}S%xrb7 zs%AnXOV+av%Z4;Rgvg&Z;8pZLkpCJj0h!K4QJkm6@$+FUc8A-OQi6Xt%-^$d69qud zrxd9=oqguv19|t$ZM&498}Rn8vU-6$Am@8()7>#@-rKCLQb-rhd811lAoD5p>156A zaQ9oKkShLu{5?SC&%mNI{nR#%&ima)DWqNC#`giy7FByl+f(J+Bwkd-4HEuxPp6Gi zNSIuQ-vI>Yx~sL`VdQ_vKPShM-9{;-TM&@1CIbW`XK$8LCHH9fpT7JU80bnNT~y5d zO1ejDObukN7M4ufREgP(aGf_uIm3K-Pf>JD*4P@zxDXI3a7Hu0+wgWPQaf+h<(JZZ zU}PH*nX~?uEn`~*l+2O4o36`JlaaE9%_4i2x67|7C)fJeQQ#g!GhCw+_UcT9a+6)uG_ly+sOBy=u@=MQ8&dWkv+`CBBPl`GVBERwcyH z5Xd_~gxk}F0jV{)g`}F+q-I+RQ(ZlT>26s`7`lMzH+|#+c~FF(0%CWE)oruXT}M>4 znC`+{Sm=^_q2_NojwA1y?keBdp|y;|3$5@p;b*~4cZeSvNGLVgQU^5}_r)OH!Prc$ zGq;=ng4Ik08RR(W@ZnX-jx>+ByG=?etf*&OYHu`8i|G!ww(_ZiM=F0}WHMwvSAN4* zNT!9`SYV$xs%>UnYEFIf(_LC`J;vY=#f0?8p8j)>c@g9?{nHjgR9e^F9mXLO5&DipB* za$E!=3NWj48ecQqEF z^h|5j;;(@KlzpWQI{Pejwko_X<5n1y_QC6hMmtm?INrLqj`j8$SL(gng>9DnVBWWP@tLX2)6p(*jrgM=V0C^$Z#*eV-zMJ0l4e`w%+CuuVA44fw zXac%NR~MS1l?uP2j;ih{GBZ4-%8&+v`)D2l88UTG_K%^1LPqY^4E^m}D)j#XZ?0ibzk}2RDE|wMJc%MKJ>CmGMiea@KrJR%nT+qU74< zTzQ$d!t_qwImOWPM6{DU$%H>+`6pFolsWG@fWiNDIUQpDE-x3=HGL1iB92OuF=d2M zRq{{+HC^0xWl#9E9L^bm2@%e_4n*EtUP;e{6I$%kSOz>M+cwFhBvV(MjMRK)ew)Sy zp$Ps_(nFWd-}Q^?07Zg_rJ4<@9h9na@Kj4mfZgZ3dt}a>y}nvq19F53@)CYZaSxMY zJDL#Ai%P~CWa3bxJdc>a^BI&e(V!{`AL zgpYOR)bp+a5k};sPZF_6E#CQ|f!q>g;!r8vG7t|@L>=pg zmi0WuKtKY4u>&0}b+EK8y>fK}xMN^2NCsUm(j8SbO~%W?Q1aJD(stgQn}IYm)Az5s z@QXr_!NN+WQCMOT7?}K21M8e15b@4Y7xTln*hlzrHUleb5mhqilq&97xv2oqBe=+& za}|h3RByXb3?oEZ(tzpwjk|J59iA%@4GGC&{t2xU!@Uhk4vd&gerq0o&aFT~T5Mmw zMYLww>UJIV7ZBzp8k^$oP8y>jTswDJD}baok0pFnmG00000 LNkvXXu0mjfmiX8& literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/images/logo1.png b/forum/skins/default/media/images/logo1.png new file mode 100644 index 0000000000000000000000000000000000000000..d79a627174b08ee90776540abad2e76f28909652 GIT binary patch literal 2752 zcmcguX* z1E?4wgA2<ibF5B?oZ9u^18n1*~xFBuz3-<%yx3)#_ftUE+Qo`d)qPK67?2S(r z0zbj_1R*mtDSopsN*FIl+1W1i27nS38t4}z)g^qLp7y;lhYt7a0jSl3L8?S)fQbgp z9GIgd0|RA`d?7_d00faod_JKJpnwt1uSD7Z2a|-B1yhe(GKu=epgmLHL<<`k8h-xr z#Rdb>9oZ&!^VeQMo#r!{pg-pxO=%%wiP-5se;)6v3??meND?n)RA!O@$<(3FLWQ+$ zOH6FdOV()2Oid#y4ULJnFZpl4eYEx}KeR}+M$Gk5S zqnm}0W#xsXD4uMIn&y?8n~Sx|d}IA$=JVCuRjV$XJ}sknP^i=`V|K{Uo}IS~d2w;U zw>#2*{<@gaLWBV!hwNSpj+t{B4+|*Cyj*>2K%9^omNa`xrFm7gG6I0)*hvDZW~2n9%+MzzWk{Tzj9=GEY?tX{kpHp1&$#)S9m54a>eAI8%Q-XV);&;*9apqP&PXYp>>%+b;E;=0xznd$s;o zNiW9QI{j^mKNAYvs+E&f1KTijaz9V7?^FoLb3{g=Tb7$1e){vBP^z+Zns=jU$thc( zw2kd%$grJDyMNT!)Sct9dhWuKlacvPp`- z8Y8&LnwaoBc`{&GJVY{O#mCp`P@%=Q3Yaf}s2B6C79AD5YR#P~V>m6)bOTOOJli=W zPNp*HEQD667cH}?Hg{}+gdgp}93YjUmp*9c`gj8_aBZ0sW8avY zX6NiSFgF7CK79K0^heY)4c1V_P-sdc?>r6RHTL$J{dk^GMMm!9(tmz0dWka-lQA#W zXdf`;=ExOJv+V3vx`;W^wrWq(KyLp>!+{ofPxJF9|Ku2~4KPy`&%r!MfevTFv?hRH zP^Pv`=qS9h326iT7wKo$i=g2*B^lbLre~#DL(I(n28>2Gg^v4`z>bmBWVf432BE}! z=%dyMYS3f}wFCnuO`6FNZ&g-=WBwi8-KIYYS~6hF|43W|sCKWJaqiix73<%aT>M#5 z?iKC8)_XL&u;F5WRi;7`VW01Uz?7QJJz;?DfTU)e2~pZhZ!ORA0JzhpA(Hd7nVDJl ztq(E=8|M-c?lGfz@4u1lb$BMrC2Om_dxO4-tz2^b_1KuqlAiJtXpSQ6`8qPSH$8nC zMZpy}rL*@|27`gFMY^W63My8YCbaRwy~}3>~>!x8!nLwhyfOZXVGBycPfMA^MtwF)p}uJ zB~ORB*G&5!yTZ<$OqwMMHLF_PJ&jGd-SXLoYICY?CVF(&Vw#m#{b%s+m#mJxx&FG3 z8EpzW20O+=4wK)eCMQ3=(LXxwIZs6*uopv5ROAkrE?rPBaxA;pgO}{nULCe zHva>TDb#W^s+&*5L4WaxR#7-B1|!KkpC9{0e>FF=@ssguLF9d|wA!E~;qk9j!Nb{V z724{=IXkjH?b#LE~)Ia~*I(j^ z&q7!0`jgh#H!;;L(ylgBpXh9_2S(;HM+P(Zgj)SipDbK@^n^bC=|1xLztn?+(cTs; zL9LTzC`o)-7zuSAeKT~#fv@{a-vaYz1u)j9(QBzS8#fk8^g^SuEFbTf!OCZm@e1Hz zk~G1k6;|w?^B68VecJwAmXgBAYOPakiL>pz(w!`at=gC8Z#gHN?r?plOG7Gru6NLxkY;S=fF2z|YbO3%qJzr|R*`t+_ElUU&&^^{glpQP5mHpLzxP@1#e01HOQOj08Vo z)cfHL0^U{i&=SYTLGdn3&Ju3SY#4p@t=L7hVjDBl94p&Pms&GXe$dl$W59QduVdfz zK_DwVt5H0(psqg{TB3EPB_MT+v zLYBDN^U+I)`GZ+Th^mq2lakVw_C)=`4cD`4R&i(Ib*)VL&w?yA@5bAw@19`~{B3xY kse_>K@2>xA`@bf7AU)z?-rE%2I=lr1p}dH|I)5bN->?*N_5c6? literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/images/logo2.png b/forum/skins/default/media/images/logo2.png new file mode 100644 index 0000000000000000000000000000000000000000..bd3cccd9f47793f86864cd068621ab07198b5ff0 GIT binary patch literal 2124 zcmcIm`&Uy}7Cw1j9(f?ogb*T6 zR7xF)f`B{(Do|8Luizj$s9*~MRt2>X1!+Z~1*!Hr|H7>K;heqp+UuPCt?ygs+owPh z9z-=}83O>QA;AHWSUia>UkH!w{6lRrED-njZSe!3sdpXe0oyJ}doah{1$l3>n$0jEa145Xm{l=`0 z1LOGW4g>)b$0!*Rr7zHbv%EOo9Z|$Fo71*7zJWCy1avV4C^u25yk)w+l|hG~+NP%M z5{YQYU@)jZ|6}ppWJ?dj(b2ICDyt7~YcEeqbnFp!&dzSWetl`j4p9%R#_ovkDqA); zH&^*-g-KjDYY(ZuwsyF`U!+y!<>j?5(BX=hvuhN^Uk??}&Q{n9Y4Zjg#C7RoPy$Wi z{*X1KD~4_Y4Z$7tC2Omw9FAC!5Acv66G_Q!o6uY7#pImab!WWHb~w?Z0Yl``%a$|q z+>?+r?Ts74s#CZ+-0m|}TfQ#NUh)?>(Z$J0GLxDv2Bn)2Z1H?F;I?WNsG~%EmS7Pjk|D)ZWt7A)huHmXOer^gBJH%1K9jp$`#$4x zb~w%Q?@pblQ{_46k=OZ}+xH(tNCYC#!ac#N_JOZWKf7U{DDkgy2`d$SnL*NS*Lyk0 z%meWH?Wzzz0i!T%m4v~F`SGnry4R#TIUJ1vJ3A0IipV;YRq&)YZGt}EW(eud{ zE;wK6z~y^1FP?Bu(p|Q0-uZ9VP{;H9U`&*-j-@vuhP{16BhsxE$`{EVKiAp{q~8uq zr@u2SsG*&O$?=ivqQiihvx^*$fSkYwVxu7!GvA=hX{$Q^|2i*Q5IBWivcFZ#_XbQb<4Md2}~BtK?7vNsOe@; z8A7rc8k5Y~=#YTZ36{>eteYjoWt1`AYnuXw~+sOqb zwxMU@^F4f%7!EBh=q1a|wAiZ_D+JOhUX!JJa2*$iS7#3pi1YLFdk4gKV7j*>o*xr) z>u|a52rtb9z` zL5qYJFB}IWKha&}+a4CAY57e4)62=pAG~t$p>#RjdtqsQ+_oJzW`~!f=cC-qY`D zk$bnEefjm5qa|4Mzj(KDJvR>pqy+7to)KGbH~EW!4A`pNaZ zoo-AVwbdl!!x09)Z|CpRy=ESskd{ID`jXDLfL?hW9X-=U$GpHyAgw3%zFZB{#O zNiG-P)g6G3eOSK97L?xGE4MfIYy#f`>zXaygC;%Ei~H&H6ZB;yzeOe+$gdv&7;t@C zrZ*)T@!lqtLa&6cr>4Y#2wypjY&gET2c|!nXb&Nw67mG0^=3cjK+>=HMrGJ-oxxOB?TeD9oE*q;kn@I-k&}bV{vSuh^`1%k6r<;IMd1E+ZG{w0g~MyWjA*d`_?1 z@A$la&+q&HfPsR8goTEOh-(y!jE#0PICFAzz_X{% zpFo2O9ZIyQ(W6L{DqYI7sne%Wqe>k@wW`&tShH%~%C)Q4uV7~+8cVjU*|TU7DOk(4 zt=qS7(BTJr4xw7TUm@^~T z%(=7Y&!9t#9!C>oFt6t5zwd>cgV;`7JySDAyxO3~?&AYen-@tu`Sa-0t6$H)z5Dm@NG7S| zk}d||0+o_zM{=bwNED(Iku7HVjkApih7e}UBq literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/images/medala_on.gif b/forum/skins/default/media/images/medala_on.gif new file mode 100644 index 0000000000000000000000000000000000000000..a18f9e8562941254941a446efad3e6edcb651d9c GIT binary patch literal 957 zcmV;u148^qNk%w1VJrb40K@E)v5}!; z41e42_PDkdcy;l$Dm3n3(BTJqC>oFt6n{%wd>cgW6KueptkMXxO3~?&AYen-@t5f($n3;DZoGDB*+&RA}La7-p#9h8%Y2;fElG zDB_4DmT2OMD5j|5iU_pm;)^iGDC3MY)@b96IOeG1jy(400+o_zM{=bwNED(Iku7Ha6Bh$gD&q69SR=%bKED(R$@R%+>`m}aW!rkr-_ z>8GHED(a{Mlxpg!sHUpws;su^>Z`EED(kGY)@tjmxaO+st^xGw>#x8DE9|hu7HjOW z$R?}ovdlK??6c5DEA6xbRBP=zwb*8>?Y7)@>+QGThAZy4+ZYo f#w+i<^ww+dz4+#<@4o!@>+in+2Q2WwApih7u&Gpl literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/images/new.gif b/forum/skins/default/media/images/new.gif new file mode 100644 index 0000000000000000000000000000000000000000..8a220b531225397b6a304918e4d96f6196ef40a8 GIT binary patch literal 635 zcmZ?wbhEHblwuHIc*el+@5|9!VKPrU6JO0wzv{&J_sfTWzkfci3%c&b^ZDfVC#}&> zyOaNZ`}}-T?z6u1zh6H-?MZpollt+%s=r@9J?->=+L`dQ-S^kCd)M4KzTZCopv3i7 zg#7c#dEf6|d^NB3$Neksx6gmOcH+A&vz~S(UH9g@mudETVb^R-1c%-`NuBSmGx$*^t}v|N3{XpZ=QLxvghB=@852m{z+->`t|DZla{DoFCJX;;C#~TaW_r#|NsA29T^4|DE?#tJ3t3Sg5rdM{YC?W zNN`JQTYE>a2m^CyU;l)Olls+yCQqLcG;PM53A6e*r27K<7?(+e+V$BkQ0bc=>K)=6 z>S?AMvXf0cM9U_`CsbZ({cMkrfKVO*0bx^n7M2ip!4S?%Tu1x7I766NnV3TOd0ax> zT^02BokLcHZqTpCs*6-yL$WVW?v z$*r&`a5&o0$QRlWP#C1jBFZZwJ5}J(3P%A63#oz=VNXx?YB3g7+`6(-c+I>yOH7zf SWil{ucTQ5<#GoL+U=08@?DshU literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/images/nophoto.png b/forum/skins/default/media/images/nophoto.png new file mode 100644 index 0000000000000000000000000000000000000000..2daf0ffd4333c90aafd71479510144bcdcb16c79 GIT binary patch literal 696 zcmV;p0!RIcP)FMR=<>BGs+}zyR+1b_A)zQ(>&CSip$;tWo`Nqb^+obycyPRT{LUB1#`*2@<8tPF=iD3b$&izilao_AJ$Uu| z?T7EI;9G2Ni9S*S|C71nPeS~@YXtLGZ#Xme_@@>8G%0p2sX6212og-Y1w$?e z!WtwvbPJl0Aa)A^G6(_^hxshQF(4>p2|9paHcPMx2&P29rJ}2$!H> zp6f%wU$+bD9?CDf;J*mCI1D_PzD=;|;mna;B%E*;B%w%FK=_dsy!3SVseTRsf;O0PA5l@7S3xZ$It;!i#Ky zUwupv47dxD@P=FMLIa$I+=l%s0_x#ZiV;DS09Qn^j9pByOm?A=ICBWn)?Lr=Eg7SN z03L<9;aJ866<{Oli{Qt&ARUsBlRXUURcV2;m^$aahm^dfY4!Hqk3O{)q1D z1TF3(2qBkcAppN3{)S!D3NGw#J)8~}C44zdYXvndi+CRUT0sljYWS7$P%CI+|Ayli em8wo4&)6FRqy5_K=O4cS0000${>eZ`v@813YY~Q2faPbmo5s9gt%|d4ho>o57Mp#$&^RgUuZ5eg+dZJYZnp<>ZnO zn8@79#>ir^py2~!6AQbMoJWI#lOrpaM#=_*i%myaxx_eX7A$CT;APg}>iKX$rJbGI pjAPFPVa3)-5*81xR0c3Hu?omAG~5VaYGz`Q2)lD?YqkS}H2_zIYl#2= literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/images/openid/aol.gif b/forum/skins/default/media/images/openid/aol.gif new file mode 100644 index 0000000000000000000000000000000000000000..decc4f12362124c74e1e0b78ad4e5d132621ab23 GIT binary patch literal 2205 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 literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/images/openid/google.gif b/forum/skins/default/media/images/openid/google.gif new file mode 100644 index 0000000000000000000000000000000000000000..1b6cd07bd8b9f27175e0a03be3b5849e0f5479c7 GIT binary patch literal 1596 zcmc(e`#aNn0DwQcU}GKMxn#>^B<6OWQKZt-cb3xBiE^HYQf=ZKSu)*APi@FdDnu7l zbE$~JN>R#|+lE{hiz(BRl~d+uOl9Xe=bt$55AUzjP!`6aB*XlLd;L`$MA0-=oh;_@%=DSKzTW5!kkg02oxN<-;QgzZXZ6@(M)5 zb-u1@<^vWqN`U%R3CtOJuojJt|Ec%yo>_yZ%MV%ajx`i>ysy4d0^mKeNIQ#t@2y*KMj}jE8i*m zl#eQZ7!1Pfn72wr)$QwY6gabm(O6$Lylw3Pd(s%KbU5crNh3{eeZxI4(dJKB1MUAN>n;QZCCkQs_tGtJY-^iC3ja3x9v_uTP?vFVW;}s># zx;OS=;!{FWer7!3y~sj6t|9~xWcTkC-QJ+z*` z{$kU`Oy-`7hp(oZDFCEubef~t-M)K#*_S^K7T>g7T^A0(oK7oIN_s0RehD8}s1*i- z0sK!m=+Xa`J^uB-PXLSoRAEU$)j~-M#?mcPT3yt{hn6u4gKCOf&J$L|DbuOJmz+s9 z2lJ2J?sjAok}A_-F35;rl&1P$i@IJDD5Bmxks6yO#s#L4K20(2&;kgjBXbWq$Y}&> zCc!g3Y<~;U$CQlDX2nU|3Dx218gg&b%2GehXilrp4#P1y;weqVUjQptQlB1c;VO7e z@*D^wqyiQ&MP@@Eiijj!C6&AUPa6q7&lm^MToqj2ny^P9>WX^Ttd-e`;&k}6HltTz=X|A>@zzv6@ z)2r$ZZK19Hq1s>pg(Wg*yF5bqcpgSu2*Z=3Y48rhsMseSGm6D#RWnKSz*rI2&e%)U zhJ)uMj-*j*Cq6a|SR^kuis=outMPl7vPf8@-OF`fK(;t_tEV6%zOcxe#-=20Mq7|L zi=0@wEQshFjSRBhL&)v46Z=EWNGr^Y7(cjs9|q}uirjP>UklMq_Cn}BpIZ=OxlIZN zrqgmZ9Daq-5mP(tkggoG%l1Mp5`!YX9P+{9a@vc{UA@ux9w!5<$J7Qsxz)kjp9U>O zdMGwqZ?ce!P4?kRO}wMe2<(jAw{y`J9zkIp$bd;9jpjy5e7h8h$r8hU6K*}g{&bHR zc4&^sGRyCwh1g@leXv&EK^8KfI43fLjtGZ4%_yBJq-Q6Ii74-1r$U?ItVA?Z^o7I@ z6Gx_VGVMkkya7VbaJCw^O!RGWg$&GOOC(&Bu>?}_7>IP{%ivHGBI}%WA6YI2+3M9H z%iJLj*4d~I--NgTJ(9t@qodNU?AaV`YHlKR%UVir5C+OujBse~Mp}pFlaUEl5;MVK zxnv+FK^_1to@fbJn^<{1g0s+?k E17=sO82|tP literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/images/openid/livejournal.ico b/forum/skins/default/media/images/openid/livejournal.ico new file mode 100644 index 0000000000000000000000000000000000000000..f3d21ec5e8f629b77c77615982cef929802fbde4 GIT binary patch literal 5222 zcmdT{3s6+o89vBPQIq*$1QbbvAP7Q0Tpmk!l@)pHf>>S> z1vbL6JXf&Eq~nt$W+Y}?NhYbg%X2}TX`y3UlPS(H)68fPIsMMP%d)ZvOQtiO{(Js= z?m7Sef9JdZJ&(JD*bxy~wQ3b%;$cU~E2vFPBC#hV6@5-lg8iE%gba#Un|TxR-jjq} zb3h#KnTHdU;W5$jSK%T=Pj@H?K_Lo-P~nPOqSb0qGXv!dp_JW0@nc==!i$LGD~{u9 zt~8U?B2Fd~qvpC~M^KA`gqqWJF*i|=E@%ujnuad%Of!pb5`P(QC7kR#SP2OTzQ<#W zlv%8aIKD~feX!6nCX`OuQ922|4;{CsbQG#}pj5FEW=E(mRL;B7LWEq0KANYW^3Y=B z_y|vmH;?ldeDaRw*8J(~hC!qOw?4H%Oa?Y1JnqbNZuG{_4-W8zh5g(k>k33gQDw%FD{ne3XB@ z!rC7_g|Gdhw<&R;HMK*r`+|J;KQGI_{V?zIn&?P3mLE&uo!9m>GUrS3pvy(U5A^kP z_$(c0u8#D!b_R{U=43cu$bYTlSjYsNjhYF)s`QQZ*3Ky}xtxmjy4jX?u^{kLVeo`r zO$S;h4E5!2dRRN72QywCwpkgq;m=hECwm&HvGsd>T}g=BENAdX&yl^G;15ZD@oDXe za5~)ny+KtH;%e1SDcr|-+q7@ZKZGT0tS19Bm$;2T93LRj8^{4y675bB zwk%H%+bJ)|KhfTK`uexou|aVjGo#E;Z%!+~BizWLXGzMgwI!$9Yp#x{^ivH+c7UV2 ztG$yM@p8QPk{Hi9qZjA|_iy0IrBz^V2FDGCt7!c=X2YVB&%j+s%GRZq6}$VIQ&2P#@0_gE zNAUFwKQH_3h_#F3ZUXmN_F>UpAnQ~Ky-mq~NZp*1ER$dEt(TrWr;qctLx_#Sm^+j7oj?A#I7DC$ zaD!T6s6Sj3{L6HyM1HcisHVO0oT2v1d(D5PMI|R99BoZLz$4vBrg+7t)3*@)h!oupL+wwg_YH%9vj*4aIjHXba4{8xkuc);A<= z{dZS2HK=KK!_1Gt{0T^;r%|h@D@A zwEWI*^|gxqvzS+eyR}HSKjd>V18&q9+tQP_E(>?D2|U7;$hbAy$_UJGbI$ekhw(yN zQ(t@t+Lp`(@GUljJClQ+q->vynK|C(jk-g{A&xDnJEp9_2N%N}naK~`m>>4s;pgp< z9QJ=jEdleAH`nptjkB2FdOtgL+Y^|;w&WZ>d7<84e)0JS>Jtat2~yqPk^=D3P?xq0 z@f7|6Jern%&D$w>p*GAGyl#LeY+CZk`Lq2)w{GZkg^@2rdyw`F-&0YZ?HMmk^)y+7 zCvI74%WRnjCpgYpE3*5H!##ZipIx~q^<9GpuZi7A2hXs1Zk~!pO}{2J>4l}(OBYOw zFj0B(Bt6hCeb>`pT-SE@^z{x5U2Ln3-?S|8w?F!!Jf|8`_0G@Mi{w$ z=X|SDy7MOoto~1%!)>|A@LPpZ)J;^~NDC9o;+|ify5q^Z+~}r~bWFtv8#b;5N6F%T zh9Q2R{a&r|U)2@;IgyF6UTmI3tzX1!nST@^QP$V_qZVtm#9wTzwZCbi{LlWU{0Anp BEK2|Y literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/images/openid/myopenid.ico b/forum/skins/default/media/images/openid/myopenid.ico new file mode 100644 index 0000000000000000000000000000000000000000..ceb06e6a3f0d88fb97cf10475a3062fb0edab33e GIT binary patch literal 2862 zcmeHJJxc>Y5S@6ZrZbS)owhjA&;jnXfl*X745tmMB3MrBW$)n`RP3b417qd6J6bL?UM{+)q(3xlv0Ik%T7j-l59$8$m>#ard~jq(8yIciUu?j(!>?&WKIe?G5c$>` zV+r!H1WS-ha)=*H^!uZEl>O1BP8%xc=fconk~~4DvKCYWn4`b_pUnoZ788}%mc>uh z9HV;s)r}P~NWtqf_p5&HGjTJoZI|S8n)uf0l05g}rdhqaIBpnv^myAWbI*7E=&N3x z95xI+0zR;x-|08c&;6Ul#XdjZARV+no-wSN`};1(+>)AIisPc*P@H*_1Kd%ys#()H z>NUmL)tL6ccj9UxPF`{LG^Rc9JypwV>?^N0yvK~LBc9f{#^OA9dQUv#8Tz7oxfa(K u#=%<%_2}PpAb{bmUKcqz}))c5uC(7v?)v4a2P)ZNa- z@$&T2)z|&~{r~^}A^8LV00000EC2ui01yBW000GQ;3tk`X`bk)Wk@<6#nZYULKH{p zEx|?+kif!I0vIL|#ZMubBmjWH2OtmxIFVa~6JQ7!1CK!f5W#StOTv&C3=E8h2vI1s n+#cd5;2fT3B_0kF0v!+!GARoV78n&7dMN`JIW(4+BOw4gP{MS* literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/images/openid/openid.gif b/forum/skins/default/media/images/openid/openid.gif new file mode 100644 index 0000000000000000000000000000000000000000..c718b0e6f37012db6c9c10d9d21c4dea0d0c01bc GIT binary patch literal 740 zcmZ?wbhEHb^k$G`xXQrrKS$vIw-5i{JotZf!T-qt{~ulWe{RG7ST3!;kh({9A)7tlU&$%5*g$f^Ar># z6BXsynVh) zJ?osJf(s)D10z?npvQi179P(djPk7Va&pI%NfVlv<{V@&c*Q1S?C^%corz71;ll+1P9`Sd{~>qE3OQZcJ(5}z4)|O#`p@vJ zr9;Fsp~HjOs*vv^`}=;ISs7Cf1bMkUpV%Pi{6*$qgF{F5!;-XxTBi=%eG5gPtV^!wdin~r1kkVO&vN=8Ce+Xv z$*|MCqlY(ACp$%vPrNeV;U*4lo)c<`?yngdvl5)QWEV6rHbi?!)@MOEE;CkPT#L!!@77y7u K{UjJ!7_0#i2pVPp literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/images/openid/technorati.ico b/forum/skins/default/media/images/openid/technorati.ico new file mode 100644 index 0000000000000000000000000000000000000000..fa1083c116527de7cdbf5897976aae8807fce878 GIT binary patch literal 2294 zcmeH}ze^lZ5XZl}d+sirdNzWcsg4x+2g0RLIAWPjuoD!tN}VeB7eq*vB1MXHX)Xi{ zAytZyMi86WScxK7SO|(gE|<@|cRuzut2sy&xMBC*%tj+A zE!c_l2H#`zaX;dYlru_mk^31KdcB@L9&W3#wIFp`YJOeP`OSr1{CK6O-`2Es@?E#T zy1MFK>-Ep~I=Vd7%e_s#J@}-Zvwh`X=4ClX_h=7BXW;)k1Ifb@+v7M5AZI7aYkiNm z$KjX;Qm=X2(~a>=Zn(6-IG8mv>sbkaBEroVrCFBdHr=3XM61uF&u!`5GI1u;Qf<+zMxU!sr1q{f@m!ic#&7x4 zjcDZqv{&NU85cGO2UiL7I^;$4kVpqJ@-E;pjlK(>l3v2Y;e5IS@q=)?(5+CX;*7LwCwnlxopJBahPURBFV+~&J eFF>coTp;}_7zqbJAQuX63TM^f{)_Klzq8-HK65Pq literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/images/openid/twitter.png b/forum/skins/default/media/images/openid/twitter.png new file mode 100755 index 0000000000000000000000000000000000000000..9a6552d18444ac98fdc776e6c58b794dd4452772 GIT binary patch literal 3130 zcmV-A48`+_P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!~g&e!~vBn4jTXf3(rYJK~!i%?V5XR z*3}uupOzLVw_czvr6Mg(q+BfJBAtnwnhhP85N5_MMqy6CWenXVS+k6ocCk@F`KpDYy&d* zrm^YBv+>x~<$2W6{i0#u<@P%U?%dX8R`^eZPDlH^|N1tw`C|q~s7fsGo@%qZGR)p# z@6K|w%`IjReqdHM*la)X?#L{&k!gwYR5Y8-1N=Ed%no#$)yy=@X3apiz!Fd45|{Nx zA2A4F&JUazd(M~@p}h0#ExjFP?>(Jn6BxUgVm6GiV{!8MRJiwCbnQiGMnQWFCESP> zfS5wuK?YO!QAYG_Akua&mJ?yq;cVK;hPi-p8mr>Jpz*VpOkt4D3q8q=@QSB5s?JC# z3;(`GDwa9wRQ(i08kP+Qk-CCppyYyudI^uT4cS_ zS$csec`_L}fQwZUmo}g3>2EffRWR0{b}c1aHlY1us@abV&0e1p<*tkzR-iv<{DC{- z=)$t~2OJl{`g5eK(j=TW3oHC1!Txgx%pM_A`@W>sY%xLn_kgJ;SE@d3b~a=+ey46u z@Ndm|pA3o6#hl_`yPK2ZW@egwdm=H>C54MZz?q;PX%5kNVY`|1V_`aNcQr+}5<8C* zXVvxISBsD{8gUo(2G_%D684h(qZpk)HY^JDV&2EnpFSF0{-Yyga7vKXhZ0424w?mf z0j4f-?qy05&3(ts2_MD4cOxO%kQol*{epAZ3kcU4GO=(B)Do{x(Meg!+HV@omSG$V z$o})`dVh^EDLz@-iubLcj1sNZJV3u6W7>q5`WBQpwOGdOlo?ZTEg6VUGQF5`kTk$; zgJ5TwS@BM@9IUnl$+wpRI3SmTM?aZXOz)lVwiXWky9}CwYHwy4j== zW{;wbP3>mS;?iHa$?R(gy#?X3P?`Xl4NWHRa&z@b?6vA(B{o#nH7W?q??`mv~n)3J%ITg@uPkn>}}HZx{vOLx4O)N_6I?B~p+KYCC6BOKSBB z);?aup2~nMP6eDUfa>ITwn`$H)G9N$)&y|ppvz(|oi6kDt%1@d4c=L-%~VN(o5<~7 zMXN>dU4?b4r75vFYZQ;I(EOGI{edv^M#St1==L&}h0?tN4RLg@JGwt{+gU~N{#Skr zK*jT?@B(?)82HNoh$1$I-{a{UPMWWG-B+lE`~pW70wbpd$uB20}Gm< zY2sW81GhoDmfKOMiwVyHsCZ@|OAw;@p&PN{u#98~e1^KiC4hJaqK|W$r^e$h<9hlx zd4F+&*>axez53$-?6J9?x4><}&7tGH$xjDh$l7_xbz>;vUGD;1R*(q@mqPsG z=`^K7YhfKpMDg_u?`6181-G)DCAd3AAy8zk0_x8wN9{G9`S90rg{WrwIl2;QJM@p` zMdelN1)|2<6oUZq%Ye0<80Gs4-j~wVk|gA0`1Ls!wlpY_Eg-2I`$=p-BK6Fo-|Z=@Oy4ujCn zFr&8+M=0gVYCZ*kpT!b}GT!axL?=>p(dqE_VsQ^+X>XIX6bCh)3-6!cw)3h0c(>US zGOY(89bShx{60d=aOY+1fVmnE{8GNz!d&*HFQWSkNslGQohH>G!8W;i=DWsx+Dv=)3})@bv&G5H&ss(C6hi?RdfCp27L00Uzdu@qQS0@rM>K z-~C#5D-3?havXW^JE+l|Fo7$EkdnLkeKaf=iw9CP6G#C5HX_~vbf~f_<9V)J(p~~v zpaj;(KeDt8&=OrPbpC61IK)aOS*c=w2f}Edqkt(l*N2#L++5b~BZ@xE1Ry1|AO}b= zub7}zE16`3;@paZQGT)vWxHK7ssE@Ee{k4r9Dp3=q!rUC$cbg2 z*cCmDyhI-)R56tEfkM_Zh398DyxQFNqsc^cD#OZIdna8qD;$WHP)5Gp7TNM&L?f z+XjOC8fr2R;#roiw_>4nA3J)>>i6R^lE(|@Q2X8S;)Q=zHro}MXO?7|SF%mO%fzioEX>2NsQrkeR zJB!e@23ZZAo$lf~xR+PIXTx9Z7M{-oWHn>T!jeh6q?`lrJH+~z z*iWql8S3~gg#?~;6)O~d!rz@$Kdr&cf7g!YU?L&aWF*}Hu#M}|?5yaR+Ws@q#IPAZ z(583eLeGpRh(l9H{qX&RDYlt-qq)17TTM(Y&vuKQ<$Ylfe;i}KpELJ3Ys2lm2+Ad9 zR-#yyPU3SbDD9pBRmcQ(IJ$}uT?v}sPTWlo)ntFcjo0z%>s^2YCo6!*J%F~7+gMp< z&(3#O5o?KWTQG)scwNXzdJC8gL{Z3P>R{k6;~qr0<5YrleA7qEqtMIj^8re37;Tp= zav{Zz0kC3Z9>S`&OSZlSWy}P4ue31DDGuBOII20tM|I#$&SJxK&Cs2IPT@r5(Us^L zQR*q^#4AG|ERZ?=uZd02G(uAv0ya_d=*a*74~Q!va}DFYVq*L{1=auM*5C907jof# UY}!*r@&Et;07*qoM6N<$f<-Fwq5uE@ literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/images/openid/verisign.ico b/forum/skins/default/media/images/openid/verisign.ico new file mode 100644 index 0000000000000000000000000000000000000000..3953af931987b0e66c122b338dc352502564eafd GIT binary patch literal 4710 zcmeHJc~n&A75@Q)fTGMB7?4e2SOip*O~H*}8$=cdnTIhNBRWh_BW_>?5imwU5pfq4 zQ4m>QjBTZ+CRIeErNk zREwwe2CX!zp{LmSu(HSiXS0Z~ELfW_hmFlL(pSOSItLB|b75<{0z+Ju!Pzbsp04@u z^IM5gez^!5o)68~JR~M1V)|=3XwNQ8?a>Ya%3;dK~By}6c*$o zzc7z#uYNYpl*u)SjcUZ3@pYK4t;Vd>gP50g5b0^P$e34y71<3~mHQrY z^v!hsAEG!C)5bMuezljdfeI&}&sTTWohhE|ksJA(uJTd-&Q zMO2ly;NXEa9IkGqoEFsAT)_K>+aNTx;izy9$B(q(h|rGX$9{>EKmIk&oIQ<;7cbz` z_Vf7oOgqlCbl~T$U*gkC?YPwb87^PGj4!TTMMqaVuHU?hTbV;iCum zZRbOL{qcZs;#f*}wrlz{`0xBe*(I;Sz zyL{rRJv~s#3{DZZtH%`9aNO9+=O$@yE9B}Ppw5xAUn^OeZyC|qoIL(nKmn&nYY>eL zKrf+{E*!VX7~r9ZuM^b@Gr+V$Li3f}Ym!#NDK|=*KfCmKlHM?d^N_SO`K0ZlR@aaD z`$@HgRh&}Pnx=3wq}{WbyQe^@gROXtjgE@>Qu-jEKP!rl1Z1YS(s35^4X=At_Fe%L7)Z2wJ zI=BcM$Zt(2XjtYnSb8HX6)sr~HNO7Fi=k7(q*OSR;1tv-=GQtZrfwBAkrlHYyo$=k zI88HxY21*dylU_ihyn*o`a+siMM#3?z2La9_bI5tty|5m@_$Kv=M-r`DWnsuDXI|SI(OnaFs5Rj zX)H<$bK-%v#3C6u8ZzR^Dh7HHqo*OR6YUU|4ygcb%!+EsW(iDpCu}sLU`Ao=$b{)2 zqYUx~O12syWYaUKC^j&d+|rIZmK*4)3nD=!V^Y|D)97J|C?PnK9W{j|5sikK_<-~j zB9Ma7re4g!anVfBnN3BRq6!uR)Iqc*BNYKUJ~~i(NLEqh;Ss1(xyk6vj3QL-oI(}E ztJRXtBFNMQ1%(&^QRrgWCG@7zAyK_e%IJBC60Uv0=A*yw|Kz_F$NuLuvWqkMvw2)i zo$zJE=5ZUFK~Q{S_xsb>KZ5B3WHsh5CkCG#~pZKK$?_l$V}GW#u_kRkqUX z`UyTd@da8=wc+%sYiMmfjdN#zfeRNd;rz$faN&FhZrr$v&dv^;YrTU@mu}(8l`dTS zyp!h7d$`$gpJq>*C%Yct?%liirkiHU``!4q`ysx0_@`GhR*Wg zL+$LnVne>U0!F8BBDJgQ#SO&zJnpsR{N}Sv)RGX5ladO5OX7h<2Rs-#nHmcwuE~3rz z2>~NUOyad;2ZtE}qZs8M)E9^dp|1)1sQ>G$Z=aRsiI#u(rfHU-9}N`$^h{weM0ms) zh?4E1IgP=fR~iiIb_PS5COyWGoN9OA+h%v^2+Yx zGJaHiGz3ONU|59!7au1(GXnz`A0HnV&|@H)n}q>L0s$9;ARnmIVdDT(f_z;3d|=N* zDLy_ze4rMf1_3@kMlc035Cr)7nOM2`_ymE1g8cmaK$@2aOo2_OfcXEPfdPgOHZw3Z X0PU5rU|_JTL0}LcBo2~?sfE!1%eOAT literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/images/openid/wordpress.ico b/forum/skins/default/media/images/openid/wordpress.ico new file mode 100644 index 0000000000000000000000000000000000000000..31b7d2c2b77c039342854190a90a8d8436992b47 GIT binary patch literal 1150 zcmaKqJxT*n6op^J!coBi!TOa}allq&O3PWo1c^ULbO>qe21^@jt1Q7KOpyh+ge)Q1 zK|J4huRH_#zwbc3UEA;R9AO!H&d%roag-+O5 zO!N95T@?S*G}Sgw{mN!=VmhZ5Rzf4>LLtlrZUAWLgGPY+oVrG5xo^yW`xZU-Q@wk~@ zYxS@D)yH#t7kvCm!TtK*mFh-7(+TYB+*i5pa!nc|Jf3SU=XN{`fjrfux$lA7e}n0{ zlfCqFi>Z(M`wVVG>yKK1*9rZ1`iw}=W^iIUOLNU?<8e1$!1FrglCO_u)5YTUvK?eu yI~YIj#-~XLO;hd+o2#A2S^08*UB1qS`QCUE52vFxJ9}scm-~xFcCkL1j=lhFMR`a7 literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/images/openid/yahoo.gif b/forum/skins/default/media/images/openid/yahoo.gif new file mode 100644 index 0000000000000000000000000000000000000000..0f0eb8efe7eb64bfb49b71bcfed741c1193fae55 GIT binary patch literal 1510 zcmW+!eN5I>6h2Lvn6<^!)Xd5hDlfLZ?*XcrP9QW)@-4ktMwaM%)M8CbQ_0*l24$96 zFCxc~J~6$k=GUBkF2{?49T|aFsb(Ko&Z+fB)4rCSet7>lyXT(mKF@icXKLw`+wYh+ z8-~IXfYE>VhrW~BSNh&w=(_*{5C8)RfPw)q2!_Bg7y+YT0W5+gunbnfDmVa#;0PRp z6L1P1z$179&)@~TLI4N|fgmshfuPafpa@E!3@V^e6Ja9)AuxhOGcER$-0QkIY5^5?}#|%n$=&Pz;G-F(O9Af>;zw zVp*(+RdFB=#gRA`C*o8*h)3}xp2drJl>ib@0!d&AB0)t#6h%ptMMYHmf)Ke8NfPmj zhEWs@s6jQPhSi7~RSRlSEvaR-qE^*`I#fsMSe>X-^`IWrlX_M!>Qw`1Knp*qp;Bn<`OCEbSmdY9b-PCpKTi3^Piz?f8 zy?dyt^GwNvp40E_er`*8*@3T<(}!)EUr^Op+Wlj^>bA2Zu5HOanSSmqx^=>0X=Kzc=shR~H^k^D_Zi zm0opINA;Grd+-0WzzmB`9htoIiPY)gdyAhf=<2`k$K9pb0|sBWeR^%q^M`Ymzy0{v zAGS1YzH-Xo)hi!8cI)gxUElrt`{^%}>1j`mD86O+Bh%J=eAmCXEE%zRZiAWIIC00W zAKEkSsqYxqQb6nD8wb6-Eq;Dd#o_vaK4;Y0sB zlrb|aKY!qiCqsAY>3-`Dw8pcK>`aU~%LVGIDmHY4O%6#(r zGy7)b{WdzUcIZF3+Y{BVyg1;&!k_|&&Wb)U7@G{hd*@p61{K8lY zrQfzUZ=akxn9jU!Wo>@vjsXJ~W_|iavS?$;@p-quF|A^H-qD(SYOemduLbk@yBARv*JGzJSqW zdH4Uj&-?iX&Pnb)_vW6Q-0jh@w{~0$JK-|~h%skR?y5pa2$0O&vznEh|N+RO!h?sK@|HmRv z{h3y(TKyEJ2mNOMtn<{HAAUcKPwGpL&s#5zxz_#Zs$8wV{*2!`hmD)b^4H{i;qK%8 aqJF!3xj$$f?HydV+s)ZpZKm7Z3>&}3(53|d literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/images/vote-accepted-on.png b/forum/skins/default/media/images/vote-accepted-on.png new file mode 100644 index 0000000000000000000000000000000000000000..2026f3bcc50e2738bdb6c21f32ffb2a82d088e11 GIT binary patch literal 1124 zcmV-q1e^PbP)(98f962%urEyvrobTSI2iWDonJaRXmcCZ2&_7W|s$8;q zX78?p{;d3j=Jt6u zRf1NRn45~bKOeQloe<4k4F*a0^w988Pn>Cnf)+6fhvK8hS8!=&g-Tr5fO=!taU4$~ zOcn}~1dq~+P+Mn_{a3^epPXDNqV_QrDaOtjsGVmiri&cVy-8sMq#8b+xcl4la{1KX zd)90%K-Mw{%5_9>cd}5B8H|=?=ws;;*H`e>Y)`)g+x+z483yu`hh? z)vUt~N$7kH$t{g&Q6XMdNY^x24}P89!Jn~piFLVw3}%z-ZxONXwNtVCms$I=Hpt<1 z;>5`C2ui916R`wY2}KWfU9>D9Dg;Y<_4cY;b#?q^Z}exPXQRCee0Tq%=HACy;vCVa z)oQ5K>nMeJcUM{?j$?G%ZU0GpPJBRg?!=6hb@~7WeF-jmZ%z`A5yxGcdRvZYk@$u9 qnpoR$QRL8vDfll$;(wNZ1Q-D2C|}A%ZKM4F00002jDb`!&=tG_0s>$|K>*|`prJ6sL73(3+qcd@u04=FA87F=kXo2eKmZ_sFf2$; zPWA&zCIel<#Ldmk0CYVA$YsbuiEN3WpdiEV-@jQtefpFN6jJ#4^XDXHW@fmf00M{! z+3>`~M022FsXRP9Oq`sYV1r>6fM^&7@nIMgTp$C5g@r+ma0TLs@87?}%>)P_xCIFb z3Bo|lB^(?aU=1+CkRuHnMz)lXj}Igk4Pmg5nWK zgM9!HKul2A3d156+4UepK$wMv1;uh~hS${8fRioA43Jqsb)q0~fB<5G=1>+`+J?D) z%9JT^&HerTa5l0y4Aj-tf${>ww{PE&GbGq-fB*uAJJ7~2AO*0T3i2vCz-BNA)YjHA z05ccZaO6}4^z~N|8z6v~pcXs;rCV4KfV@~$RYfo!V1|GF`V}PzL1I7)9)r{X1Q0AA zT!9r*AO#=|po~*pU5(RlP?-Pt@dK;cg2@4U%(nbkq83~4GrM%MlNq) zt_Q^*$WcIgHIVxmBnA*bjL2RCmZUYn2r>s2cJMME1VFhHRs;jfeUu1=7iz%L6XYu( z0EK-K5dWAvcP;}!05PGJEj>WnAAr0L$}BL~0gGY=V7brm?%g}EuaFBtSO$Xz??)hR z2I3zuIe-8{v0&c3c_1f%lFyZoA3rjH3O+6_F0kui`2-depv(ty1ju)w_yQXE94J)^ z#Ltn<1PCBTY{dew=mzBvKWLT74YCM?K^B1kDEN?45GX8n0Pzwa{)%kQf&~j000M{+ zU%3+(7sm^vtby1Jh*f}C5Gn;S=spmi2jcxe{2E;^$d?NjE@S`*AVy+~WpV*P05LJp c7ytwq03Nl7Z}J%VEdT%j07*qoM6N<$f`!tbz5oCK literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/images/vote-arrow-down-on.png b/forum/skins/default/media/images/vote-arrow-down-on.png new file mode 100644 index 0000000000000000000000000000000000000000..048dbb44dcf2aa3669386737e4e34503659cfa70 GIT binary patch literal 905 zcmV;419tq0P)-y5{XDqL?wtw8c7i~ zN0Q6^?9Pnu+qv8Oi^-+Pz_+tI^UZtT@2q2t0Un+PKF$My_TW0l0Sr+3C>}Z+0c92j zQ&9RPZchib4JRZRISTw*?j`s&D(yi!4W!bYK|2l~#3&Yl)q=eKB*_hofA~`;xN^ut zQH^pphu&R)Hdf>y>Qg&-^&xZ21q}p#FHqmiA?Xcc?0sNEuv}tToE&x%yyA;O9n_c& ztgi)$?=eay3oI<;;0F@w=OKY#QG@aB?kYGJj-3UDF8m+(%RAuS)fP`Uv|~x*NmL4j z!2}6hSuY1_FXH@Eno4SYa2fqafZMOBtL=P?a&g3-4|twf6~U}kNf0P6+^mhu1MQ&W zQ|)VwXZi;$cy;)KStHP5wXSdC?sR7rWK_$yj`F9K-3tj+pzGE&CT^qA=z+D=Rv$@J zbvtq)#Yq4@9@OYr{|tt^${b4@Tb$voWYw^jsXJl~1l){yZ6Z~mTapgVXlRrfvI@o5R%kNt_@+gec_sFS>sC)ftV7EML$Cz1 z{=+$y6i-HJNDispz>1H>a&r@96Rxcq37>Q;^OE=LI@u)mx5b(Kd2vsyAI4-kummT= zg6^eUr{s{$(8UZkHS&32F)wd(AU!Q7(Rdg%m8GwFfuZFFfcG!1F;4WvoH_`{s8d}AU+B-V@FC#${&~nKmaj< zELga3A;YgoU<-h@djJ(F zfE2(qfH25qFg7-fEDzJi!NCC%R{)s_G8-U(SfCczf+Ro+KtNAVkHN^u2xqW7e*BnW z&z?PC3($iL1VFy|@#BXrkhujQfWSWZ{rk5XND$^Ce1?O7tgI|UVqzj#J%|QjXJ=fy@M{1qdLp4}jidfk}eE+_`fx3{6f>#t=jHft;KiJOrSj0yY~UfM6D&CG|15)01!ZM3y{4xbLLD8!_w2!F~pGdzzl|Am>fU= z!Gj1S080fK85v~z06FP`06+k-pxX?ygxFk+ZUJ&W0Reyjg8Kl3VH&1PnSx) z)Ie%~{`?8YATeO(f|~^k4S)aw`v6o-fJ#FqCMIw$Jbn5!!8)eg7&Qmi00000xCZ&X`T>s1rrok967{t#$kYGS7Vd|fw?Kji~ z;lV5VHnRYeM`{4BF0c$^a34H4Y$j8XY%{~vP4Ur0f;38Hd%(7f1L*It^x#ox6Y^@F zBDnIA8Ir>UzFOH#7SIa-feR)47XkUfbW{ysEx$_Za+(2mrv@e0$jB+NG1p<%zK%|C zDhyl)$6NrHqq@G{IrJPRPyB_8o%g~qnsOblfcn? zud345g@qRO!2EE%0Z?uLjv-MxAZ&$ojSA9F#F=yV|e*{B8pP@ zPpy`qnVS0ErfIu&a2$lel&veGH2NtLOlegf9(x9-G9&7J^nDl1oLg=Hr16Fr(Oro+ zQbLGVpQ1pj;IRb_B#w0GLze=Va%%)w6bBhuOzm9OdN+s8=o$#_ccLp*OIu$G;h>rLx z;mgpUhOxf69+e^?1Ywq=jh2GNqFYu`xESK?pEsfZhXwuLEj*_r99^_vuKcEHdY&#l z0Wa8SHm?w7E{Z_H{N^6JFTqUk){$jh)SV7SR)<^84Pbk)5p-5xEIhKMBS92|x<@3= zZ)!HBX})$JOk*s=%QA0e+p{3c*vl0e=9sUD))!9#$+3Q`u!ebIIb1s(-e@F=*U1b% z9-vyObQ;pB6zoI_#Ud071$xZ|IC*EJ6cLci_@% literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/images/vote-arrow-up.png b/forum/skins/default/media/images/vote-arrow-up.png new file mode 100644 index 0000000000000000000000000000000000000000..6e9a51c7df2da30fd2e56359a93f4712077ee62d GIT binary patch literal 843 zcmV-R1GM~!P)Ze18CM!Al?Dub8&GoOqw)_0U&_j1|%mZ z3j?V-AXWlNa&mGq04*cZdym|9R2`JR?`}glckWT;thzab!KYu)chAIdN z2{HWr`xlHsmVhvfpOlou(AU?;;Ns$fQw<0U3k!q96hLNz%mxS`umwN?TMiBmxCT(X zz%UHx=;(lHFE1~+7_vH;K9D6Kvw*ZMNDV*$!7NY%c@N!iY!;y#j4TgxAjmABm>Nh7 zAb^;l8VIHqU?k#|$KnXEnE(OAf-H%y2t6WU0F?aD!Pc!?8Fud6iD4l?05Re7HZlf< zD?Z@u?ajc%#Do!S00G2;&E;unX$-o$x?py0ZZ5;*$&)b*1c~GFC9-({0mOnW3}J@j z3pQ+)=H=yK1UEncF%grAhy|b^LI(f=#DX0Dq!^ZylS5Px0R#}iRD>&trc9ZFHD1uo z0tg@`LIz>;HohQ2&zJxK#DXjV0<5g8D4IZp7&efdosDoMP(O0;g2Z7C0|+1%sG)yA zIS*DU-nemtK|@0WtN>JWV*^vCPQ{suK*a^n%pV{zfB=G7a0gg^Yk+0$lVq#(#)YQ~)1`j9~!(0t4szGKx0I>lA2$l~H18vm+Dfss78yF*}3LYLF zh6M{2fGtH24p`ms^XE@^(165%^kI-ZKmfrk*aj4E1sbd%ARqvC0Z0=pj6Z+=Ot?w| zS`LaokmG=6fn2{0Bn}Wjj4(qI5)wdFCb(AP=H>>wniTNm%NGV#&7yzfB V!E{DAJ$nEE002ovPDHLkV1i?NM1}wW literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/images/vote-favorite-off.png b/forum/skins/default/media/images/vote-favorite-off.png new file mode 100644 index 0000000000000000000000000000000000000000..c1bef0745ea9acd1c405ac11e0d5555e8b12d7e3 GIT binary patch literal 930 zcmV;T16}-yP)Q zl~7{r#<-|Zx+tX^7v1(nLEMz+FA(Vu=&lr@i%_r>io5Ej3qgvws#2;kN-aKOFk(}p z#$w4mvd&mnLCfqGB`LW%tTQXT~$@pwWz4*(Z$7u`~3X;?Ck6f-uOMADC8`%H49ug9F8Y$ zw_8+IRf)E?w)=F~D6T0AgF(40thghvTO@bbp`oEcsi>%U%r*rBP;F6Pqt;fdRTLK& z>)!xC_VxAkhJ(#!6RoYS59m#UO~C*#0w5d#&H*ynH51s~l9G~3PA->Ap>jP%V%H7; z1$bs+Zcu>R9*^fGmE?+(-EJ4{?d|vr-^j>_mM?&1XlTeop!aHPYwM^Sb=B3?ZURv7 zN9E<^df;4gaBv`wkB@cT+uMthZf$OE#_0YhP1;0PS65oCtgL+9(a~Wg;GAGQS@Lv2 zkN}h`N1!eQU0hu3oSU1o(cP$Wa&nTu!eDWr*}!96BogVHo}P|wZ*Lo!n{BzQY=b5*FE0apfWSv{QAJ{HZLNQP zem;yw6#{1^;uGuZ>*=Yfskc2nJ!4n)Ad9ee={G;^@9!H;&ZqE2X`<2Sx8B~~uQ}N? z!#RA{$lSzt+5M1{!p76PmY3|<5RSAL&84NK2CzIy@&xf8fJSJCngW4N5yyPM&Jfr$uh-k@bUF=s0dG-KZhD`{`3D>TQ-Os; z#w#l;;f;+A00-HDHurrf6dIy?@=((>&<&onoKqxC$uC;-AG8mSL?SV4mQhBw zj*gB7W@ctSFDxuPr@!!0KgLYy`Z|>GFQSP5EPn+U0OR;~TRRy%rT_o{07*qoM6N<$ Ef@zSrumAu6 literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/images/vote-favorite-on.png b/forum/skins/default/media/images/vote-favorite-on.png new file mode 100644 index 0000000000000000000000000000000000000000..1f9c14ab0813a1aec1f66b12c3eebe7232be3d8d GIT binary patch literal 1023 zcmVZd<5{Le6mt|)_<`GX($dlRu2ZJkuE zjf4$Qewch0=TDhyPWI*W3&@@n;Mrh z;ON^;+o1J6x+lSyC4yAc5*r#_RRe6^%F}2A*z}DOqn}Ca7lL#(u})I%7x#^k&|B0; z`f()bM-h)+;pZ4u$ALJ}P;Wvb3dM-H#z&Yj5-&MXQ!CKC#TXEuPEEBSJ=cPnS)ej| z2r5#+g@;jQ$TU_*6Ij=36b6CwjWTT^%5_m~{H>S28)%4W9OsGaYMC6L=yK8ysOFtZ z$a|Cc`qnVdXsKFY-eyhS#>TZMyvv&Gk-Z`#?>^Zjhh+Uaj*dLD2Glrl0XhE%M&6JK zdqZ9sxMdI+<1Dr0Qu()0dB28}pC5!-B*l|clf(rIB=P3C6)a-E6moW+nx^tEqNfk1 zhaN6W(@j2{T4UmW|9SG(9X_!#|lMVmx<&Y38Ta_F{^dJ0jyZ z^KE3Yfqc<E7fl_u~mXc`B_ShF#sjOqSPbOw6(spucpG!yqP5!xrMP zXa--)`><8~qDbZjaUY(43Ik6LqKX$#+j`!7QB^cQueWs7N thq=s2qJz(mO1EpR@jr*ce;$7X7ywPEpB^Im6SDvS002ovPDHLkV1mKF-a7yQ literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/jquery-openid/images/aol.gif b/forum/skins/default/media/jquery-openid/images/aol.gif new file mode 100644 index 0000000000000000000000000000000000000000..decc4f12362124c74e1e0b78ad4e5d132621ab23 GIT binary patch literal 2205 zcmV;O2x9j~Nk%w1VNn1b0QUd@0a~U0{{97Cr-7}~`~3at@%IK^rUhN5{r>*;`TQ|@ zxAXV<(c$m-`uz9${QUj?tjXaAU#9_Cqyt>0`uzP6Wve@Yx&8hA@b&ra^Y|liumW18 z16!pEV5rI5>-6~hXr0Mflf!YN%x>+Qkd8p6#`TXYZ_T}#Ooxt1~X|1!)YIhOpD5#NM*a<0W#j0a&CWZm>^{ zzyn;Rz}D!)*y=NTw$$VApTXTqiN5vt`#gcU#o6jVg1UmP(x}JZzt!kmmBs1t_qNaF zEOxXxf4M<~yV&LMwa?_c(&n4K+mpH2M25T;XRHups^01ID|NGbsn3_Y+VS@KWSYp* z;_ryE)EsK9fUMC>i@&bQ;tOD?Mu)vGc(qZE!4hSw$lL2DbFy2M#IVcb=I-{7w%6?P z_}%F8#@g%I=JCVW>A~0O1zM$Emc;;Aq|e~)=kN9^bhB5I!`tZcY@o~4Dwe zHhs4lW~<`t^hk)l_4)e+UZ(8x_}b_3h_Ter;O(~1GFN6(4WBES(L=`_W7sA;8&5u>hkx|;qKw=^yu*R z4rHnaUZ-7_#k9}l^!NJ*UZyBu2wvd-h_@b-M1#8yVW@Sa&Gh*C zz}DxCwAK`8te?W&k+|6D@b~`z|2%=Z24AP1z}wd3@G*F`*5vQL)#tg==7q1*{{H`_ z#^61Iy8r+GA^8LV00000EC2ui08s!P000R80RIUbNU)&6g9sBUT*$DY!-o+6Jy`H% zQ^boHGYUk|VhNoVyflg&nXrK{5cMRHfHRGxOOgqmlo8oERHm?$HPJbz#x1Z zpeoL(7W@hZxJ5+J9eE5BP*LH>O&${ zj5jX`S&`8IL!Cx*Fz7_k)nS8ApeE%1x(B4-jBvRPK#9X9Oc*K+HhF5Leg{%TGv{D8K;`L_xwpaHxaF z0b^+Mfl?X7amfU05OYC(Eiz<;2RbaUiytGQ0x5J^bziwedW^uQh}2wH>_T<*uvM9pjgf(kHL(TyFV3}K8NEHsip6(+=y z&mT8*M^FIdM4?9wB6I;j1ZWKZ^G^UCv;lw?B2a+P3?fKiKopBK>LNoIcyNRiBZxr* z3skJY1QoA5aVRDuT=9ep_zX!<2JM_N0V+YPFa`jiSn&@6Y-j*L7D^;UixL1NBE%5F zIz+$0D>L=cJ;YWUy}E(fIl&_o8nn81SpJFsxj2TRoOLkeItpmjn7WRMLXFdR_93ql-= z1sX|!ae@q8)L_8?$rNzmK!~tV!vO{eV8H9GyWWQlALOvc;zfv35CtA!F~I>nUE0WC6>_LJDwEf?Ad!6)ETg5hf{+1h8Trx)^{dte^urh{6TN9=*OL{Q)Z2o6xWB>${ z7E9g~a4RxB%)s5|;lj_{?8?N<+|JB}<)%Hfk6edRIx|)U-=MyYvG@3L2ePrxsR2Oqt zBxw2I!og0~3Z95SriB-Uh0Z(^<`&3W(C}!9Pi0zD;fb^77(DfzpVpq9FbC*K)e_f; zlH{V)#FA9q6d=I>^q-}!frYMtWr%^Hm7$51se!J6iIsuDpB2g>Fb%o+DVb@N$QsOa gjf_K#jI2zIfJz}6>YoYA12r&sy85}Sb4q9e0RGj5MF0Q* literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/jquery-openid/images/blogger.ico b/forum/skins/default/media/jquery-openid/images/blogger.ico new file mode 100644 index 0000000000000000000000000000000000000000..1b9730b01c3e60a396afa4193986b7ce800784a7 GIT binary patch literal 3638 zcmeHKJ5Iwu5Ph~|?1U(aasn#_qUHo}jTDrKiXtUdlmp}lu2BjK4iLGb^Z_VRaDbGQ zV`g{OIJ=vVkdP%LkF;y=%$twL>zy$WAVijduPI(q;0nNaEbGS@D4BQ7Jci(whr)Rj z2q%0XEZ+#_Ct;QoHXnrD7mcRRG|JxzlR06vA#8Vq`Ijl@shxhCs9J>F=Pk_Z-FQz1P;kqy49I?9vmhesu(DDd2TIQ)X`OMtv zn36eGX(BoIuReV`=lQ(PMGs~LG0b10`Y_|q?3EjYCVKU(mu83%PtE7$VCeoT#ik(R zI_G=Ly}Z;TPEX~szaC%nFS05~vowSpGdW`wz5QNNiS763 z)A|{|X4mk|Pb``6HD(c4gZ06~PM`I2P38&B*lpoil+UKHq(V!B*N4IXtYRdaF@vLl zmV&|Rr42{^DojjF{P2k7k-;lA^VR%bjPq6nUi{3FY3ZQZzmogqF?nyszfZWEm-L2B z-JibT!wa5rZ$|dN(=9gsk6^s8>ePX{)JfmC{wZV@OTAD2$6>+F!Ka<9w$43N|KZD> zPKm$&mc^Jhy;?nm8U4s*&rmI|-^laso@xK@w* zQ%73r<~Q4ldN041`4(R&vsZcBa-Fqj;u&0Ae(<|~YnlAQHvQkZs|?d;eGo36IJ3*} zU)cm;w5gW3MwBEMr6!i7>ZSk*1|tIlOI-sCT?5My14AoA6Dv~#T>}#<1A{*+ltW+| qa`RI%(<+fQnCTiBhZq@InHT|;LNwGr6P5>RVDNPHb6Mw<&;$Sg?fAn0 literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/jquery-openid/images/claimid.ico b/forum/skins/default/media/jquery-openid/images/claimid.ico new file mode 100644 index 0000000000000000000000000000000000000000..2b80f49183c7c36fee4c4f9f0a82d4fd9950fcd5 GIT binary patch literal 3638 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 literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/jquery-openid/images/flickr.png b/forum/skins/default/media/jquery-openid/images/flickr.png new file mode 100644 index 0000000000000000000000000000000000000000..142405a6e6a4d5739f00b18dd6ff5693f6db1109 GIT binary patch literal 426 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Y)RhkE)4%caKYZ?lYt_f1s;*b zK-vS0-A-oPfdtD69Mgd`SU*F|v9*VRoXegrjv*GO-%i}<$K)u`w*TgY2f{yOO759% zIVQj{S8?O+?FWuNikNpOhMmz%p*o8@@35=;(PrbQ5pQe$i2HdeZ*?nLqyz z*gx6rmBaFdf&I+MMb|f6OiX%kEqhthHhX5F*-XuQFM7Gz#J$x2c%k9W|0p|=FU=Ef zNL2hRo-L$jZzr9ZTC>}H~WqJD0<%)F#> zzDalYrg(pGEJF+otqe`9Obv7mOsotH{;W_AfoaIiPsvQH kMAl%YYh)Z^WMpMx1XK#qQ2$I=9;kuA)78&qol`;+0FfD;b^rhX literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/jquery-openid/images/google.gif b/forum/skins/default/media/jquery-openid/images/google.gif new file mode 100644 index 0000000000000000000000000000000000000000..1b6cd07bd8b9f27175e0a03be3b5849e0f5479c7 GIT binary patch literal 1596 zcmc(e`#aNn0DwQcU}GKMxn#>^B<6OWQKZt-cb3xBiE^HYQf=ZKSu)*APi@FdDnu7l zbE$~JN>R#|+lE{hiz(BRl~d+uOl9Xe=bt$55AUzjP!`6aB*XlLd;L`$MA0-=oh;_@%=DSKzTW5!kkg02oxN<-;QgzZXZ6@(M)5 zb-u1@<^vWqN`U%R3CtOJuojJt|Ec%yo>_yZ%MV%ajx`i>ysy4d0^mKeNIQ#t@2y*KMj}jE8i*m zl#eQZ7!1Pfn72wr)$QwY6gabm(O6$Lylw3Pd(s%KbU5crNh3{eeZxI4(dJKB1MUAN>n;QZCCkQs_tGtJY-^iC3ja3x9v_uTP?vFVW;}s># zx;OS=;!{FWer7!3y~sj6t|9~xWcTkC-QJ+z*` z{$kU`Oy-`7hp(oZDFCEubef~t-M)K#*_S^K7T>g7T^A0(oK7oIN_s0RehD8}s1*i- z0sK!m=+Xa`J^uB-PXLSoRAEU$)j~-M#?mcPT3yt{hn6u4gKCOf&J$L|DbuOJmz+s9 z2lJ2J?sjAok}A_-F35;rl&1P$i@IJDD5Bmxks6yO#s#L4K20(2&;kgjBXbWq$Y}&> zCc!g3Y<~;U$CQlDX2nU|3Dx218gg&b%2GehXilrp4#P1y;weqVUjQptQlB1c;VO7e z@*D^wqyiQ&MP@@Eiijj!C6&AUPa6q7&lm^MToqj2ny^P9>WX^Ttd-e`;&k}6HltTz=X|A>@zzv6@ z)2r$ZZK19Hq1s>pg(Wg*yF5bqcpgSu2*Z=3Y48rhsMseSGm6D#RWnKSz*rI2&e%)U zhJ)uMj-*j*Cq6a|SR^kuis=outMPl7vPf8@-OF`fK(;t_tEV6%zOcxe#-=20Mq7|L zi=0@wEQshFjSRBhL&)v46Z=EWNGr^Y7(cjs9|q}uirjP>UklMq_Cn}BpIZ=OxlIZN zrqgmZ9Daq-5mP(tkggoG%l1Mp5`!YX9P+{9a@vc{UA@ux9w!5<$J7Qsxz)kjp9U>O zdMGwqZ?ce!P4?kRO}wMe2<(jAw{y`J9zkIp$bd;9jpjy5e7h8h$r8hU6K*}g{&bHR zc4&^sGRyCwh1g@leXv&EK^8KfI43fLjtGZ4%_yBJq-Q6Ii74-1r$U?ItVA?Z^o7I@ z6Gx_VGVMkkya7VbaJCw^O!RGWg$&GOOC(&Bu>?}_7>IP{%ivHGBI}%WA6YI2+3M9H z%iJLj*4d~I--NgTJ(9t@qodNU?AaV`YHlKR%UVir5C+OujBse~Mp}pFlaUEl5;MVK zxnv+FK^_1to@fbJn^<{1g0s+?k E17=sO82|tP literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/jquery-openid/images/livejournal-1.png b/forum/skins/default/media/jquery-openid/images/livejournal-1.png new file mode 100644 index 0000000000000000000000000000000000000000..e643608186435818f547afa1dce4e2fdd739f7b9 GIT binary patch literal 713 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Y)RhkE)4%caKYZ?lYt_f1s;*b zK-vS0-A-oPfdtD69Mgd`SU*F|v9*U87#Kf!x;TbdobJ8spE0RW=J?0&pKa{y{zXnp z*qkXmd$q)x1@jCg99bMCCYzoJo}PDpsbln(Mf;~G$S#s%WtDqvaECy|{p_~)B)({IQtT<3dh;gx6fZV2*R)4hETWrjdFTGIR0}oUy|+$@onIkZXUXME z70-kU3m^CP)=7+)#n*o>s#|x@^YNU}vtO$J_PDrDRPFS6W;-LbjE^TsP<7g+?mN56 zm}C4rjRM@Wm z@1eUhS$3_v{E~Zv?T%@}g66;3ZC%)H>RW&Ct3;oeIK%V9;)o-;;tm1}Bh+3LZ(+PL zp=R3ekGI!6;jTF~^?=%^+^tdnv!cbXX9?eCj%wJ?kP*E8x8j|{KY>X>wZt`|B)KRx zu_RSD1xPR$85mgV8d&HWScVuFS{a&HnHuOCm{=JY{8^zK0@IM2pOTqYiLAj)*T^`; c$jHjX2&fdIq5he$JWvCJr>mdKI;Vst02%)%`~Uy| literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/jquery-openid/images/livejournal.ico b/forum/skins/default/media/jquery-openid/images/livejournal.ico new file mode 100644 index 0000000000000000000000000000000000000000..f3d21ec5e8f629b77c77615982cef929802fbde4 GIT binary patch literal 5222 zcmdT{3s6+o89vBPQIq*$1QbbvAP7Q0Tpmk!l@)pHf>>S> z1vbL6JXf&Eq~nt$W+Y}?NhYbg%X2}TX`y3UlPS(H)68fPIsMMP%d)ZvOQtiO{(Js= z?m7Sef9JdZJ&(JD*bxy~wQ3b%;$cU~E2vFPBC#hV6@5-lg8iE%gba#Un|TxR-jjq} zb3h#KnTHdU;W5$jSK%T=Pj@H?K_Lo-P~nPOqSb0qGXv!dp_JW0@nc==!i$LGD~{u9 zt~8U?B2Fd~qvpC~M^KA`gqqWJF*i|=E@%ujnuad%Of!pb5`P(QC7kR#SP2OTzQ<#W zlv%8aIKD~feX!6nCX`OuQ922|4;{CsbQG#}pj5FEW=E(mRL;B7LWEq0KANYW^3Y=B z_y|vmH;?ldeDaRw*8J(~hC!qOw?4H%Oa?Y1JnqbNZuG{_4-W8zh5g(k>k33gQDw%FD{ne3XB@ z!rC7_g|Gdhw<&R;HMK*r`+|J;KQGI_{V?zIn&?P3mLE&uo!9m>GUrS3pvy(U5A^kP z_$(c0u8#D!b_R{U=43cu$bYTlSjYsNjhYF)s`QQZ*3Ky}xtxmjy4jX?u^{kLVeo`r zO$S;h4E5!2dRRN72QywCwpkgq;m=hECwm&HvGsd>T}g=BENAdX&yl^G;15ZD@oDXe za5~)ny+KtH;%e1SDcr|-+q7@ZKZGT0tS19Bm$;2T93LRj8^{4y675bB zwk%H%+bJ)|KhfTK`uexou|aVjGo#E;Z%!+~BizWLXGzMgwI!$9Yp#x{^ivH+c7UV2 ztG$yM@p8QPk{Hi9qZjA|_iy0IrBz^V2FDGCt7!c=X2YVB&%j+s%GRZq6}$VIQ&2P#@0_gE zNAUFwKQH_3h_#F3ZUXmN_F>UpAnQ~Ky-mq~NZp*1ER$dEt(TrWr;qctLx_#Sm^+j7oj?A#I7DC$ zaD!T6s6Sj3{L6HyM1HcisHVO0oT2v1d(D5PMI|R99BoZLz$4vBrg+7t)3*@)h!oupL+wwg_YH%9vj*4aIjHXba4{8xkuc);A<= z{dZS2HK=KK!_1Gt{0T^;r%|h@D@A zwEWI*^|gxqvzS+eyR}HSKjd>V18&q9+tQP_E(>?D2|U7;$hbAy$_UJGbI$ekhw(yN zQ(t@t+Lp`(@GUljJClQ+q->vynK|C(jk-g{A&xDnJEp9_2N%N}naK~`m>>4s;pgp< z9QJ=jEdleAH`nptjkB2FdOtgL+Y^|;w&WZ>d7<84e)0JS>Jtat2~yqPk^=D3P?xq0 z@f7|6Jern%&D$w>p*GAGyl#LeY+CZk`Lq2)w{GZkg^@2rdyw`F-&0YZ?HMmk^)y+7 zCvI74%WRnjCpgYpE3*5H!##ZipIx~q^<9GpuZi7A2hXs1Zk~!pO}{2J>4l}(OBYOw zFj0B(Bt6hCeb>`pT-SE@^z{x5U2Ln3-?S|8w?F!!Jf|8`_0G@Mi{w$ z=X|SDy7MOoto~1%!)>|A@LPpZ)J;^~NDC9o;+|ify5q^Z+~}r~bWFtv8#b;5N6F%T zh9Q2R{a&r|U)2@;IgyF6UTmI3tzX1!nST@^QP$V_qZVtm#9wTzwZCbi{LlWU{0Anp BEK2|Y literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/jquery-openid/images/myopenid-2.png b/forum/skins/default/media/jquery-openid/images/myopenid-2.png new file mode 100644 index 0000000000000000000000000000000000000000..f64fb8e81b1adfbdf56a27de64731491f9cb82d2 GIT binary patch literal 511 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Y)RhkE)4%caKYZ?lYt_f1s;*b zK-vS0-A-oPfdtD69Mgd`SU*F|v9*U87#KA?T^vI!PA4ZU5HLs*?m4T~osf_a(WfS} zvm)v`SIO2h5hl-#3=Gb+7M(XRF!+<6knkWmA%Wwf`PRDfmtQUhpLjKMgGGYSl&}b! zwWmuyZqbuuV`Hl{v3$dJByFmt^UT=p<)yYQo%-{xeeGy$T*&Rq8zz20<$prqO>>R6 zE$12RG-|oJ88!xw#@LoR68=nUA^bgP7O;gK1jMIbk>J*@O-}%8c!i(EIWI z{rz}FN5!-nDf^l~AEq;E{tD!I@bFpcygPA6JI*)UIkoM-yj|S^p637m742qhFYP_hAF>%3ImO7yz6kU8N?5ex!R_zs z>o0dpe>}!^&mjdE692ya)mHQlOyXqlj0-XqdN%7fFaT9cTq8=7i&7IyQgu^+1OqUD zEOiYmbPX&+3=FLdO{`1}bPY_bfZ?@5IRvI5H$NpatrA&-nXZv>h>?+%i4jmKL__^E RVR@hi22WQ%mvv4FO#r_Lz6k&T literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/jquery-openid/images/myopenid.ico b/forum/skins/default/media/jquery-openid/images/myopenid.ico new file mode 100644 index 0000000000000000000000000000000000000000..ceb06e6a3f0d88fb97cf10475a3062fb0edab33e GIT binary patch literal 2862 zcmeHJJxc>Y5S@6ZrZbS)owhjA&;jnXfl*X745tmMB3MrBW$)n`RP3b417qd6J6bL?UM{+)q(3xlv0Ik%T7j-l59$8$m>#ard~jq(8yIciUu?j(!>?&WKIe?G5c$>` zV+r!H1WS-ha)=*H^!uZEl>O1BP8%xc=fconk~~4DvKCYWn4`b_pUnoZ788}%mc>uh z9HV;s)r}P~NWtqf_p5&HGjTJoZI|S8n)uf0l05g}rdhqaIBpnv^myAWbI*7E=&N3x z95xI+0zR;x-|08c&;6Ul#XdjZARV+no-wSN`};1(+>)AIisPc*P@H*_1Kd%ys#()H z>NUmL)tL6ccj9UxPF`{LG^Rc9JypwV>?^N0yvK~LBc9f{#^OA9dQUv#8Tz7oxfa(K u#=%<%_2}PpAb{bmUKcqz}))c5uC(7v?)v4a2P)ZNa- z@$&T2)z|&~{r~^}A^8LV00000EC2ui01yBW000GQ;3tk`X`bk)Wk@<6#nZYULKH{p zEx|?+kif!I0vIL|#ZMubBmjWH2OtmxIFVa~6JQ7!1CK!f5W#StOTv&C3=E8h2vI1s n+#cd5;2fT3B_0kF0v!+!GARoV78n&7dMN`JIW(4+BOw4gP{MS* literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/jquery-openid/images/openid.gif b/forum/skins/default/media/jquery-openid/images/openid.gif new file mode 100644 index 0000000000000000000000000000000000000000..c718b0e6f37012db6c9c10d9d21c4dea0d0c01bc GIT binary patch literal 740 zcmZ?wbhEHb^k$G`xXQrrKS$vIw-5i{JotZf!T-qt{~ulWe{RG7ST3!;kh({9A)7tlU&$%5*g$f^Ar># z6BXsynVh) zJ?osJf(s)D10z?npvQi179P(djPk7Va&pI%NfVlv<{V@&c*Q1S?C^%corz71;ll+1P9`Sd{~>qE3OQZcJ(5}z4)|O#`p@vJ zr9;Fsp~HjOs*vv^`}=;ISs7Cf1bMkUpV%Pi{6*$qgF{F5!;-XxTBi=%eG5gPtV^!wdin~r1kkVO&vN=8Ce+Xv z$*|MCqlY(ACp$%vPrNeV;U*4lo)c<`?yngdvl5)QWEV6rHbi?!)@MOEE;CkPT#L!!@77y7u K{UjJ!7_0#i2pVPp literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/jquery-openid/images/openidico.png b/forum/skins/default/media/jquery-openid/images/openidico.png new file mode 100644 index 0000000000000000000000000000000000000000..ab622669dfd9a90d93d01eb3e371b7cafe6c655e GIT binary patch literal 654 zcmeAS@N?(olHy`uVBq!ia0vp^k|4~%3?x6Bmj*I0Fp32Dgt!8^n_%GkWH}&$@vzfy z1rWt>E_T&OqG&47xJU{SOk_34Y=jC7*0n{c5V6N|Jm8Y}^&-pOTo@1JE@fGk2Mm|U zk|4ie21aHU7FITPP98pf0dWZ#894<7C1n*|6AMc#7k5uz|L};|rUGc>Lt$+fQGx%PFW>%kX-!fgBRjqK{jeiYax&1X*6P`ZiOyxhY@aaR2pHCO>zir$)LCtXD8x_6F mSx4_%OzwSX@y%uX6P_|}hZ#5T25SNR#^CAd=d#Wzp$Py+&RzQe literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/jquery-openid/images/openidico16.png b/forum/skins/default/media/jquery-openid/images/openidico16.png new file mode 100644 index 0000000000000000000000000000000000000000..ad718ac5a62bcc6e995e934064f095ead4b6e7c2 GIT binary patch literal 554 zcmV+_0@eMAP)&P@Tx$zK zg`~UL-C8ZQ2-|28!5~Wx5)m0L>Jb1(5jV;Nr?PiaL(OqwOY$%cI)+e zAGk59mne!(`M>};2%H0!rya0}IAG0BlH^dWR(m2M_f_=)@IgeLT5I34aqs`2{fRh^ s=d86?fHUW}g5FWL{!;FK`&|V13)hOgVr~+r0{{R307*qoM6N<$g5FK`NWB;egxu81uWJVQJd%?yFbuOKe63n-+pJq8^_inN|!<5KX zFN*sfuYBh^N#*37U5;*-g&BHG&qzF5KIPbyTdo@Gp3RZ!`IWi(>*ra1sXp?qirOVN z+Itk=U-XFkoPOfbN^#}~I=N0}kqkSk`viJ^ao?*q-!qH#kL$)M@5?;K9~EEDvaZD#)>=N%EREX+1lcxA4c zbH1=*x3k7Nr4-imOyRkvhk35~Crs+v$HZ_e=WoT`Y_=s6ikx`d{>Ip})TM+aN6+|d zsKDGyYt<-~H<*?|Py7&ZkT|u3=hh%dXrI`@B$d-ImSy<^r20u_tJy z{YrS?&d+&*_oKPcrceK*|8w1xcc@~H-P%ytZ}Ltj+A zE!c_l2H#`zaX;dYlru_mk^31KdcB@L9&W3#wIFp`YJOeP`OSr1{CK6O-`2Es@?E#T zy1MFK>-Ep~I=Vd7%e_s#J@}-Zvwh`X=4ClX_h=7BXW;)k1Ifb@+v7M5AZI7aYkiNm z$KjX;Qm=X2(~a>=Zn(6-IG8mv>sbkaBEroVrCFBdHr=3XM61uF&u!`5GI1u;Qf<+zMxU!sr1q{f@m!ic#&7x4 zjcDZqv{&NU85cGO2UiL7I^;$4kVpqJ@-E;pjlK(>l3v2Y;e5IS@q=)?(5+CX;*7LwCwnlxopJBahPURBFV+~&J eFF>coTp;}_7zqbJAQuX63TM^f{)_Klzq8-HK65Pq literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/jquery-openid/images/verisign-2.png b/forum/skins/default/media/jquery-openid/images/verisign-2.png new file mode 100644 index 0000000000000000000000000000000000000000..c14670084ad7edd1b0e9d0795e6528cdef94476b GIT binary patch literal 859 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Y)RhkE)4%caKYZ?lYt_f1s;*b zK-vS0-A-oPfdtD69Mgd`SU*F|v9*U87?@f-T^vI!PEVa2>oGY|}cueE8LQ-r;?xW+qEnNYQEJ<;m0d)a9Q_@mBVkN|!>Qw5|?oEj@ zJHq?c^=`&1?qzR(mMiWxyVlEZV}5@A=jwBo|BFqWIFe>RcdoZ*x}TP)x54RF-iE~s z8DC#e@LbN^5O848Yiql$Ta`ELV`DH#n{(vi8smtFhy%ax$(8JV*Rc1K+L@U%85qvq z7pmEuXL6g%@aFaAyM}BT%H@tx|9dyM>Obn2cTOoZWK*|KKKZ)!@yE$)SOq+4T&ZVtR9A6KuU$Y6Q?$#Qaa}_p@ ziv*gC<@l8sFRJ+A!F1kJ==pE9CPrpHc5R{FIYm32n)aUJnmlO|N5i&BO52{sK0kbA zOM=7O&ft=;y9;$r`wVX1A7Ex4E?i21Paf zk!EFRxSVGEDCsc+!>?d^$k<~9c|4`!9(VT;faV{nLzlk55Tah2J(E1ez6oEP5T-aWN3KX2YP1Ks=A zYy(#2_0JYBF1~uJRbbBS84GlMPM>=wiOhxKu z#nY^gWqq5n3nWU#buPR-G_mk(7c;x?hvJ{?`$|fat4cU-KApNFrdB^?r^)|yoAa;r zUc8nrk(b}TNXJ>ox==$^@_Bx!qMqv0{kMOz|Izv_r~7BpAzFVdQ&MBb@03r5qo&W#< literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/jquery-openid/images/verisign.ico b/forum/skins/default/media/jquery-openid/images/verisign.ico new file mode 100644 index 0000000000000000000000000000000000000000..3953af931987b0e66c122b338dc352502564eafd GIT binary patch literal 4710 zcmeHJc~n&A75@Q)fTGMB7?4e2SOip*O~H*}8$=cdnTIhNBRWh_BW_>?5imwU5pfq4 zQ4m>QjBTZ+CRIeErNk zREwwe2CX!zp{LmSu(HSiXS0Z~ELfW_hmFlL(pSOSItLB|b75<{0z+Ju!Pzbsp04@u z^IM5gez^!5o)68~JR~M1V)|=3XwNQ8?a>Ya%3;dK~By}6c*$o zzc7z#uYNYpl*u)SjcUZ3@pYK4t;Vd>gP50g5b0^P$e34y71<3~mHQrY z^v!hsAEG!C)5bMuezljdfeI&}&sTTWohhE|ksJA(uJTd-&Q zMO2ly;NXEa9IkGqoEFsAT)_K>+aNTx;izy9$B(q(h|rGX$9{>EKmIk&oIQ<;7cbz` z_Vf7oOgqlCbl~T$U*gkC?YPwb87^PGj4!TTMMqaVuHU?hTbV;iCum zZRbOL{qcZs;#f*}wrlz{`0xBe*(I;Sz zyL{rRJv~s#3{DZZtH%`9aNO9+=O$@yE9B}Ppw5xAUn^OeZyC|qoIL(nKmn&nYY>eL zKrf+{E*!VX7~r9ZuM^b@Gr+V$Li3f}Ym!#NDK|=*KfCmKlHM?d^N_SO`K0ZlR@aaD z`$@HgRh&}Pnx=3wq}{WbyQe^@gROXtjgE@>Qu-jEKP!rl1Z1YS(s35^4X=At_Fe%L7)Z2wJ zI=BcM$Zt(2XjtYnSb8HX6)sr~HNO7Fi=k7(q*OSR;1tv-=GQtZrfwBAkrlHYyo$=k zI88HxY21*dylU_ihyn*o`a+siMM#3?z2La9_bI5tty|5m@_$Kv=M-r`DWnsuDXI|SI(OnaFs5Rj zX)H<$bK-%v#3C6u8ZzR^Dh7HHqo*OR6YUU|4ygcb%!+EsW(iDpCu}sLU`Ao=$b{)2 zqYUx~O12syWYaUKC^j&d+|rIZmK*4)3nD=!V^Y|D)97J|C?PnK9W{j|5sikK_<-~j zB9Ma7re4g!anVfBnN3BRq6!uR)Iqc*BNYKUJ~~i(NLEqh;Ss1(xyk6vj3QL-oI(}E ztJRXtBFNMQ1%(&^QRrgWCG@7zAyK_e%IJBC60Uv0=A*yw|Kz_F$NuLuvWqkMvw2)i zo$zJE=5ZUFK~Q{S_xsb>KZ5B3WHsh5CkCG#~pZKK$?_l$V}GW#u_kRkqUX z`UyTd@da8=wc+%sYiMmfjdN#zfeRNd;rz$faN&FhZrr$v&dv^;YrTU@mu}(8l`dTS zyp!h7d$`$gpJq>*C%Yct?%liirkiHU``!4q`ysx0_@`GhR*Wg zL+$LnVne>U0!F8BBDJgQ#SO&zJnpsR{N}Sv)RGX5ladO5OX7h<2Rs-#nHmcwuE~3rz z2>~NUOyad;2ZtE}qZs8M)E9^dp|1)1sQ>G$Z=aRsiI#u(rfHU-9}N`$^h{weM0ms) zh?4E1IgP=fR~iiIb_PS5COyWGoN9OA+h%v^2+Yx zGJaHiGz3ONU|59!7au1(GXnz`A0HnV&|@H)n}q>L0s$9;ARnmIVdDT(f_z;3d|=N* zDLy_ze4rMf1_3@kMlc035Cr)7nOM2`_ymE1g8cmaK$@2aOo2_OfcXEPfdPgOHZw3Z X0PU5rU|_JTL0}LcBo2~?sfE!1%eOAT literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/jquery-openid/images/vidoop.png b/forum/skins/default/media/jquery-openid/images/vidoop.png new file mode 100644 index 0000000000000000000000000000000000000000..032c9e9897bb71bb0a603534bfac1a442be1cf20 GIT binary patch literal 499 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Y)RhkE)4%caKYZ?lYt_f1s;*b zK-vS0-A-oPfdtD69Mgd`SU*F|v9*U87#L+dT^vI!PM@7*>(3M@;Cgqe21^@jt1Q7KOpyh+ge)Q1 zK|J4huRH_#zwbc3UEA;R9AO!H&d%roag-+O5 zO!N95T@?S*G}Sgw{mN!=VmhZ5Rzf4>LLtlrZUAWLgGPY+oVrG5xo^yW`xZU-Q@wk~@ zYxS@D)yH#t7kvCm!TtK*mFh-7(+TYB+*i5pa!nc|Jf3SU=XN{`fjrfux$lA7e}n0{ zlfCqFi>Z(M`wVVG>yKK1*9rZ1`iw}=W^iIUOLNU?<8e1$!1FrglCO_u)5YTUvK?eu yI~YIj#-~XLO;hd+o2#A2S^08*UB1qS`QCUE52vFxJ9}scm-~xFcCkL1j=lhFMR`a7 literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/jquery-openid/images/wordpress.png b/forum/skins/default/media/jquery-openid/images/wordpress.png new file mode 100644 index 0000000000000000000000000000000000000000..ee29f0cf1acf19c7dfe7407b582422511d8d20c9 GIT binary patch literal 566 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Y)RhkE)4%caKYZ?lYt_f1s;*b zK-vS0-A-oPfdtD69Mgd`SU*F|v9*U87#QO{T^vI!POqJOFt^!3z;%8gR{`e|W&z#@ zTsh$k`!={g;#w5)pddwmoA!p_Mo&iVU?$Z7C%!+f+Dl}VQdLz&Yw`xl0Z#EMLLB^eIW!H4}F&{V=c{y`~F=GJ7WcP!sgnTVS9uyb*-2UJa zyX8gU^EHyJ5)F#0Z>a>7vTsvVNHXe-51kX5%U84V#``+8kTv3J@u{J|FE}JUSQ0Bg zlc#vvo$#l^NBfjkE2}kMVfyAM?Qu71W8A0etJe-(wf?BN$j{t_Xf0)0$=*^XiS;GhnchwTth?3-@)Wnih z-4r0fU}RumscT@NYhW2-U}$A%Vr6QeYhYq!VDM*!atKUAZhlH;S|zduGhHL&5F;Zi Z6CrIb?D>k<=p7w-06N?k+!11Z%BingssxX*~`Jx%e>NQL4U@#&si&Ry{X0d`1)-{ zg6ib$l60Y~mAI;tw^}Z8;o0SWTajTmc&LxG+tT3I%-z$;*=0U@n}Df z_WAbn_i93aS}k+@`~3Iy_*yJ-{r&yp+~-;?aeZ2la7u;o?eq2W_P3J0;iQSMZ9#_O-sRiQ;BHNT`1AO8Qj6r> z=!+x&Fkmx*3aAJ-s-85w)XV*n|rAH`~33k^Rb@1vYox} z>+;~*+x%&C>Lw4uMvz|_vc)vS@X=;7?8j0^uVyl!LrHg=Iz3>%W_GG_VoC(sJZv``HXa* zrIxkU(csd>*m+iriD#Si^7g{9%5+bN>E!JE{QaMVtz9y8td_W5GInuEg!AzAjBA~X zY@AsvasU7SA^8LV00000EC2ui08{`F000R80L>t9BSw)JIS9nHVe#jm!-o(fN}Ncs zqD6~fM)VmW02LY`Stc0KkV8WON`_FRxX7}l%ZU3P9D0C2ViSKjMS*eC!QC5NWc*24 z$ACo`hlu15G!rIFqD1JxyfmSQfQkeLyuEbT;aCwDIQ`|T5~at(emLt$WH9QCKO?Fj z!YZJpKo=h-$7uT!(ZdXCA3nU9VnV{d0N()k(Y5u#B%yvAamGe5)veXFd%|B&z}{E8T{37 zh=$XgAq(IDLK-DxqXs0h{KJhuWN1QBJY)EyRU!TiVM;~maN!RZP&~xT1YQjCP&*>v zG{z^SKvar9Z%BlLKM^>@P9t6rA<+h8P*4UH{%m1KMF#8;qhgZOW6=OR_%lH)|M;QC z8ZSfy%{eR3AQ2<}h+xkGOhf{J3Vrwkz(32laK<*D3lQa{dk%K=0FpwEPj!g6c z8PB|?!Vn>}!~`5T1t5_zE=)iG0UUIK4SoL5A(0aP5b%#W8tjnDD-wa@&pQ_U;|wMK zP_awA|9s&=3UeHwj6-G|f<{Y6;N(Ia64h{zIqvjAj2hSc^NKS(WVVJ5CDigyFgPgS zk0p~ZbHfV$&=8I(61-3X6MR?_!$S{5GXw`Ytg(h5N(#^hz#+8og#&*SW6=pXX^~7s zFkB+QLkzgEL@`eIumT%^WMK#(^!)R|8hsQ|gbd{TLyHi#R)~`zC)5;?9Um+JL^Gdc zPzgiQO?9dAYKu|J14=HfQCJs5ExCItCfU89hC~?OkX(YSVLmi;d zz!!K_6YW}g;8BGXI9aiX4Mdod4IRil;>SO66v75h2HbJ^?H7Hao9>T4MsJEFFH&Hw-a literal 0 HcmV?d00001 diff --git a/forum/skins/default/media/jquery-openid/jquery.openid.js b/forum/skins/default/media/jquery-openid/jquery.openid.js new file mode 100644 index 0000000..8d1cd20 --- /dev/null +++ b/forum/skins/default/media/jquery-openid/jquery.openid.js @@ -0,0 +1,111 @@ +//jQuery OpenID Plugin 1.1 Copyright 2009 Jarrett Vance http://jvance.com/pages/jQueryOpenIdPlugin.xhtml +$.fn.openid = function() { + var $this = $(this); + + //name input value - needed for name based OpenID + var $usr = $this.find('input[name=openid_username]'); + + //final url input value + var $id = $this.find('input[name=openid_url]'); + + //beginning and end of name OpenID url (name being the middle) + var $front = $this.find('p:has(input[name=openid_username])>span:eq(0)'); + var $end = $this.find('p:has(input[name=openid_username])>span:eq(1)'); + + //needed for special effects only + var $localfs = $this.find('fieldset:has(input[name=username])'); + var $usrfs = $this.find('fieldset:has(input[name=openid_username])'); + var $idfs = $this.find('fieldset:has(input[name=openid_url])'); + + var submitusr = function() { + if ($usr.val().length < 1) { + $usr.focus(); + return false; + } + $id.val($front.text() + $usr.val() + $end.text()); + return true; + }; + + var submitid = function() { + if ($id.val().length < 1) { + $id.focus(); + return false; + } + return true; + + }; + var local = function() { + var $li = $(this); + $('#openid_form .providers li').removeClass('highlight'); + $li.addClass('highlight'); + $usrfs.hide(); + $idfs.hide(); + $localfs.show(); + $this.unbind('submit').submit(submitid); + return false; + }; + + var direct = function() { + var $li = $(this); + $('#openid_form .providers li').removeClass('highlight'); + $li.addClass('highlight'); + $usrfs.fadeOut('slow'); + $localfs.fadeOut('slow'); + $idfs.fadeOut('slow'); + $id.val($this.find("li.highlight span").text()); + setTimeout(function(){$('#bsignin').click();},1000); + return false; + }; + + var openid = function() { + var $li = $(this); + $('#openid_form .providers li').removeClass('highlight'); + $li.addClass('highlight'); + $usrfs.hide(); + $localfs.hide(); + $idfs.show(); + $id.focus(); + $this.unbind('submit').submit(submitid); + return false; + }; + + var username = function() { + var $li = $(this); + $('#openid_form .providers li').removeClass('highlight'); + $li.addClass('highlight'); + $idfs.hide(); + $localfs.hide(); + $usrfs.show(); + $this.find('#enter_your_what').text($li.attr("title")); + $front.text($li.find("span").text().split("username")[0]); + $end.text("").text($li.find("span").text().split("username")[1]); + $id.focus(); + $this.unbind('submit').submit(submitusr); + return false; + }; + + $this.find('li.local').click(local); + $this.find('li.direct').click(direct); + $this.find('li.openid').click(openid); + $this.find('li.username').click(username); + $id.keypress(function(e) { + if ((e.which && e.which == 13) || (e.keyCode && e.keyCode == 13)) { + return submitid(); + } + }); + $usr.keypress(function(e) { + if ((e.which && e.which == 13) || (e.keyCode && e.keyCode == 13)) { + return submitusr(); + } + }); + $this.find('li span').hide(); + $this.find('li').css('line-height', 0).css('cursor', 'pointer'); + $usrfs.hide(); + $idfs.hide(); + $localfs.hide(); + $this.find('li:eq(0)').click(); + + return this; +}; +// submitting next=%2F&openid_username=&openid_url=http%3A%2F%2Fyahoo.com%2F +// submitting next=%2F&openid_username=&openid_url=http%3A%2F%2Fyahoo.com%2F diff --git a/forum/skins/default/media/jquery-openid/openid.css b/forum/skins/default/media/jquery-openid/openid.css new file mode 100644 index 0000000..1b7aaf8 --- /dev/null +++ b/forum/skins/default/media/jquery-openid/openid.css @@ -0,0 +1,75 @@ +fieldset { border-style:none; } +img {border-style:none;} + +.logo_box {display:inline-block;float:left;width:90px;height:40px;background:white;border:1px solid #dddddd;} +.openid_box img {margin-top:6px;} +.aol_box img {margin-top:6px;} +.yahoo_box img {margin-top:13px;} +.google_box img {margin-top:6px;} +.local_login_box img {margin-top:2px;margin-left:-3px;} + +form.openid ul{ margin:0;padding:0;text-align:center; list-style-type:none; display:block;} +form.openid ul li {float:left; padding:4px;display:inline-block;} +form.openid ul li div {display:inline-block;} +form.openid ul li span {padding:0 1em 0 3px} +form.openid ul li.first_tiny_li {clear:left;} +form.openid fieldset {clear:both;padding:10px 0px 0px 0px;} +form.openid div+fieldset {display:none} +form.openid label {display:block; font-weight:bold;} +input[name=openid_username] {width:8em} +input[name=openid_identifier] {width:18em} +form.openid ul li.highlight { -moz-border-radius:4px; -webkit-border-radius:4px; background-color: #FD6} +form.openid fieldset div { + -moz-border-radius:4px; + -webkit-border-radius:4px; + background: #DCDCDC; + padding:10px; + display:inline-block; + float:left; +} +form.openid p {margin-bottom:4px;} +form.openid fieldset div p {padding:0px;margin:0px;} +form.openid fieldset div p.login {padding:0px;margin:0 0 10px 0;} +form.openid label { + display:inline-block; + font-weight:normal; + width:6em; + text-align:right; +} +#local_login_fs div { + padding-bottom:4px; +} +#local_login_buttons { + text-align:center; + line-height:1.8em; + margin-top:3px; +} +/*form.openid input[type='submit'] {margin-left:1em;}*/ +#openid_username {background:#ffffa0;} +#openid_url {background:#ffffa0;} + +.openid_logo{color:#F7931E;padding:6px 0px 8px 28px; +background: url(images/openidico.png) no-repeat; +} + +#openid_login {float:left; width:30%; margin:2em 1em; text-align:center} +#openid_login div{margin-top:0.5em} + +form.openid ul.errorlist { + border: none; + list-style-position:inside; + list-style-type: disc; + margin-bottom:5px; +} +form.openid ul.errorlist li { + text-align: left; + margin: 5px; + float: none; + color:blue; +} +#openid_small_providers li { + margin-top:4px; +} +#openid_small_providers li.facebook { + margin-top:0px; +} diff --git a/forum/skins/default/media/js/com.cnprog.admin.js b/forum/skins/default/media/js/com.cnprog.admin.js new file mode 100644 index 0000000..39dff48 --- /dev/null +++ b/forum/skins/default/media/js/com.cnprog.admin.js @@ -0,0 +1,13 @@ +$(document).ready( function(){ + var options = { + success: function(a,b){$('.admin #action_status').html($.i18n._('changes saved'));}, + dataType:'json', + timeout:5000, + url: scriptUrl + $.i18n._('moderate-user/') + viewUserID + '/' + }; + var form = $('.admin #moderate_user_form').ajaxForm(options); + var box = $('.admin input#id_is_approved').click(function(){ + $('.admin #action_status').html($.i18n._('sending data...')); + form.ajaxSubmit(options); + }); +}); diff --git a/forum/skins/default/media/js/com.cnprog.editor.js b/forum/skins/default/media/js/com.cnprog.editor.js new file mode 100644 index 0000000..18cc516 --- /dev/null +++ b/forum/skins/default/media/js/com.cnprog.editor.js @@ -0,0 +1,68 @@ +/* + jQuery TextAreaResizer plugin + Created on 17th January 2008 by Ryan O'Dell + Version 1.0.4 +*/(function($){var textarea,staticOffset;var iLastMousePos=0;var iMin=32;var grip;$.fn.TextAreaResizer=function(){return this.each(function(){textarea=$(this).addClass('processed'),staticOffset=null;$(this).wrap('
    ').parent().append($('
    ').bind("mousedown",{el:this},startDrag));var grippie=$('div.grippie',$(this).parent())[0];grippie.style.marginRight=(grippie.offsetWidth-$(this)[0].offsetWidth)+'px'})};function startDrag(e){textarea=$(e.data.el);textarea.blur();iLastMousePos=mousePosition(e).y;staticOffset=textarea.height()-iLastMousePos;textarea.css('opacity',0.25);$(document).mousemove(performDrag).mouseup(endDrag);return false}function performDrag(e){var iThisMousePos=mousePosition(e).y;var iMousePos=staticOffset+iThisMousePos;if(iLastMousePos>=(iThisMousePos)){iMousePos-=5}iLastMousePos=iThisMousePos;iMousePos=Math.max(iMin,iMousePos);textarea.height(iMousePos+'px');if(iMousePos1&&!select.visible()){onChange(0,true);}}).bind("search",function(){var fn=(arguments.length>1)?arguments[1]:null;function findValueCallback(q,data){var result;if(data&&data.length){for(var i=0;i1){v=words.slice(0,words.length-1).join(options.multipleSeparator)+options.multipleSeparator+v;}v+=options.multipleSeparator;}$input.val(v);hideResultsNow();$input.trigger("result",[selected.data,selected.value]);return true;}function onChange(crap,skipPrevCheck){if(lastKeyPressCode==KEY.DEL){select.hide();return;}var currentValue=$input.val();if(!skipPrevCheck&¤tValue==previousValue)return;previousValue=currentValue;currentValue=lastWord(currentValue);if(currentValue.length>=options.minChars){$input.addClass(options.loadingClass);if(!options.matchCase)currentValue=currentValue.toLowerCase();request(currentValue,receiveData,hideResultsNow);}else{stopLoading();select.hide();}};function trimWords(value){if(!value){return[""];}var words=value.split(options.multipleSeparator);var result=[];$.each(words,function(i,value){if($.trim(value))result[i]=$.trim(value);});return result;}function lastWord(value){if(!options.multiple)return value;var words=trimWords(value);return words[words.length-1];}function autoFill(q,sValue){if(options.autoFill&&(lastWord($input.val()).toLowerCase()==q.toLowerCase())&&lastKeyPressCode!=KEY.BACKSPACE){$input.val($input.val()+sValue.substring(lastWord(previousValue).length));$.Autocompleter.Selection(input,previousValue.length,previousValue.length+sValue.length);}};function hideResults(){clearTimeout(timeout);timeout=setTimeout(hideResultsNow,200);};function hideResultsNow(){var wasVisible=select.visible();select.hide();clearTimeout(timeout);stopLoading();if(options.mustMatch){$input.search(function(result){if(!result){if(options.multiple){var words=trimWords($input.val()).slice(0,-1);$input.val(words.join(options.multipleSeparator)+(words.length?options.multipleSeparator:""));}else +$input.val("");}});}if(wasVisible)$.Autocompleter.Selection(input,input.value.length,input.value.length);};function receiveData(q,data){if(data&&data.length&&hasFocus){stopLoading();select.display(data,q);autoFill(q,data[0].value);select.show();}else{hideResultsNow();}};function request(term,success,failure){if(!options.matchCase)term=term.toLowerCase();var data=cache.load(term);if(data&&data.length){success(term,data);}else if((typeof options.url=="string")&&(options.url.length>0)){var extraParams={timestamp:+new Date()};$.each(options.extraParams,function(key,param){extraParams[key]=typeof param=="function"?param():param;});$.ajax({mode:"abort",port:"autocomplete"+input.name,dataType:options.dataType,url:options.url,data:$.extend({q:lastWord(term),limit:options.max},extraParams),success:function(data){var parsed=options.parse&&options.parse(data)||parse(data);cache.add(term,parsed);success(term,parsed);}});}else{select.emptyList();failure(term);}};function parse(data){var parsed=[];var rows=data.split("\n");for(var i=0;i]*)("+term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi,"\\$1")+")(?![^<>]*>)(?![^&;]+;)","gi"),"$1");},scroll:true,scrollHeight:180};$.Autocompleter.Cache=function(options){var data={};var length=0;function matchSubset(s,sub){if(!options.matchCase)s=s.toLowerCase();var i=s.indexOf(sub);if(i==-1)return false;return i==0||options.matchContains;};function add(q,value){if(length>options.cacheLength){flush();}if(!data[q]){length++;}data[q]=value;}function populate(){if(!options.data)return false;var stMatchSets={},nullData=0;if(!options.url)options.cacheLength=1;stMatchSets[""]=[];for(var i=0,ol=options.data.length;i0){var c=data[k];$.each(c,function(i,x){if(matchSubset(x.value,q)){csub.push(x);}});}}return csub;}else +if(data[q]){return data[q];}else +if(options.matchSubset){for(var i=q.length-1;i>=options.minChars;i--){var c=data[q.substr(0,i)];if(c){var csub=[];$.each(c,function(i,x){if(matchSubset(x.value,q)){csub[csub.length]=x;}});return csub;}}}return null;}};};$.Autocompleter.Select=function(options,input,select,config){var CLASSES={ACTIVE:"ac_over"};var listItems,active=-1,data,term="",needsInit=true,element,list;function init(){if(!needsInit)return;element=$("
    ").hide().addClass(options.resultsClass).css("position","absolute").appendTo(document.body);list=$("
      ").appendTo(element).mouseover(function(event){if(target(event).nodeName&&target(event).nodeName.toUpperCase()=='LI'){active=$("li",list).removeClass(CLASSES.ACTIVE).index(target(event));$(target(event)).addClass(CLASSES.ACTIVE);}}).click(function(event){$(target(event)).addClass(CLASSES.ACTIVE);select();input.focus();return false;}).mousedown(function(){config.mouseDownOnSelect=true;}).mouseup(function(){config.mouseDownOnSelect=false;});if(options.width>0)element.css("width",options.width);needsInit=false;}function target(event){var element=event.target;while(element&&element.tagName!="LI")element=element.parentNode;if(!element)return[];return element;}function moveSelect(step){listItems.slice(active,active+1).removeClass(CLASSES.ACTIVE);movePosition(step);var activeItem=listItems.slice(active,active+1).addClass(CLASSES.ACTIVE);if(options.scroll){var offset=0;listItems.slice(0,active).each(function(){offset+=this.offsetHeight;});if((offset+activeItem[0].offsetHeight-list.scrollTop())>list[0].clientHeight){list.scrollTop(offset+activeItem[0].offsetHeight-list.innerHeight());}else if(offset=listItems.size()){active=0;}}function limitNumberOfItems(available){return options.max&&options.max").html(options.highlight(formatted,term)).addClass(i%2==0?"ac_even":"ac_odd").appendTo(list)[0];$.data(li,"ac_data",data[i]);}listItems=list.find("li");if(options.selectFirst){listItems.slice(0,1).addClass(CLASSES.ACTIVE);active=0;}if($.fn.bgiframe)list.bgiframe();}return{display:function(d,q){init();data=d;term=q;fillList();},next:function(){moveSelect(1);},prev:function(){moveSelect(-1);},pageUp:function(){if(active!=0&&active-8<0){moveSelect(-active);}else{moveSelect(-8);}},pageDown:function(){if(active!=listItems.size()-1&&active+8>listItems.size()){moveSelect(listItems.size()-1-active);}else{moveSelect(8);}},hide:function(){element&&element.hide();listItems&&listItems.removeClass(CLASSES.ACTIVE);active=-1;},visible:function(){return element&&element.is(":visible");},current:function(){return this.visible()&&(listItems.filter("."+CLASSES.ACTIVE)[0]||options.selectFirst&&listItems[0]);},show:function(){var offset=$(input).offset();element.css({width:typeof options.width=="string"||options.width>0?options.width:$(input).width(),top:offset.top+input.offsetHeight,left:offset.left}).show();if(options.scroll){list.scrollTop(0);list.css({maxHeight:options.scrollHeight,overflow:'auto'});if($.browser.msie&&typeof document.body.style.maxHeight==="undefined"){var listHeight=0;listItems.each(function(){listHeight+=this.offsetHeight;});var scrollbarsVisible=listHeight>options.scrollHeight;list.css('height',scrollbarsVisible?options.scrollHeight:listHeight);if(!scrollbarsVisible){listItems.width(list.width()-parseInt(listItems.css("padding-left"))-parseInt(listItems.css("padding-right")));}}}},selected:function(){var selected=listItems&&listItems.filter("."+CLASSES.ACTIVE).removeClass(CLASSES.ACTIVE);return selected&&selected.length&&$.data(selected[0],"ac_data");},emptyList:function(){list&&list.empty();},unbind:function(){element&&element.remove();}};};$.Autocompleter.Selection=function(field,start,end){if(field.createTextRange){var selRange=field.createTextRange();selRange.collapse(true);selRange.moveStart("character",start);selRange.moveEnd("character",end);selRange.select();}else if(field.setSelectionRange){field.setSelectionRange(start,end);}else{if(field.selectionStart){field.selectionStart=start;field.selectionEnd=end;}}field.focus();};})(jQuery); +/* + * TypeWatch 2.0 - Original by Denny Ferrassoli / Refactored by Charles Christolini + * Copyright(c) 2007 Denny Ferrassoli - DennyDotNet.com + * Coprright(c) 2008 Charles Christolini - BinaryPie.com + * Dual licensed under the MIT and GPL licenses: + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html +*/(function(jQuery){jQuery.fn.typeWatch=function(o){var options=jQuery.extend({wait:750,callback:function(){},highlight:true,captureLength:2},o);function checkElement(timer,override){var elTxt=jQuery(timer.el).val();if((elTxt.length>options.captureLength&&elTxt.toUpperCase()!=timer.text)||(override&&elTxt.length>options.captureLength)){timer.text=elTxt.toUpperCase();timer.cb(elTxt)}};function watchElement(elem){if(elem.type.toUpperCase()=="TEXT"||elem.nodeName.toUpperCase()=="TEXTAREA"){var timer={timer:null,text:jQuery(elem).val().toUpperCase(),cb:options.callback,el:elem,wait:options.wait};if(options.highlight){jQuery(elem).focus(function(){this.select()})}var startWatch=function(evt){var timerWait=timer.wait;var overrideBool=false;if(evt.keyCode==13&&this.type.toUpperCase()=="TEXT"){timerWait=1;overrideBool=true}var timerCallbackFx=function(){checkElement(timer,overrideBool)};clearTimeout(timer.timer);timer.timer=setTimeout(timerCallbackFx,timerWait)};jQuery(elem).keydown(startWatch)}};return this.each(function(index){watchElement(this)})}})(jQuery); +/* +Ajax upload +*/jQuery.extend({createUploadIframe:function(d,b){var a="jUploadFrame"+d;if(window.ActiveXObject){var c=document.createElement('