Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Comitted first public revision of Solace. Development history from the

internal SVN was dropped for security reasons.
  • Loading branch information...
commit 0acfd9e704626bc32273ca15e85e9867b3917a44 0 parents
@mitsuhiko authored
Showing with 23,136 additions and 0 deletions.
  1. +8 −0 .hgignore
  2. +21 −0 AUTHORS
  3. +185 −0 HACKING
  4. +31 −0 LICENSE
  5. +170 −0 README
  6. BIN  artwork/arrows.psd
  7. +495 −0 artwork/recycle.svg
  8. BIN  artwork/tick.psd
  9. +73 −0 setup.py
  10. +14 −0 solace/__init__.py
  11. +413 −0 solace/application.py
  12. +231 −0 solace/auth.py
  13. +223 −0 solace/badges.py
  14. +371 −0 solace/database.py
  15. +258 −0 solace/forms.py
  16. +255 −0 solace/i18n/__init__.py
  17. +1 −0  solace/i18n/de/LC_MESSAGES/messages.js
  18. BIN  solace/i18n/de/LC_MESSAGES/messages.mo
  19. +1,214 −0 solace/i18n/de/LC_MESSAGES/messages.po
  20. +1,097 −0 solace/i18n/messages.pot
  21. +967 −0 solace/models.py
  22. +27 −0 solace/packs.py
  23. +385 −0 solace/scripts.py
  24. +190 −0 solace/settings.py
  25. BIN  solace/static/arrows.png
  26. BIN  solace/static/arrows_gray.png
  27. +160 −0 solace/static/babel.js
  28. BIN  solace/static/badge_earned.png
  29. BIN  solace/static/badge_not_earned.png
  30. +65 −0 solace/static/badges.css
  31. BIN  solace/static/badges/critic.png
  32. BIN  solace/static/badges/editor.png
  33. BIN  solace/static/badges/good_answer.png
  34. BIN  solace/static/badges/great_answer.png
  35. BIN  solace/static/badges/inquirer.png
  36. BIN  solace/static/badges/nice_answer.png
  37. BIN  solace/static/badges/reversal.png
  38. BIN  solace/static/badges/self_critic.png
  39. BIN  solace/static/badges/self_learner.png
  40. BIN  solace/static/badges/troubleshooter.png
  41. BIN  solace/static/badges/unique_answer.png
  42. +272 −0 solace/static/creole.js
  43. BIN  solace/static/feed.png
  44. BIN  solace/static/flashtick.png
  45. +10 −0 solace/static/ie.css
  46. +9 −0 solace/static/ie6.css
  47. +759 −0 solace/static/jquery.autocomplete.js
  48. +643 −0 solace/static/jquery.form.js
  49. +4,376 −0 solace/static/jquery.js
  50. +1,188 −0 solace/static/layout.css
  51. BIN  solace/static/recycle.png
  52. BIN  solace/static/recycle_big.png
  53. +472 −0 solace/static/solace.js
  54. +234 −0 solace/static/teal.css
  55. BIN  solace/static/tick.png
  56. BIN  solace/static/tick_gray.png
  57. +28 −0 solace/templates/_helpers.html
  58. +15 −0 solace/templates/api/debug_dump.html
  59. +106 −0 solace/templates/api/help.html
  60. +30 −0 solace/templates/badges/show_badge.html
  61. +21 −0 solace/templates/badges/show_list.html
  62. +8 −0 solace/templates/core/about.html
  63. +23 −0 solace/templates/core/login.html
  64. +10 −0 solace/templates/core/no_javascript.html
  65. +7 −0 solace/templates/core/not_found.html
  66. +12 −0 solace/templates/core/register.html
  67. +24 −0 solace/templates/core/reset_password.html
  68. +168 −0 solace/templates/kb/_boxes.html
  69. +12 −0 solace/templates/kb/_comments.html
  70. +28 −0 solace/templates/kb/_editor.html
  71. +22 −0 solace/templates/kb/by_tag.html
  72. +17 −0 solace/templates/kb/delete_post.html
  73. +26 −0 solace/templates/kb/edit_post.html
  74. +14 −0 solace/templates/kb/new.html
  75. +19 −0 solace/templates/kb/overview.html
  76. +36 −0 solace/templates/kb/post_revisions.html
  77. +28 −0 solace/templates/kb/restore_post.html
  78. +23 −0 solace/templates/kb/sections.html
  79. +18 −0 solace/templates/kb/tags.html
  80. +47 −0 solace/templates/kb/topic.html
  81. +19 −0 solace/templates/kb/unanswered.html
  82. +90 −0 solace/templates/layout.html
  83. +13 −0 solace/templates/mails/activate_user.txt
  84. +6 −0 solace/templates/mails/layout.txt
  85. +12 −0 solace/templates/mails/reset_password.txt
  86. +12 −0 solace/templates/users/edit_profile.html
  87. +76 −0 solace/templates/users/profile.html
  88. +37 −0 solace/templates/users/userlist.html
  89. +59 −0 solace/templating.py
  90. +112 −0 solace/tests/__init__.py
  91. +126 −0 solace/tests/core_views.py
  92. +124 −0 solace/tests/kb_views.py
  93. +215 −0 solace/tests/models.py
  94. +84 −0 solace/tests/querycount.py
  95. +96 −0 solace/urls.py
  96. +10 −0 solace/utils/__init__.py
  97. +2,348 −0 solace/utils/_translit_tab.py
  98. +184 −0 solace/utils/api.py
  99. +26 −0 solace/utils/ctxlocal.py
  100. +268 −0 solace/utils/formatting.py
  101. +1,859 −0 solace/utils/forms.py
  102. +115 −0 solace/utils/lazystring.py
  103. +147 −0 solace/utils/mail.py
  104. +96 −0 solace/utils/pagination.py
  105. +82 −0 solace/utils/recaptcha.py
  106. +77 −0 solace/utils/remoting.py
  107. +257 −0 solace/utils/support.py
  108. +10 −0 solace/views/__init__.py
  109. +124 −0 solace/views/api.py
  110. +35 −0 solace/views/badges.py
  111. +204 −0 solace/views/core.py
  112. +565 −0 solace/views/kb.py
  113. +89 −0 solace/views/users.py
