# HG changeset patch # User Madhusudan C.S # Date 1248464386 -19800 # Node ID 0ede2f3adbc14c70d3152975a860fd8d47ae2cbb # Parent a525a55833f13104894e608229828de40f7ddf07 Adds to Melange a tags framework based on taggable-mixin. The taggable-mixin allowed only tag per Datastore model. This is extended framework allows any arbitrary number of tags per Datastore model. Also, now one can define different models for different Tag types which are all inherited from the base Tag model provided by taggable-mixin. The GHOPTask model makes use of 2 tags per model, one for difficulty and the other for task_type, both using the tags framework. Reviewed by: Paweł Sołyga diff -r a525a55833f1 -r 0ede2f3adbc1 app/soc/modules/ghop/models/task.py --- a/app/soc/modules/ghop/models/task.py Fri Jul 24 21:00:04 2009 +0200 +++ b/app/soc/modules/ghop/models/task.py Sat Jul 25 01:09:46 2009 +0530 @@ -27,6 +27,10 @@ from django.utils.translation import ugettext +from taggable.taggable import Tag +from taggable.taggable import Taggable +from taggable.taggable import tag_property + import soc.models.linkable import soc.models.role import soc.models.student @@ -34,8 +38,51 @@ import soc.modules.ghop.models.program +class TaskTag(Tag): + """Model for storing all Task tags. + """ + + #: Each task_type tag is scoped under the program. + scope = db.ReferenceProperty(reference_class=soc.models.linkable.Linkable, + required=True, + collection_name='task_type_tags') + + @classmethod + def __key_name(cls, scope_path, tag_name): + return scope_path + '/' + tag_name -class GHOPTask(soc.models.linkable.Linkable): + @classmethod + def get_by_name(cls, tag_name): + tags = db.Query(cls).filter('tag =', tag_name).fetch(1000) + return tags + + @classmethod + def get_or_create(cls, program, tag_name): + "Get the Tag object that has the tag value given by tag_value." + tag_key_name = cls.__key_name(program.key().name(), tag_name) + existing_tag = cls.get_by_key_name(tag_key_name) + if existing_tag is None: + # The tag does not yet exist, so create it. + def create_tag_txn(): + new_tag = cls(key_name=tag_key_name, tag=tag_name, scope=program) + new_tag.put() + return new_tag + existing_tag = db.run_in_transaction(create_tag_txn) + return existing_tag + +class TaskTypeTag(TaskTag): + "Model for storing of task type tags." + + pass + + +class TaskDifficultyTag(TaskTag): + "Model for storing of task difficulty level tags." + + pass + + +class GHOPTask(Taggable, soc.models.linkable.Linkable): """Model for a task used in GHOP workflow. The scope property of Linkable will be set to the Organization to which @@ -56,15 +103,11 @@ #: Field indicating the difficulty level of the Task. This is not #: mandatory so the it can be assigned at any later stage. #: The options are configured by a Program Admin. - difficulty = db.StringProperty(required=False, - verbose_name=ugettext('Difficulty')) - difficulty.help_text = ugettext('Difficulty Level of the task') + difficulty = tag_property('difficulty') #: Required field which contains the type of the task. These types are #: configured by a Program Admin. - type = db.StringListProperty(required=True, - verbose_name=ugettext('Task Type')) - type.help_text = ugettext('Type of the task') + task_type = tag_property('task_type') #: A field which contains time allowed for completing the task (in hours) #: from the moment that this task has been assigned to a Student @@ -148,7 +191,8 @@ #: Required field containing the Mentor/Org Admin who last edited this #: task. It changes only when Mentor/Org Admin changes title, description, - #: difficulty, type, time_to_complete. + #: difficulty, task_type, time_to_complete. If site developer has modified + #: the task, it is empty. modified_by = db.ReferenceProperty(reference_class=soc.models.role.Role, required=True, collection_name='edited_tasks', @@ -179,3 +223,20 @@ #: timestamp given by the key. #: Reference properties will be stored by calling str() on their Key. history = db.TextProperty(required=True, default='') + + def __init__(self, parent=None, key_name=None, + app=None, **entity_values): + """Constructor for GHOPTask Model. + + Args: + See Google App Engine APIs. + """ + + # explicitly call the AppEngine datastore Model constructor + db.Model.__init__(self, parent, key_name, app, **entity_values) + + # call the Taggable constructor to initialize the tags specified as + # keyword arguments + Taggable.__init__(self, task_type=TaskTypeTag, + difficulty=TaskDifficultyTag) + diff -r a525a55833f1 -r 0ede2f3adbc1 app/taggable-mixin/COPYING --- a/app/taggable-mixin/COPYING Fri Jul 24 21:00:04 2009 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff -r a525a55833f1 -r 0ede2f3adbc1 app/taggable-mixin/taggable.html --- a/app/taggable-mixin/taggable.html Fri Jul 24 21:00:04 2009 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,9732 +0,0 @@ - - - - - - - - - - - - Taggable - a portable mixin class for adding Tags to Google AppEngine Models - - - - - - - - - - - -
-
-
-
-
-
-
-
-
-
-
-
<!--{{{-->
-<link rel='alternate' type='application/rss+xml' title='RSS' href='index.xml'/>
-<!--}}}-->
-
-
-
Background: #fff
-Foreground: #000
-PrimaryPale: #8cf
-PrimaryLight: #18f
-PrimaryMid: #04b
-PrimaryDark: #014
-SecondaryPale: #ffc
-SecondaryLight: #fe8
-SecondaryMid: #db4
-SecondaryDark: #841
-TertiaryPale: #eee
-TertiaryLight: #ccc
-TertiaryMid: #999
-TertiaryDark: #666
-Error: #f88
-
-
-
/*{{{*/
-body {background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];}
-
-a {color:[[ColorPalette::PrimaryMid]];}
-a:hover {background-color:[[ColorPalette::PrimaryMid]]; color:[[ColorPalette::Background]];}
-a img {border:0;}
-
-h1,h2,h3,h4,h5,h6 {color:[[ColorPalette::SecondaryDark]]; background:transparent;}
-h1 {border-bottom:2px solid [[ColorPalette::TertiaryLight]];}
-h2,h3 {border-bottom:1px solid [[ColorPalette::TertiaryLight]];}
-
-.button {color:[[ColorPalette::PrimaryDark]]; border:1px solid [[ColorPalette::Background]];}
-.button:hover {color:[[ColorPalette::PrimaryDark]]; background:[[ColorPalette::SecondaryLight]]; border-color:[[ColorPalette::SecondaryMid]];}
-.button:active {color:[[ColorPalette::Background]]; background:[[ColorPalette::SecondaryMid]]; border:1px solid [[ColorPalette::SecondaryDark]];}
-
-.header {background:[[ColorPalette::PrimaryMid]];}
-.headerShadow {color:[[ColorPalette::Foreground]];}
-.headerShadow a {font-weight:normal; color:[[ColorPalette::Foreground]];}
-.headerForeground {color:[[ColorPalette::Background]];}
-.headerForeground a {font-weight:normal; color:[[ColorPalette::PrimaryPale]];}
-
-.tabSelected{color:[[ColorPalette::PrimaryDark]];
-	background:[[ColorPalette::TertiaryPale]];
-	border-left:1px solid [[ColorPalette::TertiaryLight]];
-	border-top:1px solid [[ColorPalette::TertiaryLight]];
-	border-right:1px solid [[ColorPalette::TertiaryLight]];
-}
-.tabUnselected {color:[[ColorPalette::Background]]; background:[[ColorPalette::TertiaryMid]];}
-.tabContents {color:[[ColorPalette::PrimaryDark]]; background:[[ColorPalette::TertiaryPale]]; border:1px solid [[ColorPalette::TertiaryLight]];}
-.tabContents .button {border:0;}
-
-#sidebar {}
-#sidebarOptions input {border:1px solid [[ColorPalette::PrimaryMid]];}
-#sidebarOptions .sliderPanel {background:[[ColorPalette::PrimaryPale]];}
-#sidebarOptions .sliderPanel a {border:none;color:[[ColorPalette::PrimaryMid]];}
-#sidebarOptions .sliderPanel a:hover {color:[[ColorPalette::Background]]; background:[[ColorPalette::PrimaryMid]];}
-#sidebarOptions .sliderPanel a:active {color:[[ColorPalette::PrimaryMid]]; background:[[ColorPalette::Background]];}
-
-.wizard {background:[[ColorPalette::PrimaryPale]]; border:1px solid [[ColorPalette::PrimaryMid]];}
-.wizard h1 {color:[[ColorPalette::PrimaryDark]]; border:none;}
-.wizard h2 {color:[[ColorPalette::Foreground]]; border:none;}
-.wizardStep {background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];
-	border:1px solid [[ColorPalette::PrimaryMid]];}
-.wizardStep.wizardStepDone {background:[[ColorPalette::TertiaryLight]];}
-.wizardFooter {background:[[ColorPalette::PrimaryPale]];}
-.wizardFooter .status {background:[[ColorPalette::PrimaryDark]]; color:[[ColorPalette::Background]];}
-.wizard .button {color:[[ColorPalette::Foreground]]; background:[[ColorPalette::SecondaryLight]]; border: 1px solid;
-	border-color:[[ColorPalette::SecondaryPale]] [[ColorPalette::SecondaryDark]] [[ColorPalette::SecondaryDark]] [[ColorPalette::SecondaryPale]];}
-.wizard .button:hover {color:[[ColorPalette::Foreground]]; background:[[ColorPalette::Background]];}
-.wizard .button:active {color:[[ColorPalette::Background]]; background:[[ColorPalette::Foreground]]; border: 1px solid;
-	border-color:[[ColorPalette::PrimaryDark]] [[ColorPalette::PrimaryPale]] [[ColorPalette::PrimaryPale]] [[ColorPalette::PrimaryDark]];}
-
-#messageArea {border:1px solid [[ColorPalette::SecondaryMid]]; background:[[ColorPalette::SecondaryLight]]; color:[[ColorPalette::Foreground]];}
-#messageArea .button {color:[[ColorPalette::PrimaryMid]]; background:[[ColorPalette::SecondaryPale]]; border:none;}
-
-.popupTiddler {background:[[ColorPalette::TertiaryPale]]; border:2px solid [[ColorPalette::TertiaryMid]];}
-
-.popup {background:[[ColorPalette::TertiaryPale]]; color:[[ColorPalette::TertiaryDark]]; border-left:1px solid [[ColorPalette::TertiaryMid]]; border-top:1px solid [[ColorPalette::TertiaryMid]]; border-right:2px solid [[ColorPalette::TertiaryDark]]; border-bottom:2px solid [[ColorPalette::TertiaryDark]];}
-.popup hr {color:[[ColorPalette::PrimaryDark]]; background:[[ColorPalette::PrimaryDark]]; border-bottom:1px;}
-.popup li.disabled {color:[[ColorPalette::TertiaryMid]];}
-.popup li a, .popup li a:visited {color:[[ColorPalette::Foreground]]; border: none;}
-.popup li a:hover {background:[[ColorPalette::SecondaryLight]]; color:[[ColorPalette::Foreground]]; border: none;}
-.popup li a:active {background:[[ColorPalette::SecondaryPale]]; color:[[ColorPalette::Foreground]]; border: none;}
-.popupHighlight {background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];}
-.listBreak div {border-bottom:1px solid [[ColorPalette::TertiaryDark]];}
-
-.tiddler .defaultCommand {font-weight:bold;}
-
-.shadow .title {color:[[ColorPalette::TertiaryDark]];}
-
-.title {color:[[ColorPalette::SecondaryDark]];}
-.subtitle {color:[[ColorPalette::TertiaryDark]];}
-
-.toolbar {color:[[ColorPalette::PrimaryMid]];}
-.toolbar a {color:[[ColorPalette::TertiaryLight]];}
-.selected .toolbar a {color:[[ColorPalette::TertiaryMid]];}
-.selected .toolbar a:hover {color:[[ColorPalette::Foreground]];}
-
-.tagging, .tagged {border:1px solid [[ColorPalette::TertiaryPale]]; background-color:[[ColorPalette::TertiaryPale]];}
-.selected .tagging, .selected .tagged {background-color:[[ColorPalette::TertiaryLight]]; border:1px solid [[ColorPalette::TertiaryMid]];}
-.tagging .listTitle, .tagged .listTitle {color:[[ColorPalette::PrimaryDark]];}
-.tagging .button, .tagged .button {border:none;}
-
-.footer {color:[[ColorPalette::TertiaryLight]];}
-.selected .footer {color:[[ColorPalette::TertiaryMid]];}
-
-.sparkline {background:[[ColorPalette::PrimaryPale]]; border:0;}
-.sparktick {background:[[ColorPalette::PrimaryDark]];}
-
-.error, .errorButton {color:[[ColorPalette::Foreground]]; background:[[ColorPalette::Error]];}
-.warning {color:[[ColorPalette::Foreground]]; background:[[ColorPalette::SecondaryPale]];}
-.lowlight {background:[[ColorPalette::TertiaryLight]];}
-
-.zoomer {background:none; color:[[ColorPalette::TertiaryMid]]; border:3px solid [[ColorPalette::TertiaryMid]];}
-
-.imageLink, #displayArea .imageLink {background:transparent;}
-
-.annotation {background:[[ColorPalette::SecondaryLight]]; color:[[ColorPalette::Foreground]]; border:2px solid [[ColorPalette::SecondaryMid]];}
-
-.viewer .listTitle {list-style-type:none; margin-left:-2em;}
-.viewer .button {border:1px solid [[ColorPalette::SecondaryMid]];}
-.viewer blockquote {border-left:3px solid [[ColorPalette::TertiaryDark]];}
-
-.viewer table, table.twtable {border:2px solid [[ColorPalette::TertiaryDark]];}
-.viewer th, .viewer thead td, .twtable th, .twtable thead td {background:[[ColorPalette::SecondaryMid]]; border:1px solid [[ColorPalette::TertiaryDark]]; color:[[ColorPalette::Background]];}
-.viewer td, .viewer tr, .twtable td, .twtable tr {border:1px solid [[ColorPalette::TertiaryDark]];}
-
-.viewer pre {border:1px solid [[ColorPalette::SecondaryLight]]; background:[[ColorPalette::SecondaryPale]];}
-.viewer code {color:[[ColorPalette::SecondaryDark]];}
-.viewer hr {border:0; border-top:dashed 1px [[ColorPalette::TertiaryDark]]; color:[[ColorPalette::TertiaryDark]];}
-
-.highlight, .marked {background:[[ColorPalette::SecondaryLight]];}
-
-.editor input {border:1px solid [[ColorPalette::PrimaryMid]];}
-.editor textarea {border:1px solid [[ColorPalette::PrimaryMid]]; width:100%;}
-.editorFooter {color:[[ColorPalette::TertiaryMid]];}
-
-#backstageArea {background:[[ColorPalette::Foreground]]; color:[[ColorPalette::TertiaryMid]];}
-#backstageArea a {background:[[ColorPalette::Foreground]]; color:[[ColorPalette::Background]]; border:none;}
-#backstageArea a:hover {background:[[ColorPalette::SecondaryLight]]; color:[[ColorPalette::Foreground]]; }
-#backstageArea a.backstageSelTab {background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];}
-#backstageButton a {background:none; color:[[ColorPalette::Background]]; border:none;}
-#backstageButton a:hover {background:[[ColorPalette::Foreground]]; color:[[ColorPalette::Background]]; border:none;}
-#backstagePanel {background:[[ColorPalette::Background]]; border-color: [[ColorPalette::Background]] [[ColorPalette::TertiaryDark]] [[ColorPalette::TertiaryDark]] [[ColorPalette::TertiaryDark]];}
-.backstagePanelFooter .button {border:none; color:[[ColorPalette::Background]];}
-.backstagePanelFooter .button:hover {color:[[ColorPalette::Foreground]];}
-#backstageCloak {background:[[ColorPalette::Foreground]]; opacity:0.6; filter:'alpha(opacity:60)';}
-/*}}}*/
-
-
-
/*{{{*/
-* html .tiddler {height:1%;}
-
-body {font-size:.75em; font-family:arial,helvetica; margin:0; padding:0;}
-
-h1,h2,h3,h4,h5,h6 {font-weight:bold; text-decoration:none;}
-h1,h2,h3 {padding-bottom:1px; margin-top:1.2em;margin-bottom:0.3em;}
-h4,h5,h6 {margin-top:1em;}
-h1 {font-size:1.35em;}
-h2 {font-size:1.25em;}
-h3 {font-size:1.1em;}
-h4 {font-size:1em;}
-h5 {font-size:.9em;}
-
-hr {height:1px;}
-
-a {text-decoration:none;}
-
-dt {font-weight:bold;}
-
-ol {list-style-type:decimal;}
-ol ol {list-style-type:lower-alpha;}
-ol ol ol {list-style-type:lower-roman;}
-ol ol ol ol {list-style-type:decimal;}
-ol ol ol ol ol {list-style-type:lower-alpha;}
-ol ol ol ol ol ol {list-style-type:lower-roman;}
-ol ol ol ol ol ol ol {list-style-type:decimal;}
-
-.txtOptionInput {width:11em;}
-
-#contentWrapper .chkOptionInput {border:0;}
-
-.externalLink {text-decoration:underline;}
-
-.indent {margin-left:3em;}
-.outdent {margin-left:3em; text-indent:-3em;}
-code.escaped {white-space:nowrap;}
-
-.tiddlyLinkExisting {font-weight:bold;}
-.tiddlyLinkNonExisting {font-style:italic;}
-
-/* the 'a' is required for IE, otherwise it renders the whole tiddler in bold */
-a.tiddlyLinkNonExisting.shadow {font-weight:bold;}
-
-#mainMenu .tiddlyLinkExisting,
-	#mainMenu .tiddlyLinkNonExisting,
-	#sidebarTabs .tiddlyLinkNonExisting {font-weight:normal; font-style:normal;}
-#sidebarTabs .tiddlyLinkExisting {font-weight:bold; font-style:normal;}
-
-.header {position:relative;}
-.header a:hover {background:transparent;}
-.headerShadow {position:relative; padding:4.5em 0em 1em 1em; left:-1px; top:-1px;}
-.headerForeground {position:absolute; padding:4.5em 0em 1em 1em; left:0px; top:0px;}
-
-.siteTitle {font-size:3em;}
-.siteSubtitle {font-size:1.2em;}
-
-#mainMenu {position:absolute; left:0; width:10em; text-align:right; line-height:1.6em; padding:1.5em 0.5em 0.5em 0.5em; font-size:1.1em;}
-
-#sidebar {position:absolute; right:3px; width:16em; font-size:.9em;}
-#sidebarOptions {padding-top:0.3em;}
-#sidebarOptions a {margin:0em 0.2em; padding:0.2em 0.3em; display:block;}
-#sidebarOptions input {margin:0.4em 0.5em;}
-#sidebarOptions .sliderPanel {margin-left:1em; padding:0.5em; font-size:.85em;}
-#sidebarOptions .sliderPanel a {font-weight:bold; display:inline; padding:0;}
-#sidebarOptions .sliderPanel input {margin:0 0 .3em 0;}
-#sidebarTabs .tabContents {width:15em; overflow:hidden;}
-
-.wizard {padding:0.1em 1em 0em 2em;}
-.wizard h1 {font-size:2em; font-weight:bold; background:none; padding:0em 0em 0em 0em; margin:0.4em 0em 0.2em 0em;}
-.wizard h2 {font-size:1.2em; font-weight:bold; background:none; padding:0em 0em 0em 0em; margin:0.4em 0em 0.2em 0em;}
-.wizardStep {padding:1em 1em 1em 1em;}
-.wizard .button {margin:0.5em 0em 0em 0em; font-size:1.2em;}
-.wizardFooter {padding:0.8em 0.4em 0.8em 0em;}
-.wizardFooter .status {padding:0em 0.4em 0em 0.4em; margin-left:1em;}
-.wizard .button {padding:0.1em 0.2em 0.1em 0.2em;}
-
-#messageArea {position:fixed; top:2em; right:0em; margin:0.5em; padding:0.5em; z-index:2000; _position:absolute;}
-.messageToolbar {display:block; text-align:right; padding:0.2em 0.2em 0.2em 0.2em;}
-#messageArea a {text-decoration:underline;}
-
-.tiddlerPopupButton {padding:0.2em 0.2em 0.2em 0.2em;}
-.popupTiddler {position: absolute; z-index:300; padding:1em 1em 1em 1em; margin:0;}
-
-.popup {position:absolute; z-index:300; font-size:.9em; padding:0; list-style:none; margin:0;}
-.popup .popupMessage {padding:0.4em;}
-.popup hr {display:block; height:1px; width:auto; padding:0; margin:0.2em 0em;}
-.popup li.disabled {padding:0.4em;}
-.popup li a {display:block; padding:0.4em; font-weight:normal; cursor:pointer;}
-.listBreak {font-size:1px; line-height:1px;}
-.listBreak div {margin:2px 0;}
-
-.tabset {padding:1em 0em 0em 0.5em;}
-.tab {margin:0em 0em 0em 0.25em; padding:2px;}
-.tabContents {padding:0.5em;}
-.tabContents ul, .tabContents ol {margin:0; padding:0;}
-.txtMainTab .tabContents li {list-style:none;}
-.tabContents li.listLink { margin-left:.75em;}
-
-#contentWrapper {display:block;}
-#splashScreen {display:none;}
-
-#displayArea {margin:1em 17em 0em 14em;}
-
-.toolbar {text-align:right; font-size:.9em;}
-
-.tiddler {padding:1em 1em 0em 1em;}
-
-.missing .viewer,.missing .title {font-style:italic;}
-
-.title {font-size:1.6em; font-weight:bold;}
-
-.missing .subtitle {display:none;}
-.subtitle {font-size:1.1em;}
-
-.tiddler .button {padding:0.2em 0.4em;}
-
-.tagging {margin:0.5em 0.5em 0.5em 0; float:left; display:none;}
-.isTag .tagging {display:block;}
-.tagged {margin:0.5em; float:right;}
-.tagging, .tagged {font-size:0.9em; padding:0.25em;}
-.tagging ul, .tagged ul {list-style:none; margin:0.25em; padding:0;}
-.tagClear {clear:both;}
-
-.footer {font-size:.9em;}
-.footer li {display:inline;}
-
-.annotation {padding:0.5em; margin:0.5em;}
-
-* html .viewer pre {width:99%; padding:0 0 1em 0;}
-.viewer {line-height:1.4em; padding-top:0.5em;}
-.viewer .button {margin:0em 0.25em; padding:0em 0.25em;}
-.viewer blockquote {line-height:1.5em; padding-left:0.8em;margin-left:2.5em;}
-.viewer ul, .viewer ol {margin-left:0.5em; padding-left:1.5em;}
-
-.viewer table, table.twtable {border-collapse:collapse; margin:0.8em 1.0em;}
-.viewer th, .viewer td, .viewer tr,.viewer caption,.twtable th, .twtable td, .twtable tr,.twtable caption {padding:3px;}
-table.listView {font-size:0.85em; margin:0.8em 1.0em;}
-table.listView th, table.listView td, table.listView tr {padding:0px 3px 0px 3px;}
-
-.viewer pre {padding:0.5em; margin-left:0.5em; font-size:1.2em; line-height:1.4em; overflow:auto;}
-.viewer code {font-size:1.2em; line-height:1.4em;}
-
-.editor {font-size:1.1em;}
-.editor input, .editor textarea {display:block; width:100%; font:inherit;}
-.editorFooter {padding:0.25em 0em; font-size:.9em;}
-.editorFooter .button {padding-top:0px; padding-bottom:0px;}
-
-.fieldsetFix {border:0; padding:0; margin:1px 0px 1px 0px;}
-
-.sparkline {line-height:1em;}
-.sparktick {outline:0;}
-
-.zoomer {font-size:1.1em; position:absolute; overflow:hidden;}
-.zoomer div {padding:1em;}
-
-* html #backstage {width:99%;}
-* html #backstageArea {width:99%;}
-#backstageArea {display:none; position:relative; overflow: hidden; z-index:150; padding:0.3em 0.5em 0.3em 0.5em;}
-#backstageToolbar {position:relative;}
-#backstageArea a {font-weight:bold; margin-left:0.5em; padding:0.3em 0.5em 0.3em 0.5em;}
-#backstageButton {display:none; position:absolute; z-index:175; top:0em; right:0em;}
-#backstageButton a {padding:0.1em 0.4em 0.1em 0.4em; margin:0.1em 0.1em 0.1em 0.1em;}
-#backstage {position:relative; width:100%; z-index:50;}
-#backstagePanel {display:none; z-index:100; position:absolute; margin:0em 3em 0em 3em; padding:1em 1em 1em 1em;}
-.backstagePanelFooter {padding-top:0.2em; float:right;}
-.backstagePanelFooter a {padding:0.2em 0.4em 0.2em 0.4em;}
-#backstageCloak {display:none; z-index:20; position:absolute; width:100%; height:100px;}
-
-.whenBackstage {display:none;}
-.backstageVisible .whenBackstage {display:block;}
-/*}}}*/
-
-
-
/***
-StyleSheet for use when a translation requires any css style changes.
-This StyleSheet can be used directly by languages such as Chinese, Japanese and Korean which need larger font sizes.
-***/
-/*{{{*/
-body {font-size:0.8em;}
-#sidebarOptions {font-size:1.05em;}
-#sidebarOptions a {font-style:normal;}
-#sidebarOptions .sliderPanel {font-size:0.95em;}
-.subtitle {font-size:0.8em;}
-.viewer table.listView {font-size:0.95em;}
-/*}}}*/
-
-
-
/*{{{*/
-@media print {
-#mainMenu, #sidebar, #messageArea, .toolbar, #backstageButton, #backstageArea {display: none ! important;}
-#displayArea {margin: 1em 1em 0em 1em;}
-/* Fixes a feature in Firefox 1.5.0.2 where print preview displays the noscript content */
-noscript {display:none;}
-}
-/*}}}*/
-
-
-
<!--{{{-->
-<div class='header' macro='gradient vert [[ColorPalette::PrimaryLight]] [[ColorPalette::PrimaryMid]]'>
-<div class='headerShadow'>
-<span class='siteTitle' refresh='content' tiddler='SiteTitle'></span>&nbsp;
-<span class='siteSubtitle' refresh='content' tiddler='SiteSubtitle'></span>
-</div>
-<div class='headerForeground'>
-<span class='siteTitle' refresh='content' tiddler='SiteTitle'></span>&nbsp;
-<span class='siteSubtitle' refresh='content' tiddler='SiteSubtitle'></span>
-</div>
-</div>
-<div id='mainMenu' refresh='content' tiddler='MainMenu'></div>
-<div id='sidebar'>
-<div id='sidebarOptions' refresh='content' tiddler='SideBarOptions'></div>
-<div id='sidebarTabs' refresh='content' force='true' tiddler='SideBarTabs'></div>
-</div>
-<div id='displayArea'>
-<div id='messageArea'></div>
-<div id='tiddlerDisplay'></div>
-</div>
-<!--}}}-->
-
-
-
<!--{{{-->
-<div class='toolbar' macro='toolbar [[ToolbarCommands::ViewToolbar]]'></div>
-<div class='title' macro='view title'></div>
-<div class='subtitle'><span macro='view modifier link'></span>, <span macro='view modified date'></span> (<span macro='message views.wikified.createdPrompt'></span> <span macro='view created date'></span>)</div>
-<div class='tagging' macro='tagging'></div>
-<div class='tagged' macro='tags'></div>
-<div class='viewer' macro='view text wikified'></div>
-<div class='tagClear'></div>
-<!--}}}-->
-
-
-
<!--{{{-->
-<div class='toolbar' macro='toolbar [[ToolbarCommands::EditToolbar]]'></div>
-<div class='title' macro='view title'></div>
-<div class='editor' macro='edit title'></div>
-<div macro='annotations'></div>
-<div class='editor' macro='edit text'></div>
-<div class='editor' macro='edit tags'></div><div class='editorFooter'><span macro='message views.editor.tagPrompt'></span><span macro='tagChooser'></span></div>
-<!--}}}-->
-
-
-
To get started with this blank TiddlyWiki, you'll need to modify the following tiddlers:
-* SiteTitle & SiteSubtitle: The title and subtitle of the site, as shown above (after saving, they will also appear in the browser title bar)
-* MainMenu: The menu (usually on the left)
-* DefaultTiddlers: Contains the names of the tiddlers that you want to appear when the TiddlyWiki is opened
-You'll also need to enter your username for signing your edits: <<option txtUserName>>
-
-
-
These InterfaceOptions for customising TiddlyWiki are saved in your browser
-
-Your username for signing your edits. Write it as a WikiWord (eg JoeBloggs)
-
-<<option txtUserName>>
-<<option chkSaveBackups>> SaveBackups
-<<option chkAutoSave>> AutoSave
-<<option chkRegExpSearch>> RegExpSearch
-<<option chkCaseSensitiveSearch>> CaseSensitiveSearch
-<<option chkAnimate>> EnableAnimations
-
-----
-Also see AdvancedOptions
-
-
-
<<importTiddlers>>
-
-
- -
-
-
Adam is the author of ''Taggable.''  To find out more about him and other software that he has created, visit http://www.adamcrossland.net.  You can also visit his blog, http://blog.adamcrossland.net, to see the code in action; he created ''Taggable'' as part of the blogging software project the he wrote as a vehicle to learn about [[Google AppEngine]].
-
-
-
{{{
-Copyright 2008 Adam A. Crossland
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-}}}
-
-
-
''1.0''
-Initial release
-----
-''2.0''
-This release has major changes to both [[Tag|Tag Class]] and [[Taggable|Taggable Class]], reflecting significant and important lessons that [[the author|AdamCrossland]] learned about [[Google AppEngine]].  Specifically, this release's changes should substantially improve the performance of any code that uses tags.
-*[[Tag|Tag Class]] entities are now stored with a custom key_name that allows them to be much-more-quickly retrieved from the data store.
-*The [[Tag|Tag Class]] has been rewritten to breakdown Tag/Taggable interactions in to a series of simple, atomic transactions.
-*[[Tag|Tag Class]] has a new property, [[tagged_count|Tag tagged_count Property]], that records the number of Taggable entities in its [[tagged|Tag tagged Property]] property.
-*Two new methods, [[get_tags_by_frequency|Tag get_tags_by_frequency Method]] and [[popular_tags|Tag popular_tags Method]], provide access to lists of [[Tags|Tag Class]] based on the number of [[Taggable|Taggable Class]] entities to which they refer.
-*A new method, [[get_tags_by_name|Tag get_tags_by_name Method]], provides lists of tags in alphabetical order.
-*The [[Taggable|Taggable Class]] class has been greatly simplified.  Multiple methods for getting and setting tags have been replaced with a single property, [[tags|Taggable tags Property]], that handles both getting and setting operations.
-*A full suite of automated unit tests for both classes has been created.
-
-
-
If you have any questions or suggestions about ''Taggable'', please feel free to contact the author, AdamCrossland, at adam@adamcrossland.net.  Alternatively, you could post the question in <html><a href="http://groups.google.com/group/google-appengine/topics">the Google AppEngine Group.</a></html>
-
-
-
If you are using ''Taggable'' and you have some changes, improvements or bug fixes that you'd like to contribute, please contact AdamCrossland at adam@adamcrossland.net.
-
-
-
Because the Tag records are stored and queried slightly differently (yet much-more efficiently) in //Taggable-mixin 2.0//, an existing application will have to go through a process of converting all of its existing tags.
-
-For AdamCBlog, the application from which //Taggable-mixin// is extracted, I added a new RequestHandler called UpdateTag:
-{{{
-class UpdateTag(RequestHandler):
-    def get(self):
-        from taggable import Tag
-        tag_name = self.request.get('tag')
-        tag = db.Query(Tag).filter('tag =', tag_name).fetch(1)[0]
-        if tag is not None:
-            new_tag = Tag.get_or_create(tag_name)
-            for each_tagged in tag.tagged:
-                new_tag.add_tagged(each_tagged)
-        tag.delete()
-    
-        self.redirect('/')
-}}}
-
-Then, I made the request /~UpdateTag?tag=//inserttaghere// for each Tag in the datastore.  Yes, it is laborious.
-
-Why not just create a RequestHandler that cycled through all of the tags automatically, converting them as it went?  I tried that, but there is too much processing involved; the request uses up its time quota and is killed before it can finish.
-
-
-
A [[Google AppEngine]] datastore property that holds a Python //datetime//.
-
-http://code.google.com/appengine/docs/datastore/typesandpropertyclasses.html#DateTimeProperty
-
-
-
[[Introduction]]
-
-
-
Google ~AppEngine is Google's platform for developing web applications that run inside Google's computing cloud.
-
-Find out more about it at <html><a href="http://code.google.com/appengine/">The Google AppEngine website</a></html>.
-
-
-
-
-
A [[Google AppEngine]] datastore property that holds a Python //long//.
-
-http://code.google.com/appengine/docs/datastore/typesandpropertyclasses.html#IntegerProperty
-
-
-
''Taggable'' is a <html><a href="http://www.linuxjournal.com/node/4540/print">mixin</a></html> class that AdamCrossland created in order to add [[tags|Tag Definition]] to his personal blog (http://blog.adamcrossland.net) which is built on [[Google AppEngine]].  The design seemed clean and portable, so he decided to share it with the greater [[AppEngine|Google AppEngine]] community.  It is available under [[Apache 2.0 Open Source license]].
-
-Users of taggable-mixin 1.0 should note that taggable-mixin 2.0 is not directly compatible with 1.0.  While it is possible to upgrade, existing Tag entities will have to be [[converted|Converting from 1.0 to 2.0]].
-
-
-
A [[Google AppEngine]] class that represents a unique key for a datastore entity.
-
-http://code.google.com/appengine/docs/datastore/keyclass.html
-
-
-
A [[Google AppEngine]] datastore property that represents a Python //list//.
-
-http://code.google.com/appengine/docs/datastore/typesandpropertyclasses.html#ListProperty
-
-
-
[[Introduction|Introduction]]
-ChangeLog
-StepByStep
-<<tag api>>
-----
-[[License|Apache 2.0 Open Source license]]
-[[Contribute]]
-[[Contact]]
-
-
-
A //~RequestHandler// is a Python class that inherits from //webapp.~RequestHandler//.  It is used to answer HTTP requests that are received by your [[Google AppEngine]] application.  For more information on this subject, please consult the <html><a href="http://code.google.com/appengine/docs/webapp/requesthandlers.html">documentation.</a></html>
-
-
-
-
-
<<search>><<closeAll>><<permaview>><<newTiddler>><<saveChanges>><<slider chkSliderOptionsPanel OptionsPanel "options »" "Change TiddlyWiki advanced options">>
-
-
-
a portable mixin class for adding Tags to Google ~AppEngine Models
-
-
-
Taggable
-
-
-
Here's a step-by-step guide to adding ''Taggable'' to your [[Google AppEngine]] application.  This presents the simplest, most straightforward path to integrating ''Taggable'' as as such, it does not cover all of the options that are available.  
-
-All of the code examples are modified extracts from the blogging software for which I originally created ''Taggable.''  //''These examples do not represent what the author considers to be complete and secure code; please make sure that you are familiar with best practices for building secure web applications before creating a web application.  The author of this document and the accompanying code bears no responsibility whatsoever for any losses or damages that you incur as a result of failing to take the appropriate steps to create a stable, secure, well-written web application.''//
-
-*Copy [[taggable.py]] to your application directory.
-*Import [[taggable.py]] into the python file that defines the Model that you want to make taggable:
-{{{
-from taggable import Taggable
-}}}
-*Add //Taggable// to the list of classes from which your Model class inherits.  Taggable -- and any other mixin classes -- should come before db.Model:
-{{{
-class Post(Taggable, db.Model):
-}}}
-*Add code to your Model's //init// method to call Taggable's //init// method.  If your Model class does not already override //init//, it will have to, and you will have to explicitly call the //init// method of any other superclass -- such as db.Model:
-{{{
-    def __init__(self, parent=None, key_name=None, app=None, **entity_values):
-        db.Model.__init__(self, parent, key_name, app, **entity_values)
-        Taggable.__init__(self)
-}}}
-*Add code to your template to express the tagging information:
-{{{
-{% if post.tags %}
-    <div class="posttags">tags:&nbsp;
-        {% for each_tag in post.tags %}
-            <a href="/searchbytag?tag={{ each_tag.tag|escape }}">{{ each_tag.tag }}</a>{% if forloop.last %}{% else %}, {% endif %}
-        {% endfor %}
-    </div>
-{% endif %}
-}}}
-or
-{{{
-<tr>
-    <td>Tags:</td>
-    <td>
-        <input type="TEXT" name="tags" size="106" value="{% if post %}{{ post.tags_string }}{% endif %}" />
-    </td>
-</tr>
-}}}
-*In any RequestHandler method that updates a Model object that has tags associated with it,  assign any new value to the ''Taggable'' object's [[tags|TaggableTagsProperty]]:
-{{{
-class EditPost(SmartHandler.SmartHandler):
-    def post(self):
-        postid = self.request.get('id')
-        post = Post.get(postid)
-        post.title = self.request.get('title')
-        post.body = self.request.get('body')
-        post.edited = datetime.datetime.now()
-        post.tags = self.request.get('tags')
-        post.put()
-        .
-        . do whatever else you need to do in your handler...
-        .
-}}}
-
-
-
A [[Google AppEngine]] datastore property that holds a Python //string// of 500 characters or less.
-
-http://code.google.com/appengine/docs/datastore/typesandpropertyclasses.html#StringProperty
-
-
-
The //Tag// class is the Model class that holds the data for an individual [[tag|Tag Definition]].
-
-It has four properties:
-*[[tag|Tag tag Property]]
-*[[added|Tag tag_added Property]]
-*[[tagged|Tag tagged Property]]
-*[[tagged_count|Tag tagged_count Property]]
-
-and ten methods:
-*[[remove_tagged|Tag remove_tagged Method]]
-*[[add_tagged|Tag add_tagged Method]]
-*[[clear_tagged|Tag clear_tagged Method]]
-*[[get_by_name|Tag get_by_name Method]]
-*[[get_tags_for_key|Tag get_tags_for_key Method]]
-*[[get_or_create|Tag get_or_create Method]]
-*[[get_tags_by_frequency|Tag get_tags_by_frequency Method]]
-*[[get_tags_by_name|Tag get_tags_by_name Method]]
-*[[popular_tags|Tag popular_tags Method]]
-*[[expire_cached_tags|Tag expire_cached_tags Method]]
-
-
-
A //Tag// is a word or short phrase that acts as metadata; it describes and increases the searchability and findability of data.
-
-http://en.wikipedia.org/wiki/Tag_(metadata)
-
-
-
//tag_instance.add_tagged(key)//
-* key - a [[Key|Key Class]] for a //Taggable// Model to mark with the [[Tag|Tag Class]]
-Returns: nothing
-
-The //add_tagged// method adds the [[Key|Key Class]] passed in to the Tag's [[tagged|Tag tagged Property]] collection and increments the [[tagged_count|Tag tagged_count Property]].  The operations are performed inside a transaction to ensure data intergity.
-
-Under most circumstances, the programmer will not need to call //add_tagged// directly; it is meant to be used internally by the [[Tagged Class]].  However, it is available for use by those who wish to customize the behavior of Taggable.
-
-
-
//tag_instance.clear_tagged(key)//
-* key - a [[Key|Key Class]] for a //Taggable// object.
-Returns: nothing
-
-The //clear_tagged// method empties the Tag's [[tagged|Tag tagged Property]] collection and sets the [[tagged_count|Tag tagged_count Property]] to zero.  The operations are performed inside a transaction to ensure data intergity.
-
-Under most circumstances, the programmer will not need to call //clear_tagged// directly; it is meant to be used internally by the [[Tagged Class]].  However, it is available for use by those who wish to customize the behavior of Taggable.
-
-
-
//Tag.expire_cached_tags()//
-Returns: nothing
-
-Comments:
-This method removes from memcache any objects that have been cached by other [[Tag|Tag Class]] methods.  Usually, the programmer will not call this method directly, as it is used behind-the-scenes by code that affects the validity of the cached items.  It is available, however, in case the programmer wishes to customize existing behavior.
-
-
-
//Tag.get_by_name(tag_name)//
-*tag_name - the tag, in text form
-Returns: a [[Tag|Tag Class]] or //None//
-
-Example:
-{{{
-class SearchByTag(RequestHandler):
-    def get(self):
-        from post import Post
-        from taggable import Tag
-        
-        requested_tag = self.request.get('tag')
-        if requested_tag is not None and len(requested_tag) > 0:
-            tag = Tag.get_by_name(requested_tag)
-            posts = None
-            if tag is not None:
-                posts = Post.get(tag.tagged)
-
-            self.template_values['posts'] = posts
-}}}
-
-
-
-
//Tag.get_or_create(tag_name)//
-*tag_name - the name of the tag that is to be retrieved from or created in the datastore
-Returns: a [[Tag|Tag Class]]
-
-Example:
-{{{
-for each_tag in tags:
-    each_tag = string.strip(each_tag)
-    if len(each_tag) > 0 and each_tag not in self.__tags:
-        # A tag that was not previously assigned to this entity
-        # is present in the list that is being assigned, so we
-        # associate this entity with the tag.
-        tag = Tag.get_or_create(each_tag)
-        tag.add_tagged(self.key())
-        self.__tags.append(tag)
-}}}
-
-
-
//Tag.get_tags_by_frequency(limit=1000)//
-*limit - the number of records to return; the maximum is 1000
-Returns: a list of [[Tag|Tag Class]] objects, ordered by the number of //Taggable// objects assigned to it.
-
-Example:
-{{{
-@classmethod
-def popular_tags(cls, limit=5):
-    from google.appengine.api import memcache
-        
-    tags = memcache.get('popular_tags')
-    if tags is None:
-        tags = Tag.get_tags_by_frequency(limit)
-        memcache.add('popular_tags', tags, 3600)
-        
-    return tags
-}}}
-
-
-
//Tag.get_tags_by_name(tag_name)//
-*tag_name - the string value of a [[Tag|Tag Class]] to be retrieved from the datastore
-Returns: a [[Tag|Tag Class]] object or None if the given //tag_name// does not exist
-
-Example:
-{{{
-requested_tag = self.request.get('tag')
-if requested_tag is not None and len(requested_tag) > 0:
-    tag = Tag.get_by_name(requested_tag)
-    posts = None
-    if tag is not None:
-        posts = Post.get(tag.tagged)
-
-    self.template_values['posts'] = posts
-}}}
-
-
-
//Tag.get_tags_for_key(key)//
-*key - a [[Key|Key Class]] for a //Taggable// object.
-Returns: a //list// of [[Tag|Tag Class]] objects
-
-Example:
-{{{
-def __get_tags(self):
-    "Get a List of Tag objects for all Tags that apply to this object."
-    if self.__tags is None or len(self.__tags) == 0:
-        self.__tags = Tag.get_tags_for_key(self.key())
-    return self.__tags
-}}}
-
-
-
//Tag.popular_tags(limit=5)//
-*limit - the number of records to return; the default is 5 and the maximum is 1000
-Returns: a list of [[Tag|Tag Class]] objects, ordered by the number of [[Taggable|Taggable Class]] objects assigned to it.
-
-Comments:
-This method is a thin wrapper around [[get_tags_by_frequency|Tag get_tags_by_frequency]] that caches the returned values.
-
-Example:
-{{{
-self.template_values['popular_tags'] = Tag.popular_tags()
-}}}
-
-
-
//tag_instance.remove_tagged(key)//
-* key - a [[Key|Key Class]] for a //Taggable// object.
-Returns: nothing
-
-The //remove_tagged// method removes the [[Key|Key Class]] passed in from the Tag's [[tagged|Tag tagged Property]] collection and decrements the [[tagged_count|Tag tagged_count Property]].  The operations are performed inside a transaction to ensure data intergity.
-
-Under most circumstances, the programmer will not need to call //remove_tagged// directly; it is meant to be used internally by the [[Tagged Class]].  However, it is available for use by those who wish to customize the behavior of Taggable.
-
-
-
A StringProperty that holds the name or value of the tag.
-
-
-
The DateTimeProperty that records that date and time that the [[Tag|TagClass]] was first added.
-
-
-
A ListProperty that holds the Keys of all of the entities that have the given [[Tag|TagClass]] assigned to them.
-
-
-
An IntegerProperty that holds the number of items that have the Tag assigned to them.
-
-
-
The //Taggable// class can be mixed-in to any other Python class that inherits from //db.Model// in order to associate [[Tags|Tag Definition]] with that model.  For instance, if you were creating blogging software, one of your fundamental objects would be a //Post// that would comprise all of the information about an individual entry in your blog.  Usually, a Post can have [[Tags|Tag Definition]] added to it in order to provide easily-digested information to the reader about the content.
-
-It has two properties:
-*[[tags|Taggable tags Property]]
-*[[tag_separator|Taggable tag_separator Property]]
-
-and one method:
-*[[tags_string|Taggable tags_string Method]]
-
-Example:
-{{{
-class Post(Taggable, db.Model):
-    index = db.IntegerProperty(required=True, default=0)
-    body = db.TextProperty(required = True)
-    title = db.StringProperty()
-    added = db.DateTimeProperty(auto_now_add=True)
-    added_month = db.IntegerProperty()
-    added_year = db.IntegerProperty()
-    edited = db.DateTimeProperty()
-        
-    def __init__(self, parent=None, key_name=None, app=None, **entity_values):
-        db.Model.__init__(self, parent, key_name, app, **entity_values)
-        Taggable.__init__(self)
-}}}
-Notes:
-*Notice that [[Taggable|Taggable Class]] is placed before db.Model in the inheritance list.  While this is not strictly required for //Taggable-mixin// to function correctly, it //is// required for other mixin classes, and it is generally a good practice.
-*It //is// required that the [[Taggable|Taggable Class]] //init// method is explicitly called, so it should be placed in the inherting class's //init// method.  If the class would not otherwise have one, it should be created, and the db.Model //init// must be called as well, as shown.
-
-
-
The //tag_separator// property is the string that is used to separate individual tags in a string representation of a list of tags.  It is used by the [[tags property|TaggableTagsProperty]] to parse a string that may contain one or more tags to be applied to a [[Taggable|TaggableClass]] object.  It is also used by the [[tags_string|TaggableTagsStringMethod]] method to construct a string representation of the tags for a [[Taggable|TaggableClass]] object.
-
-*By default, it is set to a comma (','), but it can be programatically-set to whatever value the developer desires
-*It is probably best to avoid using whitespace characters, as that would prevent users from entering multi-word tags
-*Custom separator values can be set at different levels.
-**To set one value for all [[Taggable|TaggableClass]] objects, modify the //init// method in the //Taggable// class in the taggable.py file:
-{{{
-class Taggable:
-     def __init__(self):
-        self.tag_separator = ";" # Made it semi-colon instead of comma
-}}}
-**To set one value for all instances of a particular [[Taggable|TaggableClass]] class, modify the //init// method of that class:
-{{{
-class Post(Taggable, db.Model):
-    body = db.TextProperty(required = True)
-    title = db.StringProperty()
-    added = db.DateTimeProperty(auto_now_add=True)
-    edited = db.DateTimeProperty()
-            
-    def __init__(self, parent=None, key_name=None, app=None, **entity_values):
-        db.Model.__init__(self, parent, key_name, app, **entity_values)
-        Taggable.__init__(self)
-        self.tag_separator = ";" # Made it semi-colon instead of comma        
-}}}
-**To set a value for one particular instance of a [[Taggable|TaggableClass]] class, set the value after creating the instance:
-{{{
-newpost = Post(title = self.request.get('title'), body = self.request.get('body'))
-newpost.tag_separator = ";" # Made it semi-colon instead of comma
-newpost.set_tags_from_string("foo; bar; bletch; this is a tag; tags rule"     
-}}}
-
-
-
The a [[Taggable|Taggable Class]] class's //tags// property is used to assign [[Tags|Tag Class]] to the [[Taggable|Taggable Class]] instance or retrieve a //list// of those already assigned.
-
-Assignment Example:
-{{{
-class Post(Taggable, db.Model):
-    index = db.IntegerProperty(required=True, default=0)
-    body = db.TextProperty(required = True)
-    title = db.StringProperty()
-    added = db.DateTimeProperty(auto_now_add=True)
-    added_month = db.IntegerProperty()
-    added_year = db.IntegerProperty()
-    edited = db.DateTimeProperty()
-        
-    def __init__(self, parent=None, key_name=None, app=None, **entity_values):
-        db.Model.__init__(self, parent, key_name, app, **entity_values)
-        Taggable.__init__(self)
-
-    @classmethod
-    def new_post(cls, new_title=None, new_body=None, new_tags=[]):
-        if new_title is not None and new_body is not None:
-            
-            new_post = Post(title = new_title, body = new_body)
-            new_post.put()
-        
-            new_post.tags = new_tags
-            new_post.save()
-        else:
-            raise Exception("Must supply both new_title and new_body when creating a new Post.")
-}}}
-
-Retrieval Example:
-{{{
-{% if post.tags %}
-    <div class="posttags">tags:&nbsp;
-        {% for each_tag in post.tags %}
-            <a href="/searchbytag?tag={{ each_tag.tag|escape }}">{{ each_tag.tag }}</a>{% if forloop.last %}{% else %}, {% endif %}
-        {% endfor %}
-    </div>
-{% endif %}
-}}}
-
-
-
//taggable_instance.tags_string()//
-Returns: a string representation of the tags assigned to the [[Taggable|Taggable Class]] instance.
-
-This method simply joins the string versions of each [[Tag|Tag Class]] in the [[Taggable|Taggable Class]] class's //list//, placing the value stored in [[tag_separator|Taggable tag_separator Property]] inbetween each element.
-
-
-
<<tagging api>>
-
-
-
The //get_tags_as_string// method creates a string representation of all of the tags that apply to a ''Taggable'' object.
-
-
-
The //set_tags// method sets the tags for a ''Taggable'' object from a list of strings:
-{{{
-tag_list = ["foo", "bar", "bletch","this is a tag", "tags rule"]
-my_taggable_object.set_tags(tag_list)
-}}}
-
-
-
The //set_tags_from_string method// sets the tags for a Taggable object from a string that contains one or more tags separated by the character or characters in //self.tag_seperator//:
-{{{
-tag_list = "foo, bar, bletch, this is a tag, tags rule"
-my_taggable_object.set_tags_from_string(tag_list)
-}}}
-
-By default, [[tag_separator|tag_separator]] is set to a comma (','), but it can be anything that the programmer wants.
-
-
-
All of the code for the ''Taggable'' mixin class lives in the file taggable.py.  Simply copy this file in to your Google ~AppEngine main directory, and it should be available to your code.
-
-
- - - - - - - - - - diff -r a525a55833f1 -r 0ede2f3adbc1 app/taggable-mixin/taggable.py --- a/app/taggable-mixin/taggable.py Fri Jul 24 21:00:04 2009 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,202 +0,0 @@ -from google.appengine.ext import db -import string - -class Tag(db.Model): - "Google AppEngine model for store of tags." - - tag = db.StringProperty(required=True) - "The actual string value of the tag." - - added = db.DateTimeProperty(auto_now_add=True) - "The date and time that the tag was first added to the datastore." - - tagged = db.ListProperty(db.Key) - "A List of db.Key values for the datastore objects that have been tagged with this tag value." - - tagged_count = db.IntegerProperty(default=0) - "The number of entities in tagged." - - @classmethod - def __key_name(cls, tag_name): - return cls.__name__ + '_' + tag_name - - def remove_tagged(self, key): - def remove_tagged_txn(): - if key in self.tagged: - self.tagged.remove(key) - self.tagged_count -= 1 - self.put() - db.run_in_transaction(remove_tagged_txn) - self.__class__.expire_cached_tags() - - def add_tagged(self, key): - def add_tagged_txn(): - if key not in self.tagged: - self.tagged.append(key) - self.tagged_count += 1 - self.put() - db.run_in_transaction(add_tagged_txn) - self.__class__.expire_cached_tags() - - def clear_tagged(self): - def clear_tagged_txn(): - self.tagged = [] - self.tagged_count = 0 - self.put() - db.run_in_transaction(clear_tagged_txn) - self.__class__.expire_cached_tags() - - @classmethod - def get_by_name(cls, tag_name): - return cls.get_by_key_name(cls.__key_name(tag_name)) - - @classmethod - def get_tags_for_key(cls, key): - "Set the tags for the datastore object represented by key." - tags = db.Query(cls).filter('tagged =', key).fetch(1000) - return tags - - @classmethod - def get_or_create(cls, tag_name): - "Get the Tag object that has the tag value given by tag_value." - tag_key_name = cls.__key_name(tag_name) - existing_tag = cls.get_by_key_name(tag_key_name) - if existing_tag is None: - # The tag does not yet exist, so create it. - def create_tag_txn(): - new_tag = cls(key_name=tag_key_name, tag=tag_name) - new_tag.put() - return new_tag - existing_tag = db.run_in_transaction(create_tag_txn) - return existing_tag - - @classmethod - def get_tags_by_frequency(cls, limit=1000): - """Return a list of Tags sorted by the number of objects to which they have been applied, - most frequently-used first. If limit is given, return only that many tags; otherwise, - return all.""" - tag_list = db.Query(cls).filter('tagged_count >', 0).order("-tagged_count").fetch(limit) - - return tag_list - - @classmethod - def get_tags_by_name(cls, limit=1000, ascending=True): - """Return a list of Tags sorted alphabetically by the name of the tag. - If a limit is given, return only that many tags; otherwise, return all. - If ascending is True, sort from a-z; otherwise, sort from z-a.""" - - from google.appengine.api import memcache - - cache_name = cls.__name__ + '_tags_by_name' - if ascending: - cache_name += '_asc' - else: - cache_name += '_desc' - - tags = memcache.get(cache_name) - if tags is None or len(tags) < limit: - order_by = "tag" - if not ascending: - order_by = "-tag" - - tags = db.Query(cls).order(order_by).fetch(limit) - memcache.add(cache_name, tags, 3600) - else: - if len(tags) > limit: - # Return only as many as requested. - tags = tags[:limit] - - return tags - - - @classmethod - def popular_tags(cls, limit=5): - from google.appengine.api import memcache - - tags = memcache.get(cls.__name__ + '_popular_tags') - if tags is None: - tags = cls.get_tags_by_frequency(limit) - memcache.add(cls.__name__ + '_popular_tags', tags, 3600) - - return tags - - @classmethod - def expire_cached_tags(cls): - from google.appengine.api import memcache - - memcache.delete(cls.__name__ + '_popular_tags') - memcache.delete(cls.__name__ + '_tags_by_name_asc') - memcache.delete(cls.__name__ + '_tags_by_name_desc') - -class Taggable: - """A mixin class that is used for making Google AppEngine Model classes taggable. - Usage: - class Post(db.Model, taggable.Taggable): - body = db.TextProperty(required = True) - title = db.StringProperty() - added = db.DateTimeProperty(auto_now_add=True) - edited = db.DateTimeProperty() - - def __init__(self, parent=None, key_name=None, app=None, **entity_values): - db.Model.__init__(self, parent, key_name, app, **entity_values) - taggable.Taggable.__init__(self) - """ - - def __init__(self, tag_model = Tag): - self.__tags = None - self.__tag_model = tag_model - self.tag_separator = "," - """The string that is used to separate individual tags in a string - representation of a list of tags. Used by tags_string() to join the tags - into a string representation and tags setter to split a string into - individual tags.""" - - def __get_tags(self): - "Get a List of Tag objects for all Tags that apply to this object." - if self.__tags is None or len(self.__tags) == 0: - self.__tags = self.__tag_model.get_tags_for_key(self.key()) - return self.__tags - - def __set_tags(self, tags): - import types - if type(tags) is types.UnicodeType: - # Convert unicode to a plain string - tags = str(tags) - if type(tags) is types.StringType: - # Tags is a string, split it on tag_seperator into a list - tags = string.split(tags, self.tag_separator) - if type(tags) is types.ListType: - self.__get_tags() - # Firstly, we will check to see if any tags have been removed. - # Iterate over a copy of __tags, as we may need to modify __tags - for each_tag in self.__tags[:]: - if each_tag not in tags: - # A tag that was previously assigned to this entity is - # missing in the list that is being assigned, so we - # disassocaite this entity and the tag. - each_tag.remove_tagged(self.key()) - self.__tags.remove(each_tag) - # Secondly, we will check to see if any tags have been added. - for each_tag in tags: - each_tag = string.strip(each_tag) - if len(each_tag) > 0 and each_tag not in self.__tags: - # A tag that was not previously assigned to this entity - # is present in the list that is being assigned, so we - # associate this entity with the tag. - tag = self.__tag_model.get_or_create(each_tag) - tag.add_tagged(self.key()) - self.__tags.append(tag) - else: - raise Exception, "tags must be either a unicode, a string or a list" - - tags = property(__get_tags, __set_tags, None, None) - - def tags_string(self): - "Create a formatted string version of this entity's tags" - to_str = "" - for each_tag in self.tags: - to_str += each_tag.tag - if each_tag != self.tags[-1]: - to_str += self.tag_separator - return to_str - diff -r a525a55833f1 -r 0ede2f3adbc1 app/taggable-mixin/taggable_tests.py --- a/app/taggable-mixin/taggable_tests.py Fri Jul 24 21:00:04 2009 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,243 +0,0 @@ -#!/usr/bin/env python - -#Copyright 2008 Adam A. Crossland -# -#Licensed under the Apache License, Version 2.0 (the "License"); -#you may not use this file except in compliance with the License. -#You may obtain a copy of the License at -# -#http://www.apache.org/licenses/LICENSE-2.0 -# -#Unless required by applicable law or agreed to in writing, software -#distributed under the License is distributed on an "AS IS" BASIS, -#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -#See the License for the specific language governing permissions and -#limitations under the License. - -import sys -import os.path - -APPENGINE_PATH = '../../thirdparty/google_appengine' - -# Add app-engine related libraries to your path -paths = [ - APPENGINE_PATH, - os.path.join(APPENGINE_PATH, 'lib', 'django'), - os.path.join(APPENGINE_PATH, 'lib', 'webob'), - os.path.join(APPENGINE_PATH, 'lib', 'yaml', 'lib') -] -for path in paths: - if not os.path.exists(path): - raise 'Path does not exist: %s' % path -sys.path = paths + sys.path - -import unittest -from google.appengine.api import apiproxy_stub_map -from google.appengine.api import datastore_file_stub -from google.appengine.api import mail_stub -from google.appengine.api import user_service_stub -from google.appengine.ext import webapp -from google.appengine.ext import db -from google.appengine.api.memcache import memcache_stub -from taggable import * - -APP_ID = u'taggable' -AUTH_DOMAIN = 'gmail.com' -LOGGED_IN_USER = 'me@example.com' # set to '' for no logged in user - -BLOG_NAME='test_blog' - -class BlogIndex(db.Model): - "A global counter used to provide the index of the next blog post." - index = db.IntegerProperty(required=True, default=0) - "The next available index for a Post." - -class Post(Taggable, db.Model): - index = db.IntegerProperty(required=True, default=0) - body = db.TextProperty(required = True) - title = db.StringProperty() - added = db.DateTimeProperty(auto_now_add=True) - added_month = db.IntegerProperty() - added_year = db.IntegerProperty() - edited = db.DateTimeProperty() - - def __init__(self, parent=None, key_name=None, app=None, **entity_values): - db.Model.__init__(self, parent, key_name, app, **entity_values) - Taggable.__init__(self) - - def get_all_posts(): - return db.GqlQuery("SELECT * from Post ORDER BY added DESC") - Get_All_Posts = staticmethod(get_all_posts) - - @classmethod - def get_posts(cls, start_index=0, count=10): - start_index = int(start_index) # Just make sure that we have an int - posts = Post.gql('WHERE index <= :1 ORDER BY index DESC', start_index).fetch(count + 1) - if len(posts) > count: - posts = posts[:count] - - return posts - - @classmethod - def new_post(cls, new_title=None, new_body=None, new_tags=[]): - new_post = None - if new_title is not None and new_body is not None: - def txn(): - blog_index = BlogIndex.get_by_key_name(BLOG_NAME) - if blog_index is None: - blog_index = BlogIndex(key_name=BLOG_NAME) - new_index = blog_index.index - blog_index.index += 1 - blog_index.put() - - new_post_key_name = BLOG_NAME + str(new_index) - new_post = Post(key_name=new_post_key_name, parent=blog_index, - index = new_index, title = new_title, - body = new_body) - new_post.put() - - return new_post - new_post = db.run_in_transaction(txn) - - new_post.tags = new_tags - new_post.put() - else: - raise Exception("Must supply both new_title and new_body when creating a new Post.") - - return new_post - - def delete(self): - # Perform any actions that are required to maintain data integrity - # when this Post is delete. - # Disassociate this Post from any Tag - self.set_tags([]) - - # Finally, call the real delete - db.Model.delete(self) - -class MyTest(unittest.TestCase): - - def setUp(self): - # Start with a fresh api proxy. - apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap() - - # Use a fresh stub datastore. - stub = datastore_file_stub.DatastoreFileStub(APP_ID, '/dev/null', '/dev/null') - apiproxy_stub_map.apiproxy.RegisterStub('datastore_v3', stub) - - # Use a fresh memcache stub. - apiproxy_stub_map.apiproxy.RegisterStub('memcache', memcache_stub.MemcacheServiceStub()) - - # Use a fresh stub UserService. - apiproxy_stub_map.apiproxy.RegisterStub( - 'user', user_service_stub.UserServiceStub()) - os.environ['AUTH_DOMAIN'] = AUTH_DOMAIN - os.environ['USER_EMAIL'] = LOGGED_IN_USER - os.environ['APPLICATION_ID'] = APP_ID - - def testSimpleTagAdding(self): - new_post = Post.new_post(new_title='test post 1', new_body='This is a test post. Please ignore.') - assert new_post is not None - - new_post.tags = "test, testing, tests" - self.assertEqual(len(new_post.tags), 3) - - def testComplexTagAdding(self): - new_post = Post.new_post(new_title='test post 1', new_body='This is a test post. Please ignore.') - assert new_post is not None - - new_post.tags = " test, testing, tests,,,tag with spaces" - self.assertEqual(len(new_post.tags), 4) - - tag = new_post.tags[3] - assert tag is not None - self.assertEqual(tag.tag, 'tag with spaces') - self.assertEqual(tag.tagged_count, 1) - - tag2 = Tag.get_by_name('tag with spaces') - assert tag2 is not None - self.assertEqual(tag.tag, 'tag with spaces') - self.assertEqual(tag.tagged_count, 1) - - def testTagDeletion(self): - new_post = Post.new_post(new_title='test post 2', new_body='This is a test post. Please continue to ignore.') - assert new_post is not None - - new_post.tags = "test, testing, tests" - self.assertEqual(len(new_post.tags), 3) - - new_post.tags = "test" - self.assertEqual(len(new_post.tags), 1) - - def testTagCounts(self): - new_post3 = Post.new_post(new_title='test post 3', new_body='This is a test post. Please continue to ignore.') - assert new_post3 is not None - new_post3.tags = "foo, bar, baz" - new_post4 = Post.new_post(new_title='test post 4', new_body='This is a test post. Please continue to ignore.') - assert new_post4 is not None - new_post4.tags = "bar, baz, bletch" - new_post5 = Post.new_post(new_title='test post 5', new_body='This is a test post. Please continue to ignore.') - assert new_post5 is not None - new_post5.tags = "baz, bletch, quux" - - foo_tag = Tag.get_by_name('foo') - assert foo_tag is not None - self.assertEqual(foo_tag.tagged_count, 1) - - bar_tag = Tag.get_by_name('bar') - assert bar_tag is not None - self.assertEqual(bar_tag.tagged_count, 2) - - baz_tag = Tag.get_by_name('baz') - assert baz_tag is not None - self.assertEqual(baz_tag.tagged_count, 3) - - bletch_tag = Tag.get_by_name('bletch') - assert bletch_tag is not None - self.assertEqual(bletch_tag.tagged_count, 2) - - quux_tag = Tag.get_by_name('quux') - assert quux_tag is not None - self.assertEqual(quux_tag.tagged_count, 1) - - new_post3.tags = 'bar, baz' - foo_tag = Tag.get_by_name('foo') - assert foo_tag is not None - self.assertEqual(len(new_post3.tags), 2) - self.assertEqual(foo_tag.tagged_count, 0) - - def testTagGetTagsForKey(self): - new_post = Post.new_post(new_title='test post 6', new_body='This is a test post. Please continue to ignore.', new_tags='foo,bar,bletch,quux') - assert new_post is not None - - tags = Tag.get_tags_for_key(new_post.key()) - assert tags is not None - self.assertEqual(type(tags), type([])) - self.assertEqual(len(tags), 4) - - def testTagGetByName(self): - new_post = Post.new_post(new_title='test post 6', new_body='This is a test post. Please continue to ignore.', new_tags='foo,bar,bletch,quux') - assert new_post is not None - - quux_tag = Tag.get_by_name('quux') - assert quux_tag is not None - - zizzle_tag = Tag.get_by_name('zizzle') - assert zizzle_tag is None - - def testTagsString(self): - new_post = Post.new_post(new_title='test post 6', new_body='This is a test post. Please continue to ignore.', new_tags=' pal,poll ,,pip,pony') - assert new_post is not None - self.assertEqual(new_post.tags_string(), "pal,poll,pip,pony") - new_post.tag_separator = "|" - self.assertEqual(new_post.tags_string(), "pal|poll|pip|pony") - new_post.tag_separator = " , " - self.assertEqual(new_post.tags_string(), "pal , poll , pip , pony") - - new_post.tag_separator = ", " - new_post.tags = "pal, pill, pip" - self.assertEqual(len(new_post.tags), 3) - self.assertEqual(new_post.tags_string(), "pal, pill, pip") - -if __name__ == '__main__': - unittest.main() diff -r a525a55833f1 -r 0ede2f3adbc1 app/taggable/COPYING --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/app/taggable/COPYING Sat Jul 25 01:09:46 2009 +0530 @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff -r a525a55833f1 -r 0ede2f3adbc1 app/taggable/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/app/taggable/__init__.py Sat Jul 25 01:09:46 2009 +0530 @@ -0,0 +1,19 @@ +# +# Copyright 2008 the Adam Crossland and Melange authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""This is a module which contains tags framework used in Melange and +it is based on taggable-mixin. +""" + diff -r a525a55833f1 -r 0ede2f3adbc1 app/taggable/taggable.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/app/taggable/taggable.html Sat Jul 25 01:09:46 2009 +0530 @@ -0,0 +1,9732 @@ + + + + + + + + + + + + Taggable - a portable mixin class for adding Tags to Google AppEngine Models + + + + + + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
<!--{{{-->
+<link rel='alternate' type='application/rss+xml' title='RSS' href='index.xml'/>
+<!--}}}-->
+
+
+
Background: #fff
+Foreground: #000
+PrimaryPale: #8cf
+PrimaryLight: #18f
+PrimaryMid: #04b
+PrimaryDark: #014
+SecondaryPale: #ffc
+SecondaryLight: #fe8
+SecondaryMid: #db4
+SecondaryDark: #841
+TertiaryPale: #eee
+TertiaryLight: #ccc
+TertiaryMid: #999
+TertiaryDark: #666
+Error: #f88
+
+
+
/*{{{*/
+body {background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];}
+
+a {color:[[ColorPalette::PrimaryMid]];}
+a:hover {background-color:[[ColorPalette::PrimaryMid]]; color:[[ColorPalette::Background]];}
+a img {border:0;}
+
+h1,h2,h3,h4,h5,h6 {color:[[ColorPalette::SecondaryDark]]; background:transparent;}
+h1 {border-bottom:2px solid [[ColorPalette::TertiaryLight]];}
+h2,h3 {border-bottom:1px solid [[ColorPalette::TertiaryLight]];}
+
+.button {color:[[ColorPalette::PrimaryDark]]; border:1px solid [[ColorPalette::Background]];}
+.button:hover {color:[[ColorPalette::PrimaryDark]]; background:[[ColorPalette::SecondaryLight]]; border-color:[[ColorPalette::SecondaryMid]];}
+.button:active {color:[[ColorPalette::Background]]; background:[[ColorPalette::SecondaryMid]]; border:1px solid [[ColorPalette::SecondaryDark]];}
+
+.header {background:[[ColorPalette::PrimaryMid]];}
+.headerShadow {color:[[ColorPalette::Foreground]];}
+.headerShadow a {font-weight:normal; color:[[ColorPalette::Foreground]];}
+.headerForeground {color:[[ColorPalette::Background]];}
+.headerForeground a {font-weight:normal; color:[[ColorPalette::PrimaryPale]];}
+
+.tabSelected{color:[[ColorPalette::PrimaryDark]];
+	background:[[ColorPalette::TertiaryPale]];
+	border-left:1px solid [[ColorPalette::TertiaryLight]];
+	border-top:1px solid [[ColorPalette::TertiaryLight]];
+	border-right:1px solid [[ColorPalette::TertiaryLight]];
+}
+.tabUnselected {color:[[ColorPalette::Background]]; background:[[ColorPalette::TertiaryMid]];}
+.tabContents {color:[[ColorPalette::PrimaryDark]]; background:[[ColorPalette::TertiaryPale]]; border:1px solid [[ColorPalette::TertiaryLight]];}
+.tabContents .button {border:0;}
+
+#sidebar {}
+#sidebarOptions input {border:1px solid [[ColorPalette::PrimaryMid]];}
+#sidebarOptions .sliderPanel {background:[[ColorPalette::PrimaryPale]];}
+#sidebarOptions .sliderPanel a {border:none;color:[[ColorPalette::PrimaryMid]];}
+#sidebarOptions .sliderPanel a:hover {color:[[ColorPalette::Background]]; background:[[ColorPalette::PrimaryMid]];}
+#sidebarOptions .sliderPanel a:active {color:[[ColorPalette::PrimaryMid]]; background:[[ColorPalette::Background]];}
+
+.wizard {background:[[ColorPalette::PrimaryPale]]; border:1px solid [[ColorPalette::PrimaryMid]];}
+.wizard h1 {color:[[ColorPalette::PrimaryDark]]; border:none;}
+.wizard h2 {color:[[ColorPalette::Foreground]]; border:none;}
+.wizardStep {background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];
+	border:1px solid [[ColorPalette::PrimaryMid]];}
+.wizardStep.wizardStepDone {background:[[ColorPalette::TertiaryLight]];}
+.wizardFooter {background:[[ColorPalette::PrimaryPale]];}
+.wizardFooter .status {background:[[ColorPalette::PrimaryDark]]; color:[[ColorPalette::Background]];}
+.wizard .button {color:[[ColorPalette::Foreground]]; background:[[ColorPalette::SecondaryLight]]; border: 1px solid;
+	border-color:[[ColorPalette::SecondaryPale]] [[ColorPalette::SecondaryDark]] [[ColorPalette::SecondaryDark]] [[ColorPalette::SecondaryPale]];}
+.wizard .button:hover {color:[[ColorPalette::Foreground]]; background:[[ColorPalette::Background]];}
+.wizard .button:active {color:[[ColorPalette::Background]]; background:[[ColorPalette::Foreground]]; border: 1px solid;
+	border-color:[[ColorPalette::PrimaryDark]] [[ColorPalette::PrimaryPale]] [[ColorPalette::PrimaryPale]] [[ColorPalette::PrimaryDark]];}
+
+#messageArea {border:1px solid [[ColorPalette::SecondaryMid]]; background:[[ColorPalette::SecondaryLight]]; color:[[ColorPalette::Foreground]];}
+#messageArea .button {color:[[ColorPalette::PrimaryMid]]; background:[[ColorPalette::SecondaryPale]]; border:none;}
+
+.popupTiddler {background:[[ColorPalette::TertiaryPale]]; border:2px solid [[ColorPalette::TertiaryMid]];}
+
+.popup {background:[[ColorPalette::TertiaryPale]]; color:[[ColorPalette::TertiaryDark]]; border-left:1px solid [[ColorPalette::TertiaryMid]]; border-top:1px solid [[ColorPalette::TertiaryMid]]; border-right:2px solid [[ColorPalette::TertiaryDark]]; border-bottom:2px solid [[ColorPalette::TertiaryDark]];}
+.popup hr {color:[[ColorPalette::PrimaryDark]]; background:[[ColorPalette::PrimaryDark]]; border-bottom:1px;}
+.popup li.disabled {color:[[ColorPalette::TertiaryMid]];}
+.popup li a, .popup li a:visited {color:[[ColorPalette::Foreground]]; border: none;}
+.popup li a:hover {background:[[ColorPalette::SecondaryLight]]; color:[[ColorPalette::Foreground]]; border: none;}
+.popup li a:active {background:[[ColorPalette::SecondaryPale]]; color:[[ColorPalette::Foreground]]; border: none;}
+.popupHighlight {background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];}
+.listBreak div {border-bottom:1px solid [[ColorPalette::TertiaryDark]];}
+
+.tiddler .defaultCommand {font-weight:bold;}
+
+.shadow .title {color:[[ColorPalette::TertiaryDark]];}
+
+.title {color:[[ColorPalette::SecondaryDark]];}
+.subtitle {color:[[ColorPalette::TertiaryDark]];}
+
+.toolbar {color:[[ColorPalette::PrimaryMid]];}
+.toolbar a {color:[[ColorPalette::TertiaryLight]];}
+.selected .toolbar a {color:[[ColorPalette::TertiaryMid]];}
+.selected .toolbar a:hover {color:[[ColorPalette::Foreground]];}
+
+.tagging, .tagged {border:1px solid [[ColorPalette::TertiaryPale]]; background-color:[[ColorPalette::TertiaryPale]];}
+.selected .tagging, .selected .tagged {background-color:[[ColorPalette::TertiaryLight]]; border:1px solid [[ColorPalette::TertiaryMid]];}
+.tagging .listTitle, .tagged .listTitle {color:[[ColorPalette::PrimaryDark]];}
+.tagging .button, .tagged .button {border:none;}
+
+.footer {color:[[ColorPalette::TertiaryLight]];}
+.selected .footer {color:[[ColorPalette::TertiaryMid]];}
+
+.sparkline {background:[[ColorPalette::PrimaryPale]]; border:0;}
+.sparktick {background:[[ColorPalette::PrimaryDark]];}
+
+.error, .errorButton {color:[[ColorPalette::Foreground]]; background:[[ColorPalette::Error]];}
+.warning {color:[[ColorPalette::Foreground]]; background:[[ColorPalette::SecondaryPale]];}
+.lowlight {background:[[ColorPalette::TertiaryLight]];}
+
+.zoomer {background:none; color:[[ColorPalette::TertiaryMid]]; border:3px solid [[ColorPalette::TertiaryMid]];}
+
+.imageLink, #displayArea .imageLink {background:transparent;}
+
+.annotation {background:[[ColorPalette::SecondaryLight]]; color:[[ColorPalette::Foreground]]; border:2px solid [[ColorPalette::SecondaryMid]];}
+
+.viewer .listTitle {list-style-type:none; margin-left:-2em;}
+.viewer .button {border:1px solid [[ColorPalette::SecondaryMid]];}
+.viewer blockquote {border-left:3px solid [[ColorPalette::TertiaryDark]];}
+
+.viewer table, table.twtable {border:2px solid [[ColorPalette::TertiaryDark]];}
+.viewer th, .viewer thead td, .twtable th, .twtable thead td {background:[[ColorPalette::SecondaryMid]]; border:1px solid [[ColorPalette::TertiaryDark]]; color:[[ColorPalette::Background]];}
+.viewer td, .viewer tr, .twtable td, .twtable tr {border:1px solid [[ColorPalette::TertiaryDark]];}
+
+.viewer pre {border:1px solid [[ColorPalette::SecondaryLight]]; background:[[ColorPalette::SecondaryPale]];}
+.viewer code {color:[[ColorPalette::SecondaryDark]];}
+.viewer hr {border:0; border-top:dashed 1px [[ColorPalette::TertiaryDark]]; color:[[ColorPalette::TertiaryDark]];}
+
+.highlight, .marked {background:[[ColorPalette::SecondaryLight]];}
+
+.editor input {border:1px solid [[ColorPalette::PrimaryMid]];}
+.editor textarea {border:1px solid [[ColorPalette::PrimaryMid]]; width:100%;}
+.editorFooter {color:[[ColorPalette::TertiaryMid]];}
+
+#backstageArea {background:[[ColorPalette::Foreground]]; color:[[ColorPalette::TertiaryMid]];}
+#backstageArea a {background:[[ColorPalette::Foreground]]; color:[[ColorPalette::Background]]; border:none;}
+#backstageArea a:hover {background:[[ColorPalette::SecondaryLight]]; color:[[ColorPalette::Foreground]]; }
+#backstageArea a.backstageSelTab {background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];}
+#backstageButton a {background:none; color:[[ColorPalette::Background]]; border:none;}
+#backstageButton a:hover {background:[[ColorPalette::Foreground]]; color:[[ColorPalette::Background]]; border:none;}
+#backstagePanel {background:[[ColorPalette::Background]]; border-color: [[ColorPalette::Background]] [[ColorPalette::TertiaryDark]] [[ColorPalette::TertiaryDark]] [[ColorPalette::TertiaryDark]];}
+.backstagePanelFooter .button {border:none; color:[[ColorPalette::Background]];}
+.backstagePanelFooter .button:hover {color:[[ColorPalette::Foreground]];}
+#backstageCloak {background:[[ColorPalette::Foreground]]; opacity:0.6; filter:'alpha(opacity:60)';}
+/*}}}*/
+
+
+
/*{{{*/
+* html .tiddler {height:1%;}
+
+body {font-size:.75em; font-family:arial,helvetica; margin:0; padding:0;}
+
+h1,h2,h3,h4,h5,h6 {font-weight:bold; text-decoration:none;}
+h1,h2,h3 {padding-bottom:1px; margin-top:1.2em;margin-bottom:0.3em;}
+h4,h5,h6 {margin-top:1em;}
+h1 {font-size:1.35em;}
+h2 {font-size:1.25em;}
+h3 {font-size:1.1em;}
+h4 {font-size:1em;}
+h5 {font-size:.9em;}
+
+hr {height:1px;}
+
+a {text-decoration:none;}
+
+dt {font-weight:bold;}
+
+ol {list-style-type:decimal;}
+ol ol {list-style-type:lower-alpha;}
+ol ol ol {list-style-type:lower-roman;}
+ol ol ol ol {list-style-type:decimal;}
+ol ol ol ol ol {list-style-type:lower-alpha;}
+ol ol ol ol ol ol {list-style-type:lower-roman;}
+ol ol ol ol ol ol ol {list-style-type:decimal;}
+
+.txtOptionInput {width:11em;}
+
+#contentWrapper .chkOptionInput {border:0;}
+
+.externalLink {text-decoration:underline;}
+
+.indent {margin-left:3em;}
+.outdent {margin-left:3em; text-indent:-3em;}
+code.escaped {white-space:nowrap;}
+
+.tiddlyLinkExisting {font-weight:bold;}
+.tiddlyLinkNonExisting {font-style:italic;}
+
+/* the 'a' is required for IE, otherwise it renders the whole tiddler in bold */
+a.tiddlyLinkNonExisting.shadow {font-weight:bold;}
+
+#mainMenu .tiddlyLinkExisting,
+	#mainMenu .tiddlyLinkNonExisting,
+	#sidebarTabs .tiddlyLinkNonExisting {font-weight:normal; font-style:normal;}
+#sidebarTabs .tiddlyLinkExisting {font-weight:bold; font-style:normal;}
+
+.header {position:relative;}
+.header a:hover {background:transparent;}
+.headerShadow {position:relative; padding:4.5em 0em 1em 1em; left:-1px; top:-1px;}
+.headerForeground {position:absolute; padding:4.5em 0em 1em 1em; left:0px; top:0px;}
+
+.siteTitle {font-size:3em;}
+.siteSubtitle {font-size:1.2em;}
+
+#mainMenu {position:absolute; left:0; width:10em; text-align:right; line-height:1.6em; padding:1.5em 0.5em 0.5em 0.5em; font-size:1.1em;}
+
+#sidebar {position:absolute; right:3px; width:16em; font-size:.9em;}
+#sidebarOptions {padding-top:0.3em;}
+#sidebarOptions a {margin:0em 0.2em; padding:0.2em 0.3em; display:block;}
+#sidebarOptions input {margin:0.4em 0.5em;}
+#sidebarOptions .sliderPanel {margin-left:1em; padding:0.5em; font-size:.85em;}
+#sidebarOptions .sliderPanel a {font-weight:bold; display:inline; padding:0;}
+#sidebarOptions .sliderPanel input {margin:0 0 .3em 0;}
+#sidebarTabs .tabContents {width:15em; overflow:hidden;}
+
+.wizard {padding:0.1em 1em 0em 2em;}
+.wizard h1 {font-size:2em; font-weight:bold; background:none; padding:0em 0em 0em 0em; margin:0.4em 0em 0.2em 0em;}
+.wizard h2 {font-size:1.2em; font-weight:bold; background:none; padding:0em 0em 0em 0em; margin:0.4em 0em 0.2em 0em;}
+.wizardStep {padding:1em 1em 1em 1em;}
+.wizard .button {margin:0.5em 0em 0em 0em; font-size:1.2em;}
+.wizardFooter {padding:0.8em 0.4em 0.8em 0em;}
+.wizardFooter .status {padding:0em 0.4em 0em 0.4em; margin-left:1em;}
+.wizard .button {padding:0.1em 0.2em 0.1em 0.2em;}
+
+#messageArea {position:fixed; top:2em; right:0em; margin:0.5em; padding:0.5em; z-index:2000; _position:absolute;}
+.messageToolbar {display:block; text-align:right; padding:0.2em 0.2em 0.2em 0.2em;}
+#messageArea a {text-decoration:underline;}
+
+.tiddlerPopupButton {padding:0.2em 0.2em 0.2em 0.2em;}
+.popupTiddler {position: absolute; z-index:300; padding:1em 1em 1em 1em; margin:0;}
+
+.popup {position:absolute; z-index:300; font-size:.9em; padding:0; list-style:none; margin:0;}
+.popup .popupMessage {padding:0.4em;}
+.popup hr {display:block; height:1px; width:auto; padding:0; margin:0.2em 0em;}
+.popup li.disabled {padding:0.4em;}
+.popup li a {display:block; padding:0.4em; font-weight:normal; cursor:pointer;}
+.listBreak {font-size:1px; line-height:1px;}
+.listBreak div {margin:2px 0;}
+
+.tabset {padding:1em 0em 0em 0.5em;}
+.tab {margin:0em 0em 0em 0.25em; padding:2px;}
+.tabContents {padding:0.5em;}
+.tabContents ul, .tabContents ol {margin:0; padding:0;}
+.txtMainTab .tabContents li {list-style:none;}
+.tabContents li.listLink { margin-left:.75em;}
+
+#contentWrapper {display:block;}
+#splashScreen {display:none;}
+
+#displayArea {margin:1em 17em 0em 14em;}
+
+.toolbar {text-align:right; font-size:.9em;}
+
+.tiddler {padding:1em 1em 0em 1em;}
+
+.missing .viewer,.missing .title {font-style:italic;}
+
+.title {font-size:1.6em; font-weight:bold;}
+
+.missing .subtitle {display:none;}
+.subtitle {font-size:1.1em;}
+
+.tiddler .button {padding:0.2em 0.4em;}
+
+.tagging {margin:0.5em 0.5em 0.5em 0; float:left; display:none;}
+.isTag .tagging {display:block;}
+.tagged {margin:0.5em; float:right;}
+.tagging, .tagged {font-size:0.9em; padding:0.25em;}
+.tagging ul, .tagged ul {list-style:none; margin:0.25em; padding:0;}
+.tagClear {clear:both;}
+
+.footer {font-size:.9em;}
+.footer li {display:inline;}
+
+.annotation {padding:0.5em; margin:0.5em;}
+
+* html .viewer pre {width:99%; padding:0 0 1em 0;}
+.viewer {line-height:1.4em; padding-top:0.5em;}
+.viewer .button {margin:0em 0.25em; padding:0em 0.25em;}
+.viewer blockquote {line-height:1.5em; padding-left:0.8em;margin-left:2.5em;}
+.viewer ul, .viewer ol {margin-left:0.5em; padding-left:1.5em;}
+
+.viewer table, table.twtable {border-collapse:collapse; margin:0.8em 1.0em;}
+.viewer th, .viewer td, .viewer tr,.viewer caption,.twtable th, .twtable td, .twtable tr,.twtable caption {padding:3px;}
+table.listView {font-size:0.85em; margin:0.8em 1.0em;}
+table.listView th, table.listView td, table.listView tr {padding:0px 3px 0px 3px;}
+
+.viewer pre {padding:0.5em; margin-left:0.5em; font-size:1.2em; line-height:1.4em; overflow:auto;}
+.viewer code {font-size:1.2em; line-height:1.4em;}
+
+.editor {font-size:1.1em;}
+.editor input, .editor textarea {display:block; width:100%; font:inherit;}
+.editorFooter {padding:0.25em 0em; font-size:.9em;}
+.editorFooter .button {padding-top:0px; padding-bottom:0px;}
+
+.fieldsetFix {border:0; padding:0; margin:1px 0px 1px 0px;}
+
+.sparkline {line-height:1em;}
+.sparktick {outline:0;}
+
+.zoomer {font-size:1.1em; position:absolute; overflow:hidden;}
+.zoomer div {padding:1em;}
+
+* html #backstage {width:99%;}
+* html #backstageArea {width:99%;}
+#backstageArea {display:none; position:relative; overflow: hidden; z-index:150; padding:0.3em 0.5em 0.3em 0.5em;}
+#backstageToolbar {position:relative;}
+#backstageArea a {font-weight:bold; margin-left:0.5em; padding:0.3em 0.5em 0.3em 0.5em;}
+#backstageButton {display:none; position:absolute; z-index:175; top:0em; right:0em;}
+#backstageButton a {padding:0.1em 0.4em 0.1em 0.4em; margin:0.1em 0.1em 0.1em 0.1em;}
+#backstage {position:relative; width:100%; z-index:50;}
+#backstagePanel {display:none; z-index:100; position:absolute; margin:0em 3em 0em 3em; padding:1em 1em 1em 1em;}
+.backstagePanelFooter {padding-top:0.2em; float:right;}
+.backstagePanelFooter a {padding:0.2em 0.4em 0.2em 0.4em;}
+#backstageCloak {display:none; z-index:20; position:absolute; width:100%; height:100px;}
+
+.whenBackstage {display:none;}
+.backstageVisible .whenBackstage {display:block;}
+/*}}}*/
+
+
+
/***
+StyleSheet for use when a translation requires any css style changes.
+This StyleSheet can be used directly by languages such as Chinese, Japanese and Korean which need larger font sizes.
+***/
+/*{{{*/
+body {font-size:0.8em;}
+#sidebarOptions {font-size:1.05em;}
+#sidebarOptions a {font-style:normal;}
+#sidebarOptions .sliderPanel {font-size:0.95em;}
+.subtitle {font-size:0.8em;}
+.viewer table.listView {font-size:0.95em;}
+/*}}}*/
+
+
+
/*{{{*/
+@media print {
+#mainMenu, #sidebar, #messageArea, .toolbar, #backstageButton, #backstageArea {display: none ! important;}
+#displayArea {margin: 1em 1em 0em 1em;}
+/* Fixes a feature in Firefox 1.5.0.2 where print preview displays the noscript content */
+noscript {display:none;}
+}
+/*}}}*/
+
+
+
<!--{{{-->
+<div class='header' macro='gradient vert [[ColorPalette::PrimaryLight]] [[ColorPalette::PrimaryMid]]'>
+<div class='headerShadow'>
+<span class='siteTitle' refresh='content' tiddler='SiteTitle'></span>&nbsp;
+<span class='siteSubtitle' refresh='content' tiddler='SiteSubtitle'></span>
+</div>
+<div class='headerForeground'>
+<span class='siteTitle' refresh='content' tiddler='SiteTitle'></span>&nbsp;
+<span class='siteSubtitle' refresh='content' tiddler='SiteSubtitle'></span>
+</div>
+</div>
+<div id='mainMenu' refresh='content' tiddler='MainMenu'></div>
+<div id='sidebar'>
+<div id='sidebarOptions' refresh='content' tiddler='SideBarOptions'></div>
+<div id='sidebarTabs' refresh='content' force='true' tiddler='SideBarTabs'></div>
+</div>
+<div id='displayArea'>
+<div id='messageArea'></div>
+<div id='tiddlerDisplay'></div>
+</div>
+<!--}}}-->
+
+
+
<!--{{{-->
+<div class='toolbar' macro='toolbar [[ToolbarCommands::ViewToolbar]]'></div>
+<div class='title' macro='view title'></div>
+<div class='subtitle'><span macro='view modifier link'></span>, <span macro='view modified date'></span> (<span macro='message views.wikified.createdPrompt'></span> <span macro='view created date'></span>)</div>
+<div class='tagging' macro='tagging'></div>
+<div class='tagged' macro='tags'></div>
+<div class='viewer' macro='view text wikified'></div>
+<div class='tagClear'></div>
+<!--}}}-->
+
+
+
<!--{{{-->
+<div class='toolbar' macro='toolbar [[ToolbarCommands::EditToolbar]]'></div>
+<div class='title' macro='view title'></div>
+<div class='editor' macro='edit title'></div>
+<div macro='annotations'></div>
+<div class='editor' macro='edit text'></div>
+<div class='editor' macro='edit tags'></div><div class='editorFooter'><span macro='message views.editor.tagPrompt'></span><span macro='tagChooser'></span></div>
+<!--}}}-->
+
+
+
To get started with this blank TiddlyWiki, you'll need to modify the following tiddlers:
+* SiteTitle & SiteSubtitle: The title and subtitle of the site, as shown above (after saving, they will also appear in the browser title bar)
+* MainMenu: The menu (usually on the left)
+* DefaultTiddlers: Contains the names of the tiddlers that you want to appear when the TiddlyWiki is opened
+You'll also need to enter your username for signing your edits: <<option txtUserName>>
+
+
+
These InterfaceOptions for customising TiddlyWiki are saved in your browser
+
+Your username for signing your edits. Write it as a WikiWord (eg JoeBloggs)
+
+<<option txtUserName>>
+<<option chkSaveBackups>> SaveBackups
+<<option chkAutoSave>> AutoSave
+<<option chkRegExpSearch>> RegExpSearch
+<<option chkCaseSensitiveSearch>> CaseSensitiveSearch
+<<option chkAnimate>> EnableAnimations
+
+----
+Also see AdvancedOptions
+
+
+
<<importTiddlers>>
+
+
+ +
+
+
Adam is the author of ''Taggable.''  To find out more about him and other software that he has created, visit http://www.adamcrossland.net.  You can also visit his blog, http://blog.adamcrossland.net, to see the code in action; he created ''Taggable'' as part of the blogging software project the he wrote as a vehicle to learn about [[Google AppEngine]].
+
+
+
{{{
+Copyright 2008 Adam A. Crossland
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+}}}
+
+
+
''1.0''
+Initial release
+----
+''2.0''
+This release has major changes to both [[Tag|Tag Class]] and [[Taggable|Taggable Class]], reflecting significant and important lessons that [[the author|AdamCrossland]] learned about [[Google AppEngine]].  Specifically, this release's changes should substantially improve the performance of any code that uses tags.
+*[[Tag|Tag Class]] entities are now stored with a custom key_name that allows them to be much-more-quickly retrieved from the data store.
+*The [[Tag|Tag Class]] has been rewritten to breakdown Tag/Taggable interactions in to a series of simple, atomic transactions.
+*[[Tag|Tag Class]] has a new property, [[tagged_count|Tag tagged_count Property]], that records the number of Taggable entities in its [[tagged|Tag tagged Property]] property.
+*Two new methods, [[get_tags_by_frequency|Tag get_tags_by_frequency Method]] and [[popular_tags|Tag popular_tags Method]], provide access to lists of [[Tags|Tag Class]] based on the number of [[Taggable|Taggable Class]] entities to which they refer.
+*A new method, [[get_tags_by_name|Tag get_tags_by_name Method]], provides lists of tags in alphabetical order.
+*The [[Taggable|Taggable Class]] class has been greatly simplified.  Multiple methods for getting and setting tags have been replaced with a single property, [[tags|Taggable tags Property]], that handles both getting and setting operations.
+*A full suite of automated unit tests for both classes has been created.
+
+
+
If you have any questions or suggestions about ''Taggable'', please feel free to contact the author, AdamCrossland, at adam@adamcrossland.net.  Alternatively, you could post the question in <html><a href="http://groups.google.com/group/google-appengine/topics">the Google AppEngine Group.</a></html>
+
+
+
If you are using ''Taggable'' and you have some changes, improvements or bug fixes that you'd like to contribute, please contact AdamCrossland at adam@adamcrossland.net.
+
+
+
Because the Tag records are stored and queried slightly differently (yet much-more efficiently) in //Taggable-mixin 2.0//, an existing application will have to go through a process of converting all of its existing tags.
+
+For AdamCBlog, the application from which //Taggable-mixin// is extracted, I added a new RequestHandler called UpdateTag:
+{{{
+class UpdateTag(RequestHandler):
+    def get(self):
+        from taggable import Tag
+        tag_name = self.request.get('tag')
+        tag = db.Query(Tag).filter('tag =', tag_name).fetch(1)[0]
+        if tag is not None:
+            new_tag = Tag.get_or_create(tag_name)
+            for each_tagged in tag.tagged:
+                new_tag.add_tagged(each_tagged)
+        tag.delete()
+    
+        self.redirect('/')
+}}}
+
+Then, I made the request /~UpdateTag?tag=//inserttaghere// for each Tag in the datastore.  Yes, it is laborious.
+
+Why not just create a RequestHandler that cycled through all of the tags automatically, converting them as it went?  I tried that, but there is too much processing involved; the request uses up its time quota and is killed before it can finish.
+
+
+
A [[Google AppEngine]] datastore property that holds a Python //datetime//.
+
+http://code.google.com/appengine/docs/datastore/typesandpropertyclasses.html#DateTimeProperty
+
+
+
[[Introduction]]
+
+
+
Google ~AppEngine is Google's platform for developing web applications that run inside Google's computing cloud.
+
+Find out more about it at <html><a href="http://code.google.com/appengine/">The Google AppEngine website</a></html>.
+
+
+
+
+
A [[Google AppEngine]] datastore property that holds a Python //long//.
+
+http://code.google.com/appengine/docs/datastore/typesandpropertyclasses.html#IntegerProperty
+
+
+
''Taggable'' is a <html><a href="http://www.linuxjournal.com/node/4540/print">mixin</a></html> class that AdamCrossland created in order to add [[tags|Tag Definition]] to his personal blog (http://blog.adamcrossland.net) which is built on [[Google AppEngine]].  The design seemed clean and portable, so he decided to share it with the greater [[AppEngine|Google AppEngine]] community.  It is available under [[Apache 2.0 Open Source license]].
+
+Users of taggable-mixin 1.0 should note that taggable-mixin 2.0 is not directly compatible with 1.0.  While it is possible to upgrade, existing Tag entities will have to be [[converted|Converting from 1.0 to 2.0]].
+
+
+
A [[Google AppEngine]] class that represents a unique key for a datastore entity.
+
+http://code.google.com/appengine/docs/datastore/keyclass.html
+
+
+
A [[Google AppEngine]] datastore property that represents a Python //list//.
+
+http://code.google.com/appengine/docs/datastore/typesandpropertyclasses.html#ListProperty
+
+
+
[[Introduction|Introduction]]
+ChangeLog
+StepByStep
+<<tag api>>
+----
+[[License|Apache 2.0 Open Source license]]
+[[Contribute]]
+[[Contact]]
+
+
+
A //~RequestHandler// is a Python class that inherits from //webapp.~RequestHandler//.  It is used to answer HTTP requests that are received by your [[Google AppEngine]] application.  For more information on this subject, please consult the <html><a href="http://code.google.com/appengine/docs/webapp/requesthandlers.html">documentation.</a></html>
+
+
+
+
+
<<search>><<closeAll>><<permaview>><<newTiddler>><<saveChanges>><<slider chkSliderOptionsPanel OptionsPanel "options »" "Change TiddlyWiki advanced options">>
+
+
+
a portable mixin class for adding Tags to Google ~AppEngine Models
+
+
+
Taggable
+
+
+
Here's a step-by-step guide to adding ''Taggable'' to your [[Google AppEngine]] application.  This presents the simplest, most straightforward path to integrating ''Taggable'' as as such, it does not cover all of the options that are available.  
+
+All of the code examples are modified extracts from the blogging software for which I originally created ''Taggable.''  //''These examples do not represent what the author considers to be complete and secure code; please make sure that you are familiar with best practices for building secure web applications before creating a web application.  The author of this document and the accompanying code bears no responsibility whatsoever for any losses or damages that you incur as a result of failing to take the appropriate steps to create a stable, secure, well-written web application.''//
+
+*Copy [[taggable.py]] to your application directory.
+*Import [[taggable.py]] into the python file that defines the Model that you want to make taggable:
+{{{
+from taggable import Taggable
+}}}
+*Add //Taggable// to the list of classes from which your Model class inherits.  Taggable -- and any other mixin classes -- should come before db.Model:
+{{{
+class Post(Taggable, db.Model):
+}}}
+*Add code to your Model's //init// method to call Taggable's //init// method.  If your Model class does not already override //init//, it will have to, and you will have to explicitly call the //init// method of any other superclass -- such as db.Model:
+{{{
+    def __init__(self, parent=None, key_name=None, app=None, **entity_values):
+        db.Model.__init__(self, parent, key_name, app, **entity_values)
+        Taggable.__init__(self)
+}}}
+*Add code to your template to express the tagging information:
+{{{
+{% if post.tags %}
+    <div class="posttags">tags:&nbsp;
+        {% for each_tag in post.tags %}
+            <a href="/searchbytag?tag={{ each_tag.tag|escape }}">{{ each_tag.tag }}</a>{% if forloop.last %}{% else %}, {% endif %}
+        {% endfor %}
+    </div>
+{% endif %}
+}}}
+or
+{{{
+<tr>
+    <td>Tags:</td>
+    <td>
+        <input type="TEXT" name="tags" size="106" value="{% if post %}{{ post.tags_string }}{% endif %}" />
+    </td>
+</tr>
+}}}
+*In any RequestHandler method that updates a Model object that has tags associated with it,  assign any new value to the ''Taggable'' object's [[tags|TaggableTagsProperty]]:
+{{{
+class EditPost(SmartHandler.SmartHandler):
+    def post(self):
+        postid = self.request.get('id')
+        post = Post.get(postid)
+        post.title = self.request.get('title')
+        post.body = self.request.get('body')
+        post.edited = datetime.datetime.now()
+        post.tags = self.request.get('tags')
+        post.put()
+        .
+        . do whatever else you need to do in your handler...
+        .
+}}}
+
+
+
A [[Google AppEngine]] datastore property that holds a Python //string// of 500 characters or less.
+
+http://code.google.com/appengine/docs/datastore/typesandpropertyclasses.html#StringProperty
+
+
+
The //Tag// class is the Model class that holds the data for an individual [[tag|Tag Definition]].
+
+It has four properties:
+*[[tag|Tag tag Property]]
+*[[added|Tag tag_added Property]]
+*[[tagged|Tag tagged Property]]
+*[[tagged_count|Tag tagged_count Property]]
+
+and ten methods:
+*[[remove_tagged|Tag remove_tagged Method]]
+*[[add_tagged|Tag add_tagged Method]]
+*[[clear_tagged|Tag clear_tagged Method]]
+*[[get_by_name|Tag get_by_name Method]]
+*[[get_tags_for_key|Tag get_tags_for_key Method]]
+*[[get_or_create|Tag get_or_create Method]]
+*[[get_tags_by_frequency|Tag get_tags_by_frequency Method]]
+*[[get_tags_by_name|Tag get_tags_by_name Method]]
+*[[popular_tags|Tag popular_tags Method]]
+*[[expire_cached_tags|Tag expire_cached_tags Method]]
+
+
+
A //Tag// is a word or short phrase that acts as metadata; it describes and increases the searchability and findability of data.
+
+http://en.wikipedia.org/wiki/Tag_(metadata)
+
+
+
//tag_instance.add_tagged(key)//
+* key - a [[Key|Key Class]] for a //Taggable// Model to mark with the [[Tag|Tag Class]]
+Returns: nothing
+
+The //add_tagged// method adds the [[Key|Key Class]] passed in to the Tag's [[tagged|Tag tagged Property]] collection and increments the [[tagged_count|Tag tagged_count Property]].  The operations are performed inside a transaction to ensure data intergity.
+
+Under most circumstances, the programmer will not need to call //add_tagged// directly; it is meant to be used internally by the [[Tagged Class]].  However, it is available for use by those who wish to customize the behavior of Taggable.
+
+
+
//tag_instance.clear_tagged(key)//
+* key - a [[Key|Key Class]] for a //Taggable// object.
+Returns: nothing
+
+The //clear_tagged// method empties the Tag's [[tagged|Tag tagged Property]] collection and sets the [[tagged_count|Tag tagged_count Property]] to zero.  The operations are performed inside a transaction to ensure data intergity.
+
+Under most circumstances, the programmer will not need to call //clear_tagged// directly; it is meant to be used internally by the [[Tagged Class]].  However, it is available for use by those who wish to customize the behavior of Taggable.
+
+
+
//Tag.expire_cached_tags()//
+Returns: nothing
+
+Comments:
+This method removes from memcache any objects that have been cached by other [[Tag|Tag Class]] methods.  Usually, the programmer will not call this method directly, as it is used behind-the-scenes by code that affects the validity of the cached items.  It is available, however, in case the programmer wishes to customize existing behavior.
+
+
+
//Tag.get_by_name(tag_name)//
+*tag_name - the tag, in text form
+Returns: a [[Tag|Tag Class]] or //None//
+
+Example:
+{{{
+class SearchByTag(RequestHandler):
+    def get(self):
+        from post import Post
+        from taggable import Tag
+        
+        requested_tag = self.request.get('tag')
+        if requested_tag is not None and len(requested_tag) > 0:
+            tag = Tag.get_by_name(requested_tag)
+            posts = None
+            if tag is not None:
+                posts = Post.get(tag.tagged)
+
+            self.template_values['posts'] = posts
+}}}
+
+
+
+
//Tag.get_or_create(tag_name)//
+*tag_name - the name of the tag that is to be retrieved from or created in the datastore
+Returns: a [[Tag|Tag Class]]
+
+Example:
+{{{
+for each_tag in tags:
+    each_tag = string.strip(each_tag)
+    if len(each_tag) > 0 and each_tag not in self.__tags:
+        # A tag that was not previously assigned to this entity
+        # is present in the list that is being assigned, so we
+        # associate this entity with the tag.
+        tag = Tag.get_or_create(each_tag)
+        tag.add_tagged(self.key())
+        self.__tags.append(tag)
+}}}
+
+
+
//Tag.get_tags_by_frequency(limit=1000)//
+*limit - the number of records to return; the maximum is 1000
+Returns: a list of [[Tag|Tag Class]] objects, ordered by the number of //Taggable// objects assigned to it.
+
+Example:
+{{{
+@classmethod
+def popular_tags(cls, limit=5):
+    from google.appengine.api import memcache
+        
+    tags = memcache.get('popular_tags')
+    if tags is None:
+        tags = Tag.get_tags_by_frequency(limit)
+        memcache.add('popular_tags', tags, 3600)
+        
+    return tags
+}}}
+
+
+
//Tag.get_tags_by_name(tag_name)//
+*tag_name - the string value of a [[Tag|Tag Class]] to be retrieved from the datastore
+Returns: a [[Tag|Tag Class]] object or None if the given //tag_name// does not exist
+
+Example:
+{{{
+requested_tag = self.request.get('tag')
+if requested_tag is not None and len(requested_tag) > 0:
+    tag = Tag.get_by_name(requested_tag)
+    posts = None
+    if tag is not None:
+        posts = Post.get(tag.tagged)
+
+    self.template_values['posts'] = posts
+}}}
+
+
+
//Tag.get_tags_for_key(key)//
+*key - a [[Key|Key Class]] for a //Taggable// object.
+Returns: a //list// of [[Tag|Tag Class]] objects
+
+Example:
+{{{
+def __get_tags(self):
+    "Get a List of Tag objects for all Tags that apply to this object."
+    if self.__tags is None or len(self.__tags) == 0:
+        self.__tags = Tag.get_tags_for_key(self.key())
+    return self.__tags
+}}}
+
+
+
//Tag.popular_tags(limit=5)//
+*limit - the number of records to return; the default is 5 and the maximum is 1000
+Returns: a list of [[Tag|Tag Class]] objects, ordered by the number of [[Taggable|Taggable Class]] objects assigned to it.
+
+Comments:
+This method is a thin wrapper around [[get_tags_by_frequency|Tag get_tags_by_frequency]] that caches the returned values.
+
+Example:
+{{{
+self.template_values['popular_tags'] = Tag.popular_tags()
+}}}
+
+
+
//tag_instance.remove_tagged(key)//
+* key - a [[Key|Key Class]] for a //Taggable// object.
+Returns: nothing
+
+The //remove_tagged// method removes the [[Key|Key Class]] passed in from the Tag's [[tagged|Tag tagged Property]] collection and decrements the [[tagged_count|Tag tagged_count Property]].  The operations are performed inside a transaction to ensure data intergity.
+
+Under most circumstances, the programmer will not need to call //remove_tagged// directly; it is meant to be used internally by the [[Tagged Class]].  However, it is available for use by those who wish to customize the behavior of Taggable.
+
+
+
A StringProperty that holds the name or value of the tag.
+
+
+
The DateTimeProperty that records that date and time that the [[Tag|TagClass]] was first added.
+
+
+
A ListProperty that holds the Keys of all of the entities that have the given [[Tag|TagClass]] assigned to them.
+
+
+
An IntegerProperty that holds the number of items that have the Tag assigned to them.
+
+
+
The //Taggable// class can be mixed-in to any other Python class that inherits from //db.Model// in order to associate [[Tags|Tag Definition]] with that model.  For instance, if you were creating blogging software, one of your fundamental objects would be a //Post// that would comprise all of the information about an individual entry in your blog.  Usually, a Post can have [[Tags|Tag Definition]] added to it in order to provide easily-digested information to the reader about the content.
+
+It has two properties:
+*[[tags|Taggable tags Property]]
+*[[tag_separator|Taggable tag_separator Property]]
+
+and one method:
+*[[tags_string|Taggable tags_string Method]]
+
+Example:
+{{{
+class Post(Taggable, db.Model):
+    index = db.IntegerProperty(required=True, default=0)
+    body = db.TextProperty(required = True)
+    title = db.StringProperty()
+    added = db.DateTimeProperty(auto_now_add=True)
+    added_month = db.IntegerProperty()
+    added_year = db.IntegerProperty()
+    edited = db.DateTimeProperty()
+        
+    def __init__(self, parent=None, key_name=None, app=None, **entity_values):
+        db.Model.__init__(self, parent, key_name, app, **entity_values)
+        Taggable.__init__(self)
+}}}
+Notes:
+*Notice that [[Taggable|Taggable Class]] is placed before db.Model in the inheritance list.  While this is not strictly required for //Taggable-mixin// to function correctly, it //is// required for other mixin classes, and it is generally a good practice.
+*It //is// required that the [[Taggable|Taggable Class]] //init// method is explicitly called, so it should be placed in the inherting class's //init// method.  If the class would not otherwise have one, it should be created, and the db.Model //init// must be called as well, as shown.
+
+
+
The //tag_separator// property is the string that is used to separate individual tags in a string representation of a list of tags.  It is used by the [[tags property|TaggableTagsProperty]] to parse a string that may contain one or more tags to be applied to a [[Taggable|TaggableClass]] object.  It is also used by the [[tags_string|TaggableTagsStringMethod]] method to construct a string representation of the tags for a [[Taggable|TaggableClass]] object.
+
+*By default, it is set to a comma (','), but it can be programatically-set to whatever value the developer desires
+*It is probably best to avoid using whitespace characters, as that would prevent users from entering multi-word tags
+*Custom separator values can be set at different levels.
+**To set one value for all [[Taggable|TaggableClass]] objects, modify the //init// method in the //Taggable// class in the taggable.py file:
+{{{
+class Taggable:
+     def __init__(self):
+        self.tag_separator = ";" # Made it semi-colon instead of comma
+}}}
+**To set one value for all instances of a particular [[Taggable|TaggableClass]] class, modify the //init// method of that class:
+{{{
+class Post(Taggable, db.Model):
+    body = db.TextProperty(required = True)
+    title = db.StringProperty()
+    added = db.DateTimeProperty(auto_now_add=True)
+    edited = db.DateTimeProperty()
+            
+    def __init__(self, parent=None, key_name=None, app=None, **entity_values):
+        db.Model.__init__(self, parent, key_name, app, **entity_values)
+        Taggable.__init__(self)
+        self.tag_separator = ";" # Made it semi-colon instead of comma        
+}}}
+**To set a value for one particular instance of a [[Taggable|TaggableClass]] class, set the value after creating the instance:
+{{{
+newpost = Post(title = self.request.get('title'), body = self.request.get('body'))
+newpost.tag_separator = ";" # Made it semi-colon instead of comma
+newpost.set_tags_from_string("foo; bar; bletch; this is a tag; tags rule"     
+}}}
+
+
+
The a [[Taggable|Taggable Class]] class's //tags// property is used to assign [[Tags|Tag Class]] to the [[Taggable|Taggable Class]] instance or retrieve a //list// of those already assigned.
+
+Assignment Example:
+{{{
+class Post(Taggable, db.Model):
+    index = db.IntegerProperty(required=True, default=0)
+    body = db.TextProperty(required = True)
+    title = db.StringProperty()
+    added = db.DateTimeProperty(auto_now_add=True)
+    added_month = db.IntegerProperty()
+    added_year = db.IntegerProperty()
+    edited = db.DateTimeProperty()
+        
+    def __init__(self, parent=None, key_name=None, app=None, **entity_values):
+        db.Model.__init__(self, parent, key_name, app, **entity_values)
+        Taggable.__init__(self)
+
+    @classmethod
+    def new_post(cls, new_title=None, new_body=None, new_tags=[]):
+        if new_title is not None and new_body is not None:
+            
+            new_post = Post(title = new_title, body = new_body)
+            new_post.put()
+        
+            new_post.tags = new_tags
+            new_post.save()
+        else:
+            raise Exception("Must supply both new_title and new_body when creating a new Post.")
+}}}
+
+Retrieval Example:
+{{{
+{% if post.tags %}
+    <div class="posttags">tags:&nbsp;
+        {% for each_tag in post.tags %}
+            <a href="/searchbytag?tag={{ each_tag.tag|escape }}">{{ each_tag.tag }}</a>{% if forloop.last %}{% else %}, {% endif %}
+        {% endfor %}
+    </div>
+{% endif %}
+}}}
+
+
+
//taggable_instance.tags_string()//
+Returns: a string representation of the tags assigned to the [[Taggable|Taggable Class]] instance.
+
+This method simply joins the string versions of each [[Tag|Tag Class]] in the [[Taggable|Taggable Class]] class's //list//, placing the value stored in [[tag_separator|Taggable tag_separator Property]] inbetween each element.
+
+
+
<<tagging api>>
+
+
+
The //get_tags_as_string// method creates a string representation of all of the tags that apply to a ''Taggable'' object.
+
+
+
The //set_tags// method sets the tags for a ''Taggable'' object from a list of strings:
+{{{
+tag_list = ["foo", "bar", "bletch","this is a tag", "tags rule"]
+my_taggable_object.set_tags(tag_list)
+}}}
+
+
+
The //set_tags_from_string method// sets the tags for a Taggable object from a string that contains one or more tags separated by the character or characters in //self.tag_seperator//:
+{{{
+tag_list = "foo, bar, bletch, this is a tag, tags rule"
+my_taggable_object.set_tags_from_string(tag_list)
+}}}
+
+By default, [[tag_separator|tag_separator]] is set to a comma (','), but it can be anything that the programmer wants.
+
+
+
All of the code for the ''Taggable'' mixin class lives in the file taggable.py.  Simply copy this file in to your Google ~AppEngine main directory, and it should be available to your code.
+
+
+ + + + + + + + + + diff -r a525a55833f1 -r 0ede2f3adbc1 app/taggable/taggable.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/app/taggable/taggable.py Sat Jul 25 01:09:46 2009 +0530 @@ -0,0 +1,232 @@ +from google.appengine.ext import db +import string + +class Tag(db.Model): + "Google AppEngine model for store of tags." + + tag = db.StringProperty(required=True) + "The actual string value of the tag." + + added = db.DateTimeProperty(auto_now_add=True) + "The date and time that the tag was first added to the datastore." + + tagged = db.ListProperty(db.Key) + "A List of db.Key values for the datastore objects that have been tagged with this tag value." + + tagged_count = db.IntegerProperty(default=0) + "The number of entities in tagged." + + @classmethod + def __key_name(cls, tag_name): + return cls.__name__ + '_' + tag_name + + def remove_tagged(self, key): + def remove_tagged_txn(): + if key in self.tagged: + self.tagged.remove(key) + self.tagged_count -= 1 + self.put() + db.run_in_transaction(remove_tagged_txn) + self.__class__.expire_cached_tags() + + def add_tagged(self, key): + def add_tagged_txn(): + if key not in self.tagged: + self.tagged.append(key) + self.tagged_count += 1 + self.put() + db.run_in_transaction(add_tagged_txn) + self.__class__.expire_cached_tags() + + def clear_tagged(self): + def clear_tagged_txn(): + self.tagged = [] + self.tagged_count = 0 + self.put() + db.run_in_transaction(clear_tagged_txn) + self.__class__.expire_cached_tags() + + @classmethod + def get_by_name(cls, tag_name): + return cls.get_by_key_name(cls.__key_name(tag_name)) + + @classmethod + def get_tags_for_key(cls, key): + "Set the tags for the datastore object represented by key." + tags = db.Query(cls).filter('tagged =', key).fetch(1000) + return tags + + @classmethod + def get_or_create(cls, tag_name): + "Get the Tag object that has the tag value given by tag_value." + tag_key_name = cls.__key_name(tag_name) + existing_tag = cls.get_by_key_name(tag_key_name) + if existing_tag is None: + # The tag does not yet exist, so create it. + def create_tag_txn(): + new_tag = cls(key_name=tag_key_name, tag=tag_name) + new_tag.put() + return new_tag + existing_tag = db.run_in_transaction(create_tag_txn) + return existing_tag + + @classmethod + def get_tags_by_frequency(cls, limit=1000): + """Return a list of Tags sorted by the number of objects to + which they have been applied, most frequently-used first. + If limit is given, return only that many tags; otherwise, + return all.""" + tag_list = db.Query(cls).filter('tagged_count >', 0).order( + "-tagged_count").fetch(limit) + + return tag_list + + @classmethod + def get_tags_by_name(cls, limit=1000, ascending=True): + """Return a list of Tags sorted alphabetically by the name of the tag. + If a limit is given, return only that many tags; otherwise, return all. + If ascending is True, sort from a-z; otherwise, sort from z-a.""" + + from google.appengine.api import memcache + + cache_name = cls.__name__ + '_tags_by_name' + if ascending: + cache_name += '_asc' + else: + cache_name += '_desc' + + tags = memcache.get(cache_name) + if tags is None or len(tags) < limit: + order_by = "tag" + if not ascending: + order_by = "-tag" + + tags = db.Query(cls).order(order_by).fetch(limit) + memcache.add(cache_name, tags, 3600) + else: + if len(tags) > limit: + # Return only as many as requested. + tags = tags[:limit] + + return tags + + @classmethod + def popular_tags(cls, limit=5): + from google.appengine.api import memcache + + tags = memcache.get(cls.__name__ + '_popular_tags') + if tags is None: + tags = cls.get_tags_by_frequency(limit) + memcache.add(cls.__name__ + '_popular_tags', tags, 3600) + + return tags + + @classmethod + def expire_cached_tags(cls): + from google.appengine.api import memcache + + memcache.delete(cls.__name__ + '_popular_tags') + memcache.delete(cls.__name__ + '_tags_by_name_asc') + memcache.delete(cls.__name__ + '_tags_by_name_desc') + + def __str__(self): + """Returns the string representation of the entity's tag name. + """ + + return self.tag + +def tag_property(tag_name): + """Decorator that creates and returns a tag property to be used + in Google AppEngine model. + + Args: + tag_name: name of the tag to be created. + """ + + def get_tags(self): + """"Get a list of Tag objects for all Tags that apply to the + specified entity. + """ + + + if self._tags[tag_name] is None or len(self._tags[tag_name]) == 0: + self._tags[tag_name] = self._tag_model[ + tag_name].get_tags_for_key(self.key()) + return self._tags[tag_name] + + def set_tags(self, seed): + """Set a list of Tag objects for all Tags that apply to + the specified entity. + """ + + import types + if type(seed['tags']) is types.UnicodeType: + # Convert unicode to a plain string + seed['tags'] = str(seed['tags']) + if type(seed['tags']) is types.StringType: + # Tags is a string, split it on tag_seperator into a list + seed['tags'] = string.split(seed['tags'], self.tag_separator) + if type(seed['tags']) is types.ListType: + get_tags(self) + # Firstly, we will check to see if any tags have been removed. + # Iterate over a copy of _tags, as we may need to modify _tags + for each_tag in self._tags[tag_name][:]: + if each_tag not in seed['tags']: + # A tag that was previously assigned to this entity is + # missing in the list that is being assigned, so we + # disassocaite this entity and the tag. + each_tag.remove_tagged(self.key()) + self._tags[tag_name].remove(each_tag) + # Secondly, we will check to see if any tags have been added. + for each_tag in seed['tags']: + each_tag = string.strip(each_tag) + if len(each_tag) > 0 and each_tag not in self._tags[tag_name]: + # A tag that was not previously assigned to this entity + # is present in the list that is being assigned, so we + # associate this entity with the tag. + tag = self._tag_model[tag_name].get_or_create( + seed['scope'], each_tag) + tag.add_tagged(self.key()) + self._tags[tag_name].append(tag) + else: + raise Exception, "tags must be either a unicode, a string or a list" + + return property(get_tags, set_tags) + + +class Taggable(object): + """A mixin class that is used for making GAE Model classes taggable. + + This is an extended version of Taggable-mixin which allows for + multiple tag properties in the same AppEngine Model class. + """ + + def __init__(self, **kwargs): + """The constructor class for Taggable, that creates a dictionary of tags. + + The difference from the original taggable in terms of interface is + that, tag class is not used as the default tag model, since we don't + have a default tag property created in this class now. + + Args: + kwargs: keywords containing the name of the tags and arguments + containing tag model to be used. + """ + + self._tags = {} + self._tag_model = {} + + for tag_name in kwargs: + self._tags[tag_name] = None + self._tag_model[tag_name] = kwargs[tag_name] + + self.tag_separator = ", " + + def tags_string(self, tag_name): + "Create a formatted string version of this entity's tags" + to_str = "" + for each_tag in tag_name: + to_str += each_tag.tag + if each_tag != tag_name[-1]: + to_str += self.tag_separator + return to_str diff -r a525a55833f1 -r 0ede2f3adbc1 app/taggable/taggable_tests.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/app/taggable/taggable_tests.py Sat Jul 25 01:09:46 2009 +0530 @@ -0,0 +1,243 @@ +#!/usr/bin/env python + +#Copyright 2008 Adam A. Crossland +# +#Licensed under the Apache License, Version 2.0 (the "License"); +#you may not use this file except in compliance with the License. +#You may obtain a copy of the License at +# +#http://www.apache.org/licenses/LICENSE-2.0 +# +#Unless required by applicable law or agreed to in writing, software +#distributed under the License is distributed on an "AS IS" BASIS, +#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +#See the License for the specific language governing permissions and +#limitations under the License. + +import sys +import os.path + +APPENGINE_PATH = '../../thirdparty/google_appengine' + +# Add app-engine related libraries to your path +paths = [ + APPENGINE_PATH, + os.path.join(APPENGINE_PATH, 'lib', 'django'), + os.path.join(APPENGINE_PATH, 'lib', 'webob'), + os.path.join(APPENGINE_PATH, 'lib', 'yaml', 'lib') +] +for path in paths: + if not os.path.exists(path): + raise 'Path does not exist: %s' % path +sys.path = paths + sys.path + +import unittest +from google.appengine.api import apiproxy_stub_map +from google.appengine.api import datastore_file_stub +from google.appengine.api import mail_stub +from google.appengine.api import user_service_stub +from google.appengine.ext import webapp +from google.appengine.ext import db +from google.appengine.api.memcache import memcache_stub +from taggable import * + +APP_ID = u'taggable' +AUTH_DOMAIN = 'gmail.com' +LOGGED_IN_USER = 'me@example.com' # set to '' for no logged in user + +BLOG_NAME='test_blog' + +class BlogIndex(db.Model): + "A global counter used to provide the index of the next blog post." + index = db.IntegerProperty(required=True, default=0) + "The next available index for a Post." + +class Post(Taggable, db.Model): + index = db.IntegerProperty(required=True, default=0) + body = db.TextProperty(required = True) + title = db.StringProperty() + added = db.DateTimeProperty(auto_now_add=True) + added_month = db.IntegerProperty() + added_year = db.IntegerProperty() + edited = db.DateTimeProperty() + + def __init__(self, parent=None, key_name=None, app=None, **entity_values): + db.Model.__init__(self, parent, key_name, app, **entity_values) + Taggable.__init__(self) + + def get_all_posts(): + return db.GqlQuery("SELECT * from Post ORDER BY added DESC") + Get_All_Posts = staticmethod(get_all_posts) + + @classmethod + def get_posts(cls, start_index=0, count=10): + start_index = int(start_index) # Just make sure that we have an int + posts = Post.gql('WHERE index <= :1 ORDER BY index DESC', start_index).fetch(count + 1) + if len(posts) > count: + posts = posts[:count] + + return posts + + @classmethod + def new_post(cls, new_title=None, new_body=None, new_tags=[]): + new_post = None + if new_title is not None and new_body is not None: + def txn(): + blog_index = BlogIndex.get_by_key_name(BLOG_NAME) + if blog_index is None: + blog_index = BlogIndex(key_name=BLOG_NAME) + new_index = blog_index.index + blog_index.index += 1 + blog_index.put() + + new_post_key_name = BLOG_NAME + str(new_index) + new_post = Post(key_name=new_post_key_name, parent=blog_index, + index = new_index, title = new_title, + body = new_body) + new_post.put() + + return new_post + new_post = db.run_in_transaction(txn) + + new_post.tags = new_tags + new_post.put() + else: + raise Exception("Must supply both new_title and new_body when creating a new Post.") + + return new_post + + def delete(self): + # Perform any actions that are required to maintain data integrity + # when this Post is delete. + # Disassociate this Post from any Tag + self.set_tags([]) + + # Finally, call the real delete + db.Model.delete(self) + +class MyTest(unittest.TestCase): + + def setUp(self): + # Start with a fresh api proxy. + apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap() + + # Use a fresh stub datastore. + stub = datastore_file_stub.DatastoreFileStub(APP_ID, '/dev/null', '/dev/null') + apiproxy_stub_map.apiproxy.RegisterStub('datastore_v3', stub) + + # Use a fresh memcache stub. + apiproxy_stub_map.apiproxy.RegisterStub('memcache', memcache_stub.MemcacheServiceStub()) + + # Use a fresh stub UserService. + apiproxy_stub_map.apiproxy.RegisterStub( + 'user', user_service_stub.UserServiceStub()) + os.environ['AUTH_DOMAIN'] = AUTH_DOMAIN + os.environ['USER_EMAIL'] = LOGGED_IN_USER + os.environ['APPLICATION_ID'] = APP_ID + + def testSimpleTagAdding(self): + new_post = Post.new_post(new_title='test post 1', new_body='This is a test post. Please ignore.') + assert new_post is not None + + new_post.tags = "test, testing, tests" + self.assertEqual(len(new_post.tags), 3) + + def testComplexTagAdding(self): + new_post = Post.new_post(new_title='test post 1', new_body='This is a test post. Please ignore.') + assert new_post is not None + + new_post.tags = " test, testing, tests,,,tag with spaces" + self.assertEqual(len(new_post.tags), 4) + + tag = new_post.tags[3] + assert tag is not None + self.assertEqual(tag.tag, 'tag with spaces') + self.assertEqual(tag.tagged_count, 1) + + tag2 = Tag.get_by_name('tag with spaces') + assert tag2 is not None + self.assertEqual(tag.tag, 'tag with spaces') + self.assertEqual(tag.tagged_count, 1) + + def testTagDeletion(self): + new_post = Post.new_post(new_title='test post 2', new_body='This is a test post. Please continue to ignore.') + assert new_post is not None + + new_post.tags = "test, testing, tests" + self.assertEqual(len(new_post.tags), 3) + + new_post.tags = "test" + self.assertEqual(len(new_post.tags), 1) + + def testTagCounts(self): + new_post3 = Post.new_post(new_title='test post 3', new_body='This is a test post. Please continue to ignore.') + assert new_post3 is not None + new_post3.tags = "foo, bar, baz" + new_post4 = Post.new_post(new_title='test post 4', new_body='This is a test post. Please continue to ignore.') + assert new_post4 is not None + new_post4.tags = "bar, baz, bletch" + new_post5 = Post.new_post(new_title='test post 5', new_body='This is a test post. Please continue to ignore.') + assert new_post5 is not None + new_post5.tags = "baz, bletch, quux" + + foo_tag = Tag.get_by_name('foo') + assert foo_tag is not None + self.assertEqual(foo_tag.tagged_count, 1) + + bar_tag = Tag.get_by_name('bar') + assert bar_tag is not None + self.assertEqual(bar_tag.tagged_count, 2) + + baz_tag = Tag.get_by_name('baz') + assert baz_tag is not None + self.assertEqual(baz_tag.tagged_count, 3) + + bletch_tag = Tag.get_by_name('bletch') + assert bletch_tag is not None + self.assertEqual(bletch_tag.tagged_count, 2) + + quux_tag = Tag.get_by_name('quux') + assert quux_tag is not None + self.assertEqual(quux_tag.tagged_count, 1) + + new_post3.tags = 'bar, baz' + foo_tag = Tag.get_by_name('foo') + assert foo_tag is not None + self.assertEqual(len(new_post3.tags), 2) + self.assertEqual(foo_tag.tagged_count, 0) + + def testTagGetTagsForKey(self): + new_post = Post.new_post(new_title='test post 6', new_body='This is a test post. Please continue to ignore.', new_tags='foo,bar,bletch,quux') + assert new_post is not None + + tags = Tag.get_tags_for_key(new_post.key()) + assert tags is not None + self.assertEqual(type(tags), type([])) + self.assertEqual(len(tags), 4) + + def testTagGetByName(self): + new_post = Post.new_post(new_title='test post 6', new_body='This is a test post. Please continue to ignore.', new_tags='foo,bar,bletch,quux') + assert new_post is not None + + quux_tag = Tag.get_by_name('quux') + assert quux_tag is not None + + zizzle_tag = Tag.get_by_name('zizzle') + assert zizzle_tag is None + + def testTagsString(self): + new_post = Post.new_post(new_title='test post 6', new_body='This is a test post. Please continue to ignore.', new_tags=' pal,poll ,,pip,pony') + assert new_post is not None + self.assertEqual(new_post.tags_string(), "pal,poll,pip,pony") + new_post.tag_separator = "|" + self.assertEqual(new_post.tags_string(), "pal|poll|pip|pony") + new_post.tag_separator = " , " + self.assertEqual(new_post.tags_string(), "pal , poll , pip , pony") + + new_post.tag_separator = ", " + new_post.tags = "pal, pill, pip" + self.assertEqual(len(new_post.tags), 3) + self.assertEqual(new_post.tags_string(), "pal, pill, pip") + +if __name__ == '__main__': + unittest.main() diff -r a525a55833f1 -r 0ede2f3adbc1 scripts/build.sh --- a/scripts/build.sh Fri Jul 24 21:00:04 2009 +0200 +++ b/scripts/build.sh Sat Jul 25 01:09:46 2009 +0530 @@ -12,7 +12,7 @@ DEFAULT_APP_FOLDER="../app" DEFAULT_APP_FILES="app.yaml cron.yaml index.yaml queue.yaml main.py settings.py shell.py urls.py gae_django.py" DEFAULT_APP_DIRS="soc ghop gsoc feedparser python25src reflistprop jquery \ - ranklist shell json htmlsanitizer taggable-mixin gviz" + ranklist shell json htmlsanitizer taggable gviz" DEFAULT_ZIP_FILES="tiny_mce.zip" APP_BUILD=${APP_BUILD:-"${DEFAULT_APP_BUILD}"}