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.
-
-
- - - - - - - - - -