8 .hgignore
@@ -0,0 +1,8 @@
+\.py[oc]$
+\.DS_Store$
+^dist/
+\.egg$
+\.log$
+\.cfg$
+^env/
+\.egg-info$
21 AUTHORS
@@ -0,0 +1,21 @@
+Solace is written and maintained by the Plurk Inc. and various contributors.
+
+Development Lead:
+
+- Armin Ronacher <armin.ronacher@active-4.com>
+
+Some parts of the code are taken from existing projects:
+
+ From the Zine blogging platform:
+
+ - solace.utils.forms
+ - solace.utils.mail
+ - solace.i18n
+
+ From the Sphinx documentation tool:
+
+ - solace.scripts
+
+ From Jason Kirtland's translit tab:
+
+ - solace.utils._translit_tab
185 HACKING
@@ -0,0 +1,185 @@
+
+
+ // HACKING ON SOLACE //
+
+ ~ HACKING ~
+
+ Hacking on Solace is fun. It's written in Python and based
+ on pupular libraries. The dynamic parts in the user interface
+ are created using HTML 4 and jQuery.
+
+ This file should give you a brief overview how the code works
+ and what you should keep in mind when working on it.
+
+ ~ STYLEGUIDE ~
+
+ Solace follows PEP 8 for Python code. The rule you should
+ follow is "adapt to the code style used in the file". And
+ that also means, put whitespace where the original author
+ put it and so on.
+
+ Indent with 4 *spaces* only.
+
+ JavaScript code is intended with 2 spaces and only two spaces.
+ The same rule applies for HTML as well.
+
+ **Every page has to validate against HTML 4.01 transitional**.
+
+ If you need features HTML 4 does not give you, use JavaScript!
+ We're using the transitional DTD because recaptcha uses iframes
+ for non-JavaScript enabled browsers.
+
+ We will probably switch to HTML 5 once support is widespread
+ but until then, the doctype has to be HTML 4.01 transitional.
+ The reason for this rule is that many browser validators such as
+ the firefox validator plugin does not support HTML 5 yet which
+ makes it nearly impossible to ensure that every page validates.
+
+ CSS spacing rules should follow the existing rules in the files.
+ Use whitespace after property, value colons.
+
+ CSS files *have* to use relative paths, so do templates. A
+ template should use `url_for` where possible. The application
+ does not enforce itself to be mounted on the URL root, so assume
+ it can be anywhere.
+
+ CSS classes are lowercase and use underscore for separation.
+
+ All filenames have to be lowercase and ASCII only.
+
+ ~ URLS ~
+
+ Rules for URLs are simple. Stuff that depends on a language and
+ is *publically* available is mounted below `/<lang_code>`.
+
+ Stuff invoked by JavaScript, that redirects back or is remotely
+ JavaScript related has a leading underscore.
+
+ Examples:
+
+ /login
+ /<lang_code>/
+ /_set_timezone
+ /_vote/<post>
+
+ ~ INTERFACE GUIDELINES ~
+
+ The Solace user interface is intended to be used by everybody
+ not just hackers. Keep that in mind when working on the code
+ base.
+
+ // Emphasis //
+
+ The emphasis is always on the non-technical elements. For
+ example in the overview page the emphasis is on the title,
+ number of votes and replies, but not on the tags. Tags are
+ elements not everybody is familiar with.
+
+ // Font Sizes //
+
+ The following font sizes are in use:
+
+ 10px preferred bold and only for navigation, errors
+ and popup menus.
+ 11px for meta information such as authors etc.
+ 13px for regular text
+ 20px for headline like content such as questions on
+ the overview page.
+ 24px the biggest headline and in caps the header
+
+ Special font sizes
+
+ 18px used by the hinted editor. As long as a field is
+ empty it may display a 18px big and #69A9B8 colored
+ text instead.
+
+ // Margins & Padding //
+
+ Standard margins 10px
+ above header, below footer 50px
+ small separation 3px 7px
+
+ // Colors //
+
+ Use the following colors for the CSS:
+
+ #2a606d standard link color, darkish blue
+ #0E3640 very dark blue, nav background
+ (combine with white) or marked text
+ #458696 popup background
+ #C2DDE3 blue box background
+ #E1EDF0 tag background
+ #EEF8FA editor background
+
+ #69A9B8 hint text color
+ #B5DDE6 editor border color
+
+ #FAFAFA preview boxes, deleted content
+ #eee inactive elements, headline background
+ #ccc boxes on deleted content
+ #555 text on deleted content
+
+ #A8DFAA background for inserted text
+ #FDF5F5 background for deleted text
+ #EB4040 color for deleted text
+ #F5F2D7 background for modified text
+
+ // Client-side Scripting //
+
+ The core functionality of Solace should work without
+ JavaScript on modern browsers. (We do not consider either
+ IE6 or IE7 as modern browser).
+
+ Currently the only feature that does not work without
+ JavaScript is adding comments to questions and replies.
+
+ Whenever you cannot provide a non-javascript implementation
+ of a feature, make sure that the user somehow ends up on
+ the special `_no_javascript` page. Either on click or
+ somehow else. That one tells the user to enable JavaScript
+ to use this feature.
+
+ // Language Handling //
+
+ Solace has two indepdendent language context. The language
+ of the UI and the language of the section the user is active
+ in.
+
+ Sometimes the user leaves a section in which case the links
+ that point back to a section (in the navigation bar) will
+ point to the section of the UI language. Whenever that
+ happens the link that points to that section has to be marked
+ with the faded color so that the user knows the link might
+ not take him back to where he came from. At the same time
+ the link to the active section disappears.
+
+ You can try that by clicking on the "badges" link. The
+ "ask" and other buttons will be slightly faded out.
+
+ If you're adding new pages, keep that behavior in mind.
+
+ ~ TEMPLATES ~
+
+ Templates must not contain any CSS information besides classes.
+ Use classes as appropriate, and use as many of them as you like.
+ Keep them easy to read.
+
+ Use macros to ensure that you are using the same elements and
+ classes for the same widget (tags, users, badges etc.)
+
+ ~ UNIT TESTS ~
+
+ Solace uses standard Python unittests for all tests. You can use
+ doctest as well if you convert it into a unittest testsuite
+ using the `doctest.DocTestSuite`.
+
+ In general what you have to do is to add a test to one of the
+ existing test modules (`solace.tests.module`) and make sure that
+ you either add the test to an existing suite or add a new suite
+ and register it in the `suite` factory function.
+
+ If you add a full new module to the test module, also open the
+ `tests` package's `__init__` file and make sure your suite is
+ added properly.
+
+ To run the tests you can use `python setup.py test`.
31 LICENSE
@@ -0,0 +1,31 @@
+Copyright (c) 2009 by the Solace Team, see AUTHORS for more details.
+
+Some rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+
+ * The names of the contributors may not be used to endorse or
+ promote products derived from this software without specific
+ prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
170 README
@@ -0,0 +1,170 @@
+
+
+ // SOLACE //
+
+ ~ a multilingual support system ~
+
+
+ ~ INTRODUCTION ~
+
+ Solace is a multilingual support system developed at Plurk
+ for end user support. The application design is heavily
+ influenced by bulletin boards like phpBB and the new
+ stackoverflow programming community site.
+
+ ~ INSTALLING ~
+
+ For a four-step quickstart have a look at the end of the
+ file. It explains how to quickly test Solace on your
+ local box.
+
+ Solace is developed in Python as a standard conforming WSGI
+ application with the help of the following libraries:
+
+ - Werkzeug
+ - Jinja2
+ - SQLAlchemy
+ - Babel
+ - creoleparser
+ - simplejson
+ - webdepcompress
+
+ If you want to hack on Solace on your own the best way to get
+ started is using the all-mygthy `setup.py` script in a virtual
+ Python environment.
+
+ If you're not familiar with virtualenv, be sure to have it
+ installed and run it like this in the solace folder:
+
+ $ virtualenv env
+
+ Aferwards you can activate it. On linux or OS X you can use
+ the following command:
+
+ $ source env/bin/activate
+
+ If you're working on a Windows box, use the activate batch
+ file instead:
+
+ $ env\Scripts\activate.bat
+
+ After you have activated the environment you can use the
+ `develop` command from the setup script to start working on
+ Solace:
+
+ $ python setup.py develop
+
+ If you want to install it into a virtual environment (or
+ system wide, which we however do not recommend) you can use
+ the `install` command
+
+ $ python setup.py install
+
+ Both `develop` and `install` will take care of dependencies
+ for you.
+
+ ~ THE CONFIGURATION ~
+
+ Where does Solace get the settings from? It comes with some
+ default settings that unless overridden will be the ones it
+ uses. The defaults are intended for development purposes only
+ have *have to be changed* if you want to use Solace in
+ production.
+
+ WHen Solace initializes it checks for a `SOLACE_SETTINGS_FILE`
+ environment variable. If it finds one, it will execute the
+ file set as a Python script and overrides the assigned variables
+ in that script in the config.
+
+ Example configuration:
+
+ DATABASE_URI = 'mysql://root@localhost/my_database'
+ SECRET_KEY = 'a-super-secret-and-random-key'
+
+ ~ SERVER SETUP ~
+
+ As mentioned before Solace is a WSGI application. The WSGI
+ application object is know as `solace.application.application`.
+ For example if you want to use `mod_wsgi` all you have to do
+ is to create a `solace.wsgi` file with the following contents:
+
+ from solace.application import application
+
+ This however would require that the `SOLACE_SETTINGS_FILE`
+ variable is set in the server config. If you don't want to
+ do that, you can also set it in the `.wsgi` file or tell
+ the settings system to load the config from a file:
+
+ from solace import settings
+ settings.configure_from_file('/var/www/solace/solace.cfg')
+ from solace.application import application
+
+ Be sure to use absolute paths for the configuration!
+
+ ~ LOCAL TEST SERVER ~
+
+ If you want to test Solace locally or hack on it, you can use
+ the `runserver` command of the setup script:
+
+ $ python setup.py runserver
+
+ This will start a development server on `localhost:3000`.
+
+ ~ DATABASE INITIALIZATION ~
+
+ Solace uses a database for testing. Currently the following
+ database systems are supported:
+
+ - sqlite3
+ - MySQL
+ - Postgres
+
+ We recommend sqlite3 for testing (which incidentally is the
+ defualt) and Postgres for production.
+
+ To initialize the database make sure to have the database
+ URI set in a config, the `SOLACE_SETTINGS_FILE` environment
+ variable exported and then run the following command:
+
+ $ python setup.py initdb
+
+ This will create the database tables for you.
+
+ If you also want a administrator user to be created you
+ can sue the `reset` command instead:
+
+ $ python setup.py reset
+
+ This is especially handy during development.
+
+ ~ TESTING ~
+
+ Solace is using standard Python unittests which you can run
+ from the `setup.py` script
+
+ $ python setup.py test
+
+ If you want to fill the database with data for testing you can
+ use the setup script as well:
+
+ $ python setup.py make_testdata
+
+ Warning on tests: For tests make sure to have a newer version
+ than 0.5.1 in your venv (at the time of this writing this means
+ installing a development version) due to a bug in the redirect
+ support and path quoting of the test client in 0.5.1.
+
+ ~ QUICKSTART ~
+
+ - make sure to have virtualenv installed
+ - run "virtualenv env" in the folder that contains this file.
+ - depending on your operating system run:
+ Windows: "env\Scripts\activate.bat"
+ Linux / OS X: "source env/bin/activate"
+ - run "python setup.py develop"
+ - run "python setup.py reset"
+ - run "python setup.py runserver"
+
+ The server will then run on `localhost:3000`. The database
+ is stored in a temporary folder unless you provide a config.
+ This is fine for development and testing.
BIN  artwork/arrows.psd
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
495 artwork/recycle.svg
@@ -0,0 +1,495 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+ xmlns:i="&amp;ns_ai;"
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="48"
+ height="48"
+ id="svg57"
+ sodipodi:version="0.32"
+ inkscape:version="0.46"
+ sodipodi:docbase="/home/tigert/cvs/freedesktop.org/tango-icon-theme/scalable/mimetypes"
+ sodipodi:docname="recycle.svg"
+ inkscape:output_extension="org.inkscape.output.svg.inkscape"
+ version="1.0"
+ inkscape:export-filename="/Users/mitsuhiko/Development/plurk/solace/solace/static/recycle.png"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90">
+ <defs
+ id="defs3">
+ <inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 24 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="48 : 24 : 1"
+ inkscape:persp3d-origin="24 : 16 : 1"
+ id="perspective47146" />
+ <linearGradient
+ id="linearGradient381"
+ inkscape:collect="always">
+ <stop
+ id="stop382"
+ offset="0"
+ style="stop-color:#ffffff;stop-opacity:1;" />
+ <stop
+ id="stop383"
+ offset="1"
+ style="stop-color:#ffffff;stop-opacity:0;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient368">
+ <stop
+ style="stop-color:#ffffff;stop-opacity:0.10309278;"
+ offset="0.0000000"
+ id="stop369" />
+ <stop
+ style="stop-color:#ffffff;stop-opacity:0.0000000;"
+ offset="1.0000000"
+ id="stop372" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient1065">
+ <stop
+ style="stop-color:#b5c051;stop-opacity:1.0000000;"
+ offset="0.0000000"
+ id="stop1066" />
+ <stop
+ style="stop-color:#858e3f;stop-opacity:1.0000000;"
+ offset="1.0000000"
+ id="stop1067" />
+ </linearGradient>
+ <linearGradient
+ y2="72.608902"
+ x2="192.3857"
+ y1="72.608902"
+ x1="78.245598"
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient641">
+ <stop
+ id="stop642"
+ style="stop-color:#f9feff;stop-opacity:1.0000000;"
+ offset="0.0000000" />
+ <stop
+ id="stop643"
+ style="stop-color:#afb4b6;stop-opacity:1.0000000;"
+ offset="1.0000000" />
+ </linearGradient>
+ <linearGradient
+ y2="137.97153"
+ x2="136.9856"
+ y1="67.364906"
+ x1="81.307533"
+ gradientTransform="scale(1.244363,0.803624)"
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient234"
+ xlink:href="#linearGradient177"
+ inkscape:collect="always" />
+ <linearGradient
+ id="linearGradient513">
+ <stop
+ id="stop514"
+ offset="0.0000000"
+ style="stop-color:#696969;stop-opacity:1.0000000;" />
+ <stop
+ id="stop515"
+ offset="1.0000000"
+ style="stop-color:#ffffff;stop-opacity:1.0000000;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient508">
+ <stop
+ id="stop509"
+ offset="0.0000000"
+ style="stop-color:#b0b0b0;stop-opacity:1.0000000;" />
+ <stop
+ id="stop510"
+ offset="1.0000000"
+ style="stop-color:#b0b0b0;stop-opacity:1.0000000;" />
+ </linearGradient>
+ <linearGradient
+ y2="72.608902"
+ x2="192.3857"
+ y1="72.608902"
+ x1="78.245598"
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient477">
+ <stop
+ id="stop478"
+ style="stop-color:#e4e9ea;stop-opacity:1.0000000;"
+ offset="0.0000000" />
+ <stop
+ id="stop479"
+ style="stop-color:#85898A;fill-rule:nonzero;"
+ offset="1" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient454">
+ <stop
+ id="stop455"
+ offset="0.0000000"
+ style="stop-color:#ffffff;stop-opacity:1.0000000;" />
+ <stop
+ id="stop457"
+ offset="0.50000000"
+ style="stop-color:#bebebe;stop-opacity:1.0000000;" />
+ <stop
+ id="stop456"
+ offset="1.0000000"
+ style="stop-color:#b0b0b0;stop-opacity:1.0000000;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient177">
+ <stop
+ id="stop178"
+ offset="0.0000000"
+ style="stop-color:#ffffff;stop-opacity:1.0000000;" />
+ <stop
+ id="stop179"
+ offset="1.0000000"
+ style="stop-color:#b0b0b0;stop-opacity:1.0000000;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient172">
+ <stop
+ id="stop173"
+ offset="0.0000000"
+ style="stop-color:#616c08;stop-opacity:1.0000000;" />
+ <stop
+ id="stop174"
+ offset="1.0000000"
+ style="stop-color:#495106;stop-opacity:1.0000000;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient161">
+ <stop
+ id="stop162"
+ offset="0.0000000"
+ style="stop-color:#575955;stop-opacity:1.0000000;" />
+ <stop
+ id="stop163"
+ offset="1.0000000"
+ style="stop-color:#7c7e79;stop-opacity:1.0000000;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient157">
+ <stop
+ id="stop158"
+ offset="0.0000000"
+ style="stop-color:#babdb6;stop-opacity:1.0000000;" />
+ <stop
+ id="stop159"
+ offset="1.0000000"
+ style="stop-color:#f1f5ec;stop-opacity:1.0000000;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient149"
+ inkscape:collect="always">
+ <stop
+ id="stop150"
+ offset="0"
+ style="stop-color:#000000;stop-opacity:1;" />
+ <stop
+ id="stop151"
+ offset="1"
+ style="stop-color:#000000;stop-opacity:0;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient1869">
+ <stop
+ style="stop-color:#eff3f4;stop-opacity:1.0000000;"
+ offset="0.0000000"
+ id="stop1870" />
+ <stop
+ style="stop-color:#939596;stop-opacity:1.0000000;"
+ offset="1.0000000"
+ id="stop1871" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient1869"
+ id="linearGradient1872"
+ gradientTransform="matrix(1.464893,0,0,0.475906,30.56501,-34.34268)"
+ x1="-4.6375198"
+ y1="104.38752"
+ x2="-4.523921"
+ y2="110.61378"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ gradientUnits="userSpaceOnUse"
+ y2="69.460503"
+ x2="7.5291119"
+ y1="27.376621"
+ x1="7.3738608"
+ gradientTransform="matrix(3.495016,0,0,0.344323,-2.972087,-3.408148e-2)"
+ id="linearGradient152"
+ xlink:href="#linearGradient149"
+ inkscape:collect="always" />
+ <linearGradient
+ gradientUnits="userSpaceOnUse"
+ y2="6.8897982"
+ x2="60.685902"
+ y1="6.8897982"
+ x1="55.208271"
+ gradientTransform="matrix(0.772488,0,0,1.55784,-2.79531,-0.166664)"
+ id="linearGradient160"
+ xlink:href="#linearGradient157"
+ inkscape:collect="always" />
+ <radialGradient
+ r="5.6434927"
+ fy="20.45278"
+ fx="16.280994"
+ cy="20.45278"
+ cx="16.280994"
+ gradientTransform="matrix(2.202254,0,0,0.574568,-13.83631,0.652472)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient605"
+ xlink:href="#linearGradient477"
+ inkscape:collect="always" />
+ <linearGradient
+ y2="14.74888"
+ x2="18.086929"
+ y1="11.165159"
+ x1="14.248631"
+ gradientTransform="matrix(1.399756,0,0,0.903977,-3.99312,0.7519)"
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient606"
+ xlink:href="#linearGradient454"
+ inkscape:collect="always" />
+ <linearGradient
+ y2="122.61145"
+ x2="132.98843"
+ y1="116.66409"
+ x1="128.35213"
+ gradientTransform="scale(1.244363,0.803624)"
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient607"
+ xlink:href="#linearGradient177"
+ inkscape:collect="always" />
+ <linearGradient
+ y2="14.744809"
+ x2="20.135639"
+ y1="8.7251825"
+ x1="9.9626188"
+ gradientTransform="matrix(1.399756,0,0,0.903977,-3.99312,0.797381)"
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient608"
+ xlink:href="#linearGradient177"
+ inkscape:collect="always" />
+ <linearGradient
+ y2="10.115389"
+ x2="40.437176"
+ y1="2.3488793"
+ x1="24.162909"
+ gradientTransform="matrix(1.124876,0,0,1.124876,-3.99312,0.7519)"
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient609"
+ xlink:href="#linearGradient477"
+ inkscape:collect="always" />
+ <linearGradient
+ y2="24.077389"
+ x2="-36.301399"
+ y1="18.817307"
+ x1="-32.400455"
+ gradientTransform="matrix(-1.449414,0.286552,-0.469381,0.965804,-2.70059,0.453616)"
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient610"
+ xlink:href="#linearGradient513"
+ inkscape:collect="always" />
+ <linearGradient
+ y2="20.664473"
+ x2="45.130928"
+ y1="13.131673"
+ x1="36.155384"
+ gradientTransform="matrix(0.82649,0.763061,0.763061,-0.82649,-30.95946,-9.495656)"
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient611"
+ xlink:href="#linearGradient477"
+ inkscape:collect="always" />
+ <linearGradient
+ y2="9.3615303"
+ x2="13.763388"
+ y1="14.035932"
+ x1="16.551964"
+ gradientTransform="matrix(1.399756,0,0,0.903977,-2.86035,0.227876)"
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient612"
+ xlink:href="#linearGradient177"
+ inkscape:collect="always" />
+ <linearGradient
+ y2="14.744809"
+ x2="20.135639"
+ y1="8.7251825"
+ x1="9.9626188"
+ gradientTransform="matrix(1.027326,-0.134612,0.528454,0.53648,8.12216,4.8634)"
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient613"
+ xlink:href="#linearGradient508"
+ inkscape:collect="always" />
+ <linearGradient
+ y2="137.97153"
+ x2="136.9856"
+ y1="67.364906"
+ x1="81.307533"
+ gradientTransform="scale(1.244363,0.803624)"
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient632"
+ xlink:href="#linearGradient177"
+ inkscape:collect="always" />
+ <radialGradient
+ gradientUnits="userSpaceOnUse"
+ r="13.265761"
+ fy="8.9303417"
+ fx="39.14772"
+ cy="8.9303417"
+ cx="39.14772"
+ gradientTransform="matrix(0.878817,0,0,1.025708,1.24328,2.850095)"
+ id="radialGradient640"
+ xlink:href="#linearGradient641"
+ inkscape:collect="always" />
+ <linearGradient
+ y2="48.805084"
+ x2="25.31245"
+ y1="25.335417"
+ x1="17.573946"
+ gradientTransform="scale(1.175636,0.850604)"
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient374"
+ xlink:href="#linearGradient368"
+ inkscape:collect="always" />
+ <linearGradient
+ gradientUnits="userSpaceOnUse"
+ y2="23.600779"
+ x2="19.857769"
+ y1="38.962704"
+ x1="19.977491"
+ gradientTransform="scale(1.215669,0.822592)"
+ id="linearGradient384"
+ xlink:href="#linearGradient381"
+ inkscape:collect="always" />
+ </defs>
+ <sodipodi:namedview
+ showborder="true"
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="13.618797"
+ inkscape:cx="24"
+ inkscape:cy="24.613205"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ inkscape:grid-bbox="true"
+ inkscape:document-units="px"
+ inkscape:window-width="1440"
+ inkscape:window-height="852"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:showpageshadow="false" />
+ <metadata
+ id="metadata4">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title>Trash Full</dc:title>
+ <dc:date>2003-02-03</dc:date>
+ <dc:creator>
+ <cc:Agent>
+ <dc:title>Jakub Steiner</dc:title>
+ </cc:Agent>
+ </dc:creator>
+ <dc:subject>
+ <rdf:Bag>
+ <rdf:li>trash</rdf:li>
+ <rdf:li>delete</rdf:li>
+ <rdf:li>deleted files</rdf:li>
+ <rdf:li>waste</rdf:li>
+ <rdf:li>recycle</rdf:li>
+ <rdf:li>bin</rdf:li>
+ <rdf:li>full</rdf:li>
+ </rdf:Bag>
+ </dc:subject>
+ <dc:publisher>
+ <cc:Agent>
+ <dc:title>Novell, Inc.</dc:title>
+ </cc:Agent>
+ </dc:publisher>
+ <cc:license
+ rdf:resource="http://creativecommons.org/licenses/by-sa/2.0/" />
+ </cc:Work>
+ <cc:License
+ rdf:about="http://creativecommons.org/licenses/by-sa/2.0/">
+ <cc:permits
+ rdf:resource="http://web.resource.org/cc/Reproduction" />
+ <cc:permits
+ rdf:resource="http://web.resource.org/cc/Distribution" />
+ <cc:requires
+ rdf:resource="http://web.resource.org/cc/Notice" />
+ <cc:requires
+ rdf:resource="http://web.resource.org/cc/Attribution" />
+ <cc:permits
+ rdf:resource="http://web.resource.org/cc/DerivativeWorks" />
+ <cc:requires
+ rdf:resource="http://web.resource.org/cc/ShareAlike" />
+ </cc:License>
+ </rdf:RDF>
+ </metadata>
+ <g
+ id="layer1"
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ transform="translate(-1.9364206,25.97742)">
+ <path
+ style="fill:#aeaeae;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="M 25.573309,-11.367274 L 39.023552,-10.341066 L 44.224653,-23.003395 L 38.070664,-18.678228 C 38.070664,-18.678228 35.431941,-24.83272 33.535358,-25.327483 C 31.638778,-25.822243 23.547033,-25.657322 23.547033,-25.657322 C 26.120772,-25.020805 28.121036,-20.5984 28.151784,-20.548234 C 28.954727,-19.238075 31.202357,-14.829528 31.202357,-14.829528 L 25.573309,-11.367274 z"
+ id="path41"
+ sodipodi:nodetypes="ccccccscc" />
+ <path
+ style="fill:#d5d5d5;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
+ d="M 27.237312,-20.126383 L 22.785035,-11.632996 L 13.631964,-17.405201 C 13.631964,-17.405201 17.782526,-25.238907 21.053373,-25.238907 C 24.35189,-25.238907 25.82591,-22.395699 27.237312,-20.126383 z"
+ id="path130"
+ sodipodi:nodetypes="ccczc" />
+ <path
+ style="fill:#aeaeae;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="M 33.790949,0.039420776 L 25.119184,10.403397 L 33.39456,21.089879 L 33.547618,14.358991 C 33.547618,14.358991 39.643158,14.934236 41.042169,13.561435 C 42.441173,12.188634 48.261285,5.2669617 48.261285,5.2669617 C 46.392707,7.1478383 41.570981,6.5913532 41.512143,6.5919554 C 39.975593,6.6075524 33.894123,6.4656353 33.894123,6.4656353 L 33.790949,0.039420776 z"
+ id="path139"
+ sodipodi:nodetypes="ccccccscc" />
+ <path
+ style="fill:#d5d5d5;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
+ d="M 41.929254,5.4089567 L 36.930779,-2.7749032 L 46.58596,-7.6610575 C 46.58596,-7.6610575 51.173548,-0.075011208 49.49281,2.7309777 C 47.797861,5.5607038 44.601303,5.3642417 41.929254,5.4089567 z"
+ id="path140"
+ sodipodi:nodetypes="ccczc" />
+ <path
+ style="fill:#aeaeae;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="M 18.626929,-0.56239567 L 14.346013,-11.836691 L 1.8629906,-10.954505 L 6.9874103,-7.2215585 C 6.9874103,-7.2215585 3.4484744,-2.2253151 3.9405135,-0.32802336 C 4.4325526,1.5692649 7.2084811,9.429705 7.2084811,9.429705 C 6.5102915,6.8720063 9.3976127,2.9704545 9.4264403,2.9191578 C 10.179327,1.5796085 13.335668,-3.6205749 13.335668,-3.6205749 L 18.626929,-0.56239567 z"
+ id="path142"
+ sodipodi:nodetypes="ccccccscc" />
+ <path
+ style="fill:#d5d5d5;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
+ d="M 10.063585,3.991281 L 19.649913,3.740954 L 19.069021,14.546491 C 19.069021,14.546491 10.205778,14.738872 8.6120759,11.882554 C 7.0048953,9.0020693 8.7695652,6.3295242 10.063585,3.991281 z"
+ id="path143"
+ sodipodi:nodetypes="ccczc" />
+ </g>
+ <g
+ inkscape:label="trash"
+ id="layer2"
+ inkscape:groupmode="layer"
+ transform="translate(-1.9364206,25.97742)">
+ <g
+ transform="matrix(0.273209,0,0,0.273209,6.153125,9.65744)"
+ id="g620"
+ style="font-size:12px;fill:url(#linearGradient632);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2.13333321;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
+ i:knockout="Off" />
+ </g>
+</svg>
BIN  artwork/tick.psd
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
73 setup.py
@@ -0,0 +1,73 @@
+# -*- coding: utf-8 -*-
+"""
+Description missing
+"""
+
+# we require setuptools because of dependencies and testing.
+# we may provide a distutils fallback later.
+from setuptools import setup
+
+extra = {}
+try:
+ import babel
+except ImportError:
+ pass
+else:
+ extra['message_extractors'] = {
+ 'solace': [
+ ('**.py', 'python', None),
+ ('**/templates/**', 'jinja2', None),
+ ('**.js', 'javascript', None)
+ ]
+ }
+
+try:
+ from solace import scripts
+except ImportError:
+ pass
+else:
+ extra['cmdclass'] = {
+ 'runserver': scripts.RunserverCommand,
+ 'initdb': scripts.InitDatabaseCommand,
+ 'reset': scripts.ResetDatabase,
+ 'make_testdata': scripts.MakeTestData,
+ 'compile_catalog': scripts.CompileCatalogEx
+ }
+
+try:
+ import webdepcompress
+except ImportError:
+ pass
+else:
+ extra['webdepcompress_manager'] = 'solace.packs.pack_mgr'
+
+setup(
+ name='Solace',
+ version='0.1',
+ url='http://opensource.plurk.com/solace/',
+ license='BSD',
+ author='Plurk Inc.',
+ author_email='opensource@plurk.com',
+ description='Multilangual User Support Platform',
+ long_description=__doc__,
+ packages=['solace', 'solace.views', 'solace.i18n', 'solace.utils'],
+ package_data={
+ 'solace.i18n': ['*'],
+ 'solace': ['templates/*', 'static/*']
+ },
+ platforms='any',
+ test_suite='solace.tests.suite',
+ install_requires=[
+ 'Werkzeug>=0.5.1',
+ 'Jinja2',
+ 'Babel',
+ 'SQLAlchemy>=0.5',
+ 'creoleparser',
+ 'simplejson',
+ 'webdepcompress'
+ ],
+ tests_require=[
+ 'lxml',
+ 'html5lib'
+ ], **extra
+)
14 solace/__init__.py
@@ -0,0 +1,14 @@
+# -*- coding: utf-8 -*-
+"""
+ solace
+ ~~~~~~
+
+ Solace is a community driven support vehicle for Plurk that might also be
+ useful for others. It's heavily inspired by support forums such as PHPBB
+ and the stackoverflow system.
+
+ :copyright: (c) 2009 by Plurk Inc., see AUTHORS for more details.
+ :license: BSD, see LICENSE for more details.
+"""
+# note on imports: This __init__ file must not import anything in order fo
+# the solace.scripts module not importing anything. More information there.
413 solace/application.py
@@ -0,0 +1,413 @@
+# -*- coding: utf-8 -*-
+"""
+ solace.application
+ ~~~~~~~~~~~~~~~~~~
+
+ The WSGI application for Solace.
+
+ :copyright: (c) 2009 by Plurk Inc., see AUTHORS for more details.
+ :license: BSD, see LICENSE for more details.
+"""
+import os
+from urlparse import urlparse, urlsplit, urljoin
+from fnmatch import fnmatch
+from functools import update_wrapper
+from simplejson import dumps
+
+from babel import UnknownLocaleError, Locale
+from werkzeug import Request as RequestBase, Response, cached_property, \
+ import_string, redirect, SharedDataMiddleware, url_quote
+from werkzeug.exceptions import HTTPException, NotFound, Forbidden
+from werkzeug.routing import BuildError, RequestRedirect
+from werkzeug.contrib.securecookie import SecureCookie
+
+from solace.utils.ctxlocal import local, local_mgr, LocalProperty
+
+
+class Request(RequestBase):
+ """The request class."""
+
+ in_api = False
+ _locale = None
+ _pulled_flash_messages = None
+
+ def __init__(self, environ):
+ RequestBase.__init__(self, environ)
+ self.url_adapter = url_map.bind_to_environ(self.environ)
+ self.view_lang = self.match_exception = None
+ try:
+ self.endpoint, self.view_arguments = self.url_adapter.match()
+ view_lang = self.view_arguments.pop('lang_code', None)
+ if view_lang is not None:
+ try:
+ self.view_lang = Locale.parse(view_lang)
+ if not has_section(self.view_lang):
+ raise UnknownLocaleError(str(self.view_lang))
+ except UnknownLocaleError:
+ self.view_lang = None
+ self.match_exception = NotFound()
+ except HTTPException, e:
+ self.endpoint = self.view_arguments = None
+ self.match_exception = e
+ self.sql_queries = []
+ local.request = self
+
+ current = LocalProperty('request')
+
+ def dispatch(self):
+ """Where do we want to go today?"""
+ try:
+ if self.match_exception is not None:
+ raise self.match_exception
+ rv = self.view(self, **self.view_arguments)
+ except NotFound, e:
+ rv = get_view('core.not_found')(self)
+ if isinstance(rv, basestring):
+ rv = Response(rv, mimetype='text/html')
+ return rv
+
+ def _get_locale(self):
+ """The locale of the incoming request. If a locale is unsupported, the
+ default english locale is used. If the locale is assigned it will be
+ stored in the session so that that language changes are persistent.
+ """
+ if self._locale is not None:
+ return self._locale
+ rv = self.session.get('locale')
+ if rv is not None:
+ rv = Locale.parse(rv)
+ # we could trust the cookie here because it's signed, but we do not
+ # because the configuration could have changed in the meantime.
+ if not has_section(rv):
+ rv = None
+ if rv is None:
+ rv = select_locale(self.accept_languages)
+ self._locale = rv
+ return rv
+ def _set_locale(self, locale):
+ self._locale = Locale.parse(locale)
+ self.__dict__.pop('translations', None)
+ self.session['locale'] = str(self._locale)
+ locale = property(_get_locale, _set_locale)
+ del _get_locale, _set_locale
+
+ @cached_property
+ def translations(self):
+ """The translations for this request."""
+ return load_translations(self.locale)
+
+ @property
+ def timezone_known(self):
+ """If the JavaScript on the client set the timezone already this returns
+ True, otherwise False.
+ """
+ return self.session.get('timezone') is not None
+
+ @cached_property
+ def tzinfo(self):
+ """The timezone information."""
+ offset = self.session.get('timezone')
+ if offset is not None:
+ return Timezone(offset)
+
+ @cached_property
+ def next_url(self):
+ """Sometimes we want to redirect to different URLs back or forth.
+ For example the login function uses this attribute to find out
+ where it should go.
+
+ If there is a `next` parameter on the URL or in the form data, the
+ function will redirect there, if it's not there, it checks the
+ referrer.
+
+ It's usually better to use the get_redirect_target method.
+ """
+ return self.get_redirect_target()
+
+ def get_localized_next_url(self, locale=None):
+ """Like `next_url` but tries to go to the localized section."""
+ if locale is None:
+ locale = self.locale
+ next_url = self.get_redirect_target()
+ if next_url is None:
+ return
+ scheme, netloc, path, query = urlsplit(next_url)[:4]
+ path = path.decode('utf-8')
+
+ # aha. we're redirecting somewhere out of our control
+ if netloc != self.host or not path.startswith(self.script_root):
+ return next_url
+
+ path = path[len(self.script_root):]
+ try:
+ endpoint, values = self.url_adapter.match(path)
+ except NotFound, e:
+ return next_url
+ except RequestRedirect:
+ pass
+ if 'lang_code' not in values:
+ return next_url
+
+ values['lang_code'] = str(locale)
+ return self.url_adapter.build(endpoint, values) + \
+ (query and '?' + query or '')
+
+ def get_redirect_target(self, invalid_targets=()):
+ """Check the request and get the redirect target if possible.
+ If not this function returns just `None`. The return value of this
+ function is suitable to be passed to `redirect`
+ """
+ check_target = self.values.get('_redirect_target') or \
+ self.values.get('next') or \
+ self.referrer
+
+ # if there is no information in either the form data
+ # or the wsgi environment about a jump target we have
+ # to use the target url
+ if not check_target:
+ return
+
+ # otherwise drop the leading slash
+ check_target = check_target.lstrip('/')
+
+ root_url = self.url_root
+ root_parts = urlparse(root_url)
+ check_parts = urlparse(urljoin(root_url, check_target))
+
+ # if the jump target is on a different server we probably have
+ # a security problem and better try to use the target url.
+ # except the host is whitelisted in the config
+ if root_parts[:2] != check_parts[:2]:
+ host = check_parts[1].split(':', 1)[0]
+ for rule in settings.ALLOWED_REDIRECTS:
+ if fnmatch(host, rule):
+ break
+ else:
+ return
+
+ # if the jump url is the same url as the current url we've had
+ # a bad redirect before and use the target url to not create a
+ # infinite redirect.
+ current_parts = urlparse(urljoin(root_url, self.path))
+ if check_parts[:5] == current_parts[:5]:
+ return
+
+ # if the `check_target` is one of the invalid targets we also
+ # fall back.
+ for invalid in invalid_targets:
+ if check_parts[:5] == urlparse(urljoin(root_url, invalid))[:5]:
+ return
+
+ return check_target
+
+ @cached_property
+ def user(self):
+ """The current user."""
+ return get_auth_system().get_user(self)
+
+ @property
+ def is_logged_in(self):
+ """Is the user logged in?"""
+ return self.user is not None
+
+ @cached_property
+ def view(self):
+ """The view function."""
+ return get_view(self.endpoint)
+
+ @cached_property
+ def session(self):
+ """The active session."""
+ return SecureCookie.load_cookie(self, settings.COOKIE_NAME,
+ settings.SECRET_KEY)
+
+ def list_languages(self):
+ """Lists all languages."""
+ return [dict(
+ name=locale.display_name,
+ key=key,
+ selected=self.locale == locale,
+ select_url=url_for('core.set_language', locale=key),
+ section_url=url_for('kb.overview', lang_code=key)
+ ) for key, locale in list_languages()]
+
+ def flash(self, message):
+ """Flashes a message."""
+ self.session.setdefault('flashes', []).append(message)
+
+ def pull_flash_messages(self):
+ """Returns all flash messages. They will be removed from the
+ session at the same time. This also pulls the messages from
+ the database that are queued for the user.
+ """
+ msgs = self._pulled_flash_messages or []
+ if self.user is not None:
+ to_delete = set()
+ for msg in UserMessage.query.filter_by(user=self.user).all():
+ msgs.append(msg.text)
+ to_delete.add(msg.id)
+ if to_delete:
+ UserMessage.query.filter(UserMessage.id.in_(to_delete)).delete()
+ session.commit()
+ if 'flashes' in self.session:
+ msgs += self.session.pop('flashes')
+ self._pulled_flash_messages = msgs
+ return msgs
+
+ def save_session(self, response):
+ """Save the session to the response if changed."""
+ if not self.in_api and self.session.should_save:
+ self.session.save_cookie(response, settings.COOKIE_NAME)
+
+
+def get_view(endpoint):
+ """Returns the view for the endpoint."""
+ try:
+ return import_string('solace.views.' + endpoint)
+ except (ImportError, AttributeError):
+ try:
+ return import_string(endpoint)
+ except (ImportError, AttributeError):
+ raise RuntimeError('could not locate view for %r' % endpoint)
+
+
+def json_response(message=None, html=None, error=False, login_could_fix=False,
+ **extra):
+ """Returns a JSON response for the JavaScript code. The "wire protocoll"
+ is basically just a JSON object with some common attributes that are
+ checked by the success callback in the JavaScript code before the handler
+ processes it.
+
+ The `error` and `login_could_fix` keys are internally used by the flashing
+ system on the client.
+ """
+ extra.update(message=message, html=html, error=error,
+ login_could_fix=login_could_fix)
+ for key, value in extra.iteritems():
+ extra[key] = remote_export_primitive(value)
+ return Response(dumps(extra), mimetype='application/json')
+
+
+def not_logged_in_json_response():
+ """Standard response that the user is not logged in."""
+ return json_response(message=_(u'You have to login in order to '
+ u'visit this page.'),
+ error=True, login_could_fix=True)
+
+
+def require_login(f):
+ """Decorates a view function so that it requires a user that is
+ logged in.
+ """
+ def decorated(request, **kwargs):
+ if not request.is_logged_in:
+ if request.is_xhr:
+ return not_logged_in_json_response()
+ request.flash(_(u'You have to login in order to visit this page.'))
+ return redirect(url_for('core.login', next=request.url))
+ return f(request, **kwargs)
+ return update_wrapper(decorated, f)
+
+
+def iter_endpoint_choices(new, current=None):
+ """Iterate over all possibilities for URL generation."""
+ yield new
+ if current is not None and '.' in current:
+ yield current.rsplit('.', 1)[0] + '.' + new
+
+
+def inject_lang_code(request, endpoint, values):
+ """Returns a dict with the values for the given endpoint. You must not alter
+ the dict because it might be shared. If the given endpoint does not exist
+ `None` is returned.
+ """
+ rv = values
+ if 'lang_code' not in rv:
+ try:
+ if request.url_adapter.map.is_endpoint_expecting(
+ endpoint, 'lang_code'):
+ rv = values.copy()
+ rv['lang_code'] = request.view_lang or str(request.locale)
+ except KeyError:
+ return
+ return rv
+
+
+def url_for(endpoint, **values):
+ """Returns a URL for a given endpoint with some interpolation."""
+ external = values.pop('_external', False)
+ if hasattr(endpoint, 'get_url_values'):
+ endpoint, values = endpoint.get_url_values(**values)
+ request = Request.current
+ anchor = values.pop('_anchor', None)
+ assert request is not None, 'no active request'
+ for endpoint_choice in iter_endpoint_choices(endpoint, request.endpoint):
+ real_values = inject_lang_code(request, endpoint_choice, values)
+ if real_values is None:
+ continue
+ try:
+ url = request.url_adapter.build(endpoint_choice, real_values,
+ force_external=external)
+ except BuildError:
+ continue
+ if anchor is not None:
+ url += '#' + url_quote(anchor)
+ return url
+ raise BuildError(endpoint, values, 'GET')
+
+
+def add_query_debug_headers(request, response):
+ """Add headers with the SQL info."""
+ count = len(request.sql_queries)
+ sql_time = 0.0
+ for stmt, param, start, end in request.sql_queries:
+ sql_time += (end - start)
+ response.headers['X-SQL-Query-Count'] = str(count)
+ response.headers['X-SQL-Query-Time'] = str(sql_time)
+
+
+def finalize_response(request, response):
+ """Finalizes the response. Applies common response processors."""
+ request.save_session(response)
+ if settings.TRACK_QUERIES:
+ add_query_debug_headers(request, response)
+ if response.status == 200:
+ response.add_etag()
+ response = response.make_conditional(request)
+ return response
+
+
+@Request.application
+def application(request):
+ """The WSGI application."""
+ try:
+ try:
+ response = request.dispatch()
+ except HTTPException, e:
+ response = e.get_response(request.environ)
+ if not isinstance(response, Response):
+ response = Response.force_type(response, request.environ)
+ return finalize_response(request, response)
+ finally:
+ # at the end of the request we get rid of the open database
+ # session. If it was comitted before that's fine, otherwise
+ # we make sure there is no pending transaction left open.
+ session.remove()
+ # also get rid of all the locals we still have
+ local_mgr.cleanup()
+
+
+application = SharedDataMiddleware(application, {
+ '/_static': os.path.join(os.path.dirname(__file__), 'static')
+})
+
+
+# imported here because of possible circular dependencies
+from solace import settings
+from solace.urls import url_map
+from solace.i18n import select_locale, load_translations, Timezone, _, \
+ list_languages, has_section
+from solace.auth import get_auth_system
+from solace.database import session
+from solace.models import UserMessage
+from solace.utils.remoting import remote_export_primitive
231 solace/auth.py
@@ -0,0 +1,231 @@
+# -*- coding: utf-8 -*-
+"""
+ solace.auth
+ ~~~~~~~~~~~
+
+ This module implements the auth system.
+
+ :copyright: (c) 2009 by Plurk Inc., see AUTHORS for more details.
+ :license: BSD, see LICENSE for more details.
+"""
+from __future__ import with_statement
+from threading import Lock
+from werkzeug import import_string, redirect
+from werkzeug.contrib.securecookie import SecureCookie
+from datetime import datetime
+
+from solace import settings
+from solace.application import url_for
+from solace.templating import render_template
+from solace.utils.support import UIException
+from solace.utils.mail import send_email
+
+
+_auth_system = None
+_auth_select_lock = Lock()
+
+
+def get_auth_system():
+ """Return the auth system."""
+ global _auth_system
+ with _auth_select_lock:
+ if _auth_system is None:
+ _auth_system = import_string(settings.AUTH_SYSTEM)()
+ return _auth_system
+
+
+def refresh_auth_system():
+ """Tears down the auth system after a config change."""
+ global _auth_system
+ with _auth_system_lock:
+ _auth_system = None
+
+
+class LoginUnsucessful(UIException):
+ """Raised if the login failed."""
+
+
+class AuthSystemBase(object):
+ """The base auth system.
+
+ Most functionality is described in the methods and properties you have
+ to override for subclasses. A special notice applies for user
+ registration.
+
+ Different auth systems may create users at different stages (first login,
+ register etc.). At that point (where the user is created in the
+ database) the system has to call `after_register` and pass it the user
+ (and request) object. That method handles the confirmation mails and
+ whatever else is required. If you do not want your auth system to send
+ confirmation mails you still have to call the method but tell the user
+ of your class to disable registration activation in the configuration.
+
+ `after_register` should *not* be called if the registration process
+ should happen transparently for the user. eg, the user has already
+ registered somewhere else and the Solace account is created based on the
+ already existing account on first login.
+ """
+
+ #: for auth systems that are managing the email externally this
+ #: attributes has to set to `True`. In that case the user will
+ #: be unable to change the email from the profile. (True for
+ #: the plurk auth, possible OpenID support and more.)
+ email_managed_external = False
+
+ #: like `email_managed_external` but for the password
+ password_managed_external = False
+
+ #: set to True if the form should not have a password entry.
+ passwordless = False
+
+ @property
+ def can_reset_password(self):
+ """You can either override this property or leave the default
+ implementation that should work most of the time. By default
+ the auth system can reset the password if the password is not
+ externally managed and not passwordless.
+ """
+ return not (self.passwordless or self.password_managed_external)
+
+ def reset_password(self, request, user):
+ if settings.REGISTRATION_REQUIRES_ACTIVATION:
+ user.is_active = False
+ confirmation_url = url_for('core.activate_user', email=user.email,
+ key=user.activation_key, _external=True)
+ send_email(_(u'Registration Confirmation'),
+ render_template('mails/activate_user.txt', user=user,
+ confirmation_url=confirmation_url),
+ user.email)
+ request.flash(_(u'A mail was sent to %s with a link to finish the '
+ u'registration.') % user.email)
+ else:
+ request.flash(_(u'You\'re registered. You can login now.'))
+
+ def before_register(self, request):
+ """Invoked before teh standard register form processing. This is
+ intended to be used to redirect to an external register URL if
+ if the syncronization is only one-directional. If this function
+ returns a response object, Solace will abort standard registration
+ handling.
+ """
+
+ def register(self, request, username, password, email):
+ """Called on registration. Auth systems that only use the internal
+ database do not have to override this method.
+
+ Passwordless systems have to live with `before_register` because we
+ do not provide a standard way to sign up passwordless.
+
+ This method may return a response which is returned *after* the
+ database transaction is comitted but *before* a success message
+ is flashed.
+
+ Have a look at the classes docstring about user registration.
+ """
+ self.after_register(request, User(username, email, password))
+
+ def after_register(self, request, user):
+ """Handles activation."""
+ if settings.REGISTRATION_REQUIRES_ACTIVATION:
+ user.is_active = False
+ confirmation_url = url_for('core.activate_user', email=user.email,
+ key=user.activation_key, _external=True)
+ send_email(_(u'Registration Confirmation'),
+ render_template('mails/activate_user.txt', user=user,
+ confirmation_url=confirmation_url),
+ user.email)
+ request.flash(_(u'A mail was sent to %s with a link to finish the '
+ u'registration.') % user.email)
+ else:
+ request.flash(_(u'You\'re registered. You can login now.'))
+
+ def before_login(self, request):
+ """If this login system uses an external login URL, this function
+ has to return a redirect response, otherwise None. This is called
+ before the standard form handling to allow redirecting to an
+ external login URL.
+ """
+
+ def login(self, request, username, password):
+ """Has to perform the login. If the login was successful with
+ the credentials provided the function has to somehow make sure
+ that the user is remembered. Internal auth systems may use the
+ `set_user` method. If logging is is not successful the system
+ has to raise an `LoginUnsucessful` exception. If the `set_user`
+ method is not used, the auth system has to set the `last_login`
+ attribute of the user.
+
+ If the auth system needs the help of an external resource for
+ login it may return a response object with a redirect code
+ instead. The user is then redirected to that page to complete
+ the login. This page then has to ensure that the user is
+ redirected back to the login page to trigger this function
+ again. The back-redirect may attach extra argument to the URL
+ which the function might want to used to find out if the login
+ was successful.
+
+ If the `activation_key` column and/or `is_active` property of the
+ user object are in use for this authentication system, the register
+ function has to ensure that it's checked before logging in. If the
+ user is not active, a `LoginUnsucessful` error should be raised.
+
+ For passwordless logins the password will be `None`.
+ """
+ raise NotImplementedError()
+
+ def logout(self, request):
+ """This has to logout the user again. This method must not fail.
+ If the logout requires the redirect to an external resource it
+ might return a redirect response. That resource then should not
+ redirect back to the logout page, but instead directly to the
+ **current** `request.next_url`.
+
+ Most auth systems do not have to implement this method. The
+ default one calls `set_user(request, None)`.
+ """
+ self.set_user(request, None)
+
+ def get_user(self, request):
+ """If the user is logged in this method has to return the user
+ object for the user that is logged in. Beware: the request
+ class provides some attributes such as `user` and `is_logged_in`
+ you may never use from this function to avoid recursion. The
+ request object will call this function for those two attributes.
+
+ If the user is not logged in, the return value has to be `None`.
+
+ Most auth systems do not have to implement this method.
+ """
+ user_id = request.session.get('user_id')
+ if user_id is not None:
+ return User.query.get(user_id)
+
+ def set_user(self, request, user):
+ """Can be used by the login function to set the user. This function
+ should only be used for auth systems internally if they are not using
+ an external session.
+ """
+ if user is None:
+ request.session.pop('user_id', None)
+ else:
+ user.last_login = datetime.utcnow()
+ request.session['user_id'] = user.id
+
+
+class InternalAuth(AuthSystemBase):
+ """Authenticate against the internal database."""
+
+ def login(self, request, username, password):
+ user = User.query.filter_by(username=username).first()
+ if user is None:
+ raise LoginUnsucessful(_(u'No user named %s') % username)
+ if not user.is_active:
+ raise LoginUnsucessful(_(u'The user is not yet activated.'))
+ if not user.check_password(password):
+ raise LoginUnsucessful(_(u'Invalid password'))
+ self.set_user(request, user)
+
+
+# circular dependencies
+from solace.models import User
+from solace.i18n import _
223 solace/badges.py
@@ -0,0 +1,223 @@
+# -*- coding: utf-8 -*-
+"""
+ solace.badges
+ ~~~~~~~~~~~~~
+
+ This module implements the badge system.
+
+ :copyright: (c) 2009 by Plurk Inc., see AUTHORS for more details.
+ :license: BSD, see LICENSE for more details.
+"""
+from operator import attrgetter
+
+from solace.i18n import lazy_gettext, _
+from solace.utils.remoting import RemoteObject
+
+
+def try_award(event, *args):
+ """Tries to avard a badge for the given event. The events correspond
+ to the `on_X` callbacks on the badges, just without the `on_` prefix.
+ """
+ from solace.application import Request
+ request = Request.current
+ awarded = False
+
+ lookup = attrgetter('on_' + event)
+ for badge in badge_list:
+ cb = lookup(badge)
+ if cb is None:
+ continue
+ user = cb(*args)
+ if user is not None:
+ if isinstance(user, tuple):
+ user, payload = user
+ else:
+ payload = None
+ if badge.single_awarded and badge in user.badges:
+ continue
+ user._badges.append(UserBadge(badge, payload))
+ # inactive or banned users don't get messages.
+ if user.is_active and not user.is_banned:
+ UserMessage(user, _(u'You earned the “%s” badge') % badge.name)
+ awarded = True
+ return awarded
+
+
+_numeric_levels = dict(zip(('bronce', 'silver', 'gold', 'platin'),
+ range(4)))
+
+
+class Badge(RemoteObject):
+ """Represents a badge.
+
+ It can react to the following events::
+
+ on_vote = lambda user, post, delta
+ on_accept = lambda user, post, answer
+ on_reply = lambda user, post
+ on_new_topic = lambda user, topic
+ on_edit = lambda user, post
+ """
+
+ remote_object_type = 'solace.badge'
+ public_fields = ('level', 'identifier', 'name', 'description')
+
+ def __init__(self, level, identifier, name, description=None,
+ single_awarded=False,
+ on_vote=None, on_accept=None, on_reply=None,
+ on_new_topic=None, on_edit=None):
+ assert level in ('bronce', 'silver', 'gold', 'platin')
+ assert len(identifier) <= 30
+ self.level = level
+ self.identifier = identifier
+ self.name = name
+ self.single_awarded = single_awarded
+ self.description = description
+ self.on_vote = on_vote
+ self.on_accept = on_accept
+ self.on_reply = on_reply
+ self.on_new_topic = on_new_topic
+ self.on_edit = on_edit
+
+ @property
+ def numeric_level(self):
+ return _numeric_levels[self.level]
+
+ def get_url_values(self):
+ return 'badges.show_badge', {'identifier': self.identifier}
+
+ def __repr__(self):
+ return '<%s \'%s\' (%s)>' % (
+ type(self).__name__,
+ self.name.encode('utf-8'),
+ ('bronce', 'silver', 'gold', 'platin')[self.numeric_level]
+ )
+
+
+def _try_award_special_answer(post, badge, votes_required):
+ """Helper for nice and good answer."""
+ pid = str(post.id)
+ user = post.author
+ for user_badge in user._badges:
+ if user_badge.badge == badge and \
+ user_badge.payload == pid:
+ return
+ if post.is_answer and post.votes >= votes_required:
+ return user, pid
+
+
+def _try_award_self_learner(post):
+ """Helper for the self learner badge."""
+ pid = str(post.id)
+ user = post.author
+ for user_badge in user._badges:
+ if user_badge.badge == SELF_LEARNER and \
+ user_badge.payload == pid:
+ return
+ if post.is_answer and post.author == post.topic.author \
+ and post.votes >= 3:
+ return user, pid
+
+
+def _try_award_reversal(post):
+ """Helper for the reversal badge."""
+ pid = str(post.id)
+ user = post.author
+ for user_badge in user._badges:
+ if user_badge.badge == REVERSAL and \
+ user_badge.payload == pid:
+ return
+ if post.is_answer and post.votes >= 20 and \
+ post.topic.votes <= -5:
+ return user, pid
+
+
+CRITIC = Badge('bronce', 'critic', lazy_gettext(u'Critic'),
+ lazy_gettext(u'First down vote'),
+ single_awarded=True,
+ on_vote=lambda user, post, delta:
+ user if delta < 0 and user != post.author else None
+)
+
+SELF_CRITIC = Badge('silver', 'self-critic', lazy_gettext(u'Self-Critic'),
+ lazy_gettext(u'First downvote on own reply or question'),
+ single_awarded=True,
+ on_vote=lambda user, post, delta:
+ user if delta < 0 and user == post.author else None
+)
+
+EDITOR = Badge('bronce', 'editor', lazy_gettext(u'Editor'),
+ lazy_gettext(u'First edited post'),
+ single_awarded=True,
+ on_edit=lambda user, post: user
+)
+
+INQUIRER = Badge('bronce', 'inquirer', lazy_gettext(u'Inquirer'),
+ lazy_gettext(u'First asked question'),
+ single_awarded=True,
+ on_new_topic=lambda user, topic: user
+)
+
+TROUBLESHOOTER = Badge('silver', 'troubleshooter',
+ lazy_gettext(u'Troubleshooter'),
+ lazy_gettext(u'First answered question'),
+ single_awarded=True,
+ on_accept=lambda user, topic, post: post.author if post else None
+)
+
+NICE_ANSWER = Badge('bronce', 'nice-answer', lazy_gettext(u'Nice Answer'),
+ lazy_gettext(u'Answer was upvoted 10 times'),
+ on_accept=lambda user, topic, post: _try_award_special_answer(post,
+ NICE_ANSWER, 10) if post else None,
+ on_vote=lambda user, post, delta: _try_award_special_answer(post,
+ NICE_ANSWER, 10)
+)
+
+GOOD_ANSWER = Badge('silver', 'good-answer', lazy_gettext(u'Good Answer'),
+ lazy_gettext(u'Answer was upvoted 25 times'),
+ on_accept=lambda user, topic, post: _try_award_special_answer(post,
+ GOOD_ANSWER, 25) if post else None,
+ on_vote=lambda user, post, delta: _try_award_special_answer(post,
+ GOOD_ANSWER, 25)
+)
+
+GREAT_ANSWER = Badge('gold', 'great-answer', lazy_gettext(u'Great Answer'),
+ lazy_gettext(u'Answer was upvoted 75 times'),
+ on_accept=lambda user, topic, post: _try_award_special_answer(post,
+ GOOD_ANSWER, 75) if post else None,
+ on_vote=lambda user, post, delta: _try_award_special_answer(post,
+ GOOD_ANSWER, 75)
+)
+
+UNIQUE_ANSWER = Badge('platin', 'unique-answer', lazy_gettext(u'Unique Answer'),
+ lazy_gettext(u'Answer was upvoted 150 times'),
+ on_accept=lambda user, topic, post: _try_award_special_answer(post,
+ GOOD_ANSWER, 150) if post else None,
+ on_vote=lambda user, post, delta: _try_award_special_answer(post,
+ GOOD_ANSWER, 150)
+)
+
+REVERSAL = Badge('gold', 'reversal', lazy_gettext(u'Reversal'),
+ lazy_gettext(u'Provided answer of +20 score to a question of -5 score'),
+ on_accept=lambda user, topic, post: _try_award_reversal(post) if post else None,
+ on_vote=lambda user, post, delta: _try_award_reversal(post)
+)
+
+SELF_LEARNER = Badge('silver', 'self-learner', lazy_gettext(u'Self-Learner'),
+ lazy_gettext(u'Answered your own question with at least 4 upvotes'),
+ on_accept=lambda user, topic, post: _try_award_self_learner(post) if post else None,
+ on_vote=lambda user, post, delta: _try_award_self_learner(post)
+)
+
+
+#: list of all badges
+badge_list = [CRITIC, EDITOR, INQUIRER, TROUBLESHOOTER, NICE_ANSWER,
+ GOOD_ANSWER, SELF_LEARNER, SELF_CRITIC, GREAT_ANSWER,
+ UNIQUE_ANSWER, REVERSAL]
+
+#: all the badges by key
+badges_by_id = dict((x.identifier, x) for x in badge_list)
+
+
+# circular dependencies
+from solace.models import UserBadge, UserMessage
371 solace/database.py
@@ -0,0 +1,371 @@
+# -*- coding: utf-8 -*-
+"""
+ solace.database
+ ~~~~~~~~~~~~~~~
+
+ This module defines the solace database. The structure is pretty simple
+ and should scale up to the number of posts we expect. Not much magic
+ happening here.
+
+ :copyright: (c) 2009 by Plurk Inc., see AUTHORS for more details.
+ :license: BSD, see LICENSE for more details.
+"""
+from __future__ import with_statement
+import sys
+import time
+from threading import Lock
+from datetime import datetime
+from babel import Locale
+from sqlalchemy.types import TypeDecorator
+from sqlalchemy.interfaces import ConnectionProxy
+from sqlalchemy import MetaData, Table, Column, Integer, String, Text, \
+ DateTime, ForeignKey, Boolean, Float, orm, sql, create_engine
+
+
+_engine = None
+_engine_lock = Lock()
+
+
+# the best timer for the platform. on windows systems we're using clock
+# for timing which has a higher resolution.
+if sys.platform == 'win32':
+ _timer = time.clock
+else:
+ _timer = time.time
+
+
+def get_engine():
+ """Creates or returns the engine."""
+ global _engine
+ with _engine_lock:
+ if _engine is None:
+ options = {'echo': settings.DATABASE_ECHO}
+ if settings.TRACK_QUERIES:
+ options['proxy'] = ConnectionQueryTrackingProxy()
+ _engine = create_engine(settings.DATABASE_URI, **options)
+ return _engine
+
+
+def refresh_engine():
+ """Gets rid of the existing engine. Useful for unittesting, use with care.
+ Do not call this function if there are multiple threads accessing the
+ engine. Only do that in single-threaded test environments or console
+ sessions.
+ """
+ global _engine
+ with _engine_lock:
+ session.remove()
+ if _engine is not None:
+ _engine.dispose()
+ _engine = None
+
+
+def atomic_add(obj, column, delta, expire=False):
+ """Performs an atomic add (or subtract) of the given column on the
+ object. This updates the object in place for reflection but does
+ the real add on the server to avoid race conditions. This assumes
+ that the database's '+' operation is atomic.
+
+ If `expire` is set to `True`, the value is expired and reloaded instead
+ of added of the local value. This is a good idea if the value should
+ be used for reflection.
+ """
+ sess = orm.object_session(obj) or session
+ mapper = orm.object_mapper(obj)
+ pk = mapper.primary_key_from_instance(obj)
+ assert len(pk) == 1, 'atomic_add not supported for classes with ' \
+ 'more than one primary key'
+
+ val = orm.attributes.get_attribute(obj, column)
+ if expire:
+ orm.attributes.instance_state(obj).expire_attributes([column])
+ else:
+ orm.attributes.set_committed_value(obj, column, val + delta)
+
+ table = mapper.tables[0]
+ stmt = sql.update(table, mapper.primary_key[0] == pk[0], {
+ column: table.c[column] + delta
+ })
+ sess.execute(stmt)
+
+
+class ConnectionQueryTrackingProxy(ConnectionProxy):
+ """A proxy that if enabled counts the queries."""
+
+ def cursor_execute(self, execute, cursor, statement, parameters,
+ context, executemany):
+ start = _timer()
+ try:
+ return execute(cursor, statement, parameters, context)
+ finally:
+ from solace.application import Request
+ request = Request.current
+ if request is not None:
+ request.sql_queries.append((statement, parameters,
+ start, _timer()))
+
+
+class LocaleType(TypeDecorator):
+ """A locale in the database."""
+
+ impl = String
+
+ def __init__(self):
+ TypeDecorator.__init__(self, 10)
+
+ def process_bind_param(self, value, dialect):
+ if value is None:
+ return
+ return unicode(str(value))
+
+ def process_result_value(self, value, dialect):
+ if value is not None:
+ return Locale.parse(value)
+
+ def is_mutable(self):
+ return False
+
+
+class BadgeType(TypeDecorator):
+ """Holds a badge."""
+
+ impl = String
+
+ def __init__(self):
+ TypeDecorator.__init__(self, 30)
+
+ def process_bind_param(self, value, dialect):
+ if value is None:
+ return
+ return value.identifier
+
+ def process_result_value(self, value, dialect):
+ if value is not None:
+ from solace.badges import badges_by_id
+ return badges_by_id.get(value)
+
+ def is_mutable(self):
+ return False
+
+
+metadata = MetaData()
+session = orm.scoped_session(lambda: orm.create_session(get_engine(),
+ autoflush=True, autocommit=False))
+
+
+users = Table('users', metadata,
+ # the internal ID of the user. Even if an external Auth system is
+ # used, we're storing the users a second time internal so that we
+ # can easilier work with relations.
+ Column('user_id', Integer, primary_key=True),
+ # the user's reputation
+ Column('reputation', Integer, nullable=False),
+ # the username of the user. For external auth systems it makes a
+ # lot of sense to allow the user to chose a name on first login.
+ Column('username', String(40), unique=True),
+ # the email of the user. If an external auth system is used, the
+ # login code should update that information automatically on login
+ Column('email', String(200), index=True),
+ # the password hash. Probably only used for the builtin auth system.
+ Column('pw_hash', String(60)),
+ # the realname of the user
+ Column('real_name', String(200)),
+ # the number of upvotes casted
+ Column('upvotes', Integer, nullable=False),
+ # the number of downvotes casted
+ Column('downvotes', Integer, nullable=False),
+ # the number of bronce badges
+ Column('bronce_badges', Integer, nullable=False),
+ # the number of silver badges
+ Column('silver_badges', Integer, nullable=False),
+ # the number of gold badges
+ Column('gold_badges', Integer, nullable=False),
+ # the number of platin badges
+ Column('platin_badges', Integer, nullable=False),
+ # true if the user is an administrator
+ Column('is_admin', Boolean, nullable=False),
+ # the date of the last login
+ Column('last_login', DateTime),
+ # the user's activation key. If this is NULL, the user is already
+ # activated, otherwise this is the key the user has to enter on the
+ # activation page (it's part of the link actually) to activate the
+ # account.
+ Column('activation_key', String(10))
+)
+
+user_activities = Table('user_activities', metadata,
+ # the id of the actitity, exists only for the database
+ Column('activity_id', Integer, primary_key=True),
+ # the user the activity is for
+ Column('user_id', Integer, ForeignKey('users.user_id')),
+ # the language code for this activity stat
+ Column('locale', LocaleType, index=True),
+ # the internal activity counter
+ Column('counter', Integer, nullable=False),
+ # the date of the first activity in a language
+ Column('first_activity', DateTime, nullable=False),
+ # the date of the last activity in the language
+ Column('last_activity', DateTime, nullable=False)
+)
+
+user_badges = Table('user_badges', metadata,
+ # the internal id
+ Column('badge_id', Integer, primary_key=True),
+ # who was the badge awarded to?
+ Column('user_id', Integer, ForeignKey('users.user_id')),
+ # which badge?
+ Column('badge', BadgeType(), index=True),
+ # when was the badge awarded?
+ Column('awarded', DateTime),
+ # optional extra information for the badge system
+ Column('payload', String(255))
+)
+
+user_messages = Table('user_messages', metadata,
+ # the message id
+ Column('message_id', Integer, primary_key=True),
+ # who was the message sent to?
+ Column('user_id', Integer, ForeignKey('users.user_id')),
+ # the text of the message
+ Column('text', String(512))
+)
+
+topics = Table('topics', metadata,
+ # each topic has an internal ID. This ID is also displayed in the
+ # URL next to an automatically slugified version of the title.
+ Column('topic_id', Integer, primary_key=True),
+ # the language of the topic
+ Column('locale', LocaleType, index=True),
+ # the number of votes on the question_post (reflected)
+ Column('votes', Integer, nullable=False),
+ # the title for the topic (actually, the title of the question, just
+ # that posts do not have titles, so it's only stored here)
+ Column('title', String(100)),
+ # the ID of the first post, the post that is the actual question.
+ Column('question_post_id', Integer, ForeignKey('posts.post_id')),
+ # the ID of the post that is accepted as answer. If no answer is
+ # accepted, this is None.
+ Column('answer_post_id', Integer, ForeignKey('posts.post_id')),
+ # the following information is denormalized from the posts table
+ # in the PostSetterExtension
+ Column('date', DateTime),
+ Column('author_id', Integer, ForeignKey('users.user_id')),
+ Column('answer_date', DateTime),
+ Column('answer_author_id', Integer, ForeignKey('users.user_id')),
+ # the date of the last change in the topic
+ Column('last_change', DateTime),
+ # the number of replies on the question (post-count - 1)
+ # the ReplyCounterExtension takes care of that
+ Column('reply_count', Integer, nullable=False),
+ # the hotness
+ Column('hotness', Float, nullable=False),
+ # reflected from the question post. True if deleted
+ Column('is_deleted', Boolean, nullable=False)
+)
+
+posts = Table('posts', metadata,
+ # the internal ID of the post, also used as anchor
+ Column('post_id', Integer, primary_key=True),
+ # the id of the topic the post belongs to
+ Column('topic_id', Integer, ForeignKey('topics.topic_id')),
+ # the text of the post
+ Column('text', Text),
+ # the text rendered to HTML
+ Column('rendered_text', Text),
+ # the id of the user that wrote the post.
+ Column('author_id', Integer, ForeignKey('users.user_id')),
+ # the id of the user that edited the post.
+ Column('editor_id', Integer, ForeignKey('users.user_id')),
+ # true if the post is an answer
+ Column('is_answer', Boolean),
+ # true if the post is a question
+ Column('is_question', Boolean),
+ # the date of the post creation
+ Column('created', DateTime),
+ # the date of the last edit
+ Column('updated', DateTime),
+ # the number of votes
+ Column('votes', Integer),
+ # the number of edits
+ Column('edits', Integer, nullable=False),
+ # the number of comments attached to the post
+ Column('comment_count', Integer, nullable=False),
+ # true if the post is deleted
+ Column('is_deleted', Boolean)