5 | {% endblock %}
6 |
7 | {% block content %}
8 | {% if artist and lyrics %}
9 | {% for lyric in lyrics %}
10 | {% include "lyric.html" %}
11 | {% endfor %}
12 | {% else %}
13 |
No lyrics found by that artist.
14 | {% endif %}
15 | {% endblock %}
--------------------------------------------------------------------------------
/models.py:
--------------------------------------------------------------------------------
1 | from google.appengine.ext import db
2 | from google.appengine.api import users
3 |
4 | class Lyric(db.Model):
5 | body = db.TextProperty()
6 | artist = db.StringProperty()
7 | album = db.StringProperty()
8 | ASIN = db.StringProperty()
9 | song = db.StringProperty()
10 | user = db.UserProperty()
11 | key_id = db.IntegerProperty()
12 | date = db.DateTimeProperty(auto_now_add=True)
13 |
14 | class Favorite(db.Model):
15 | user = db.UserProperty(required=True)
16 | lyric = db.ReferenceProperty(Lyric)
17 | date = db.DateTimeProperty(auto_now_add=True)
18 |
--------------------------------------------------------------------------------
/betterhandler.py:
--------------------------------------------------------------------------------
1 | from google.appengine.ext import webapp
2 | from google.appengine.api import users
3 | import os
4 |
5 | class BetterHandler(webapp.RequestHandler):
6 | def template_path(self, filename):
7 | return os.path.join(os.path.dirname(__file__), 'templates', filename)
8 |
9 | def template_values(self, extra_dict=None):
10 | standard_values = {
11 | 'user': users.get_current_user(),
12 | 'login_url': users.create_login_url('/'),
13 | 'logout_url': users.create_logout_url('/'),
14 | }
15 |
16 | if extra_dict:
17 | standard_values.update(extra_dict)
18 |
19 | return standard_values
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | lyricswelove test suite
3 |
4 | The test suite requires the following::
5 |
6 | easy_install nose WebTest http://nose-gae.googlecode.com/svn/trunk/
7 |
8 | The last one is a path to the noseGAE plugin (see http://code.google.com/p/nose-gae/) and it must be revision 25 or higher (currently 0.1.3a).
9 | noseGAE sets up everything you need to run the pypione app in a simulated Google App Engine environment.
10 |
11 | NOTE: as of this writing, issue6 (http://code.google.com/p/nose-gae/issues/detail?id=6) means you'll have to manually delete WebOb from your site-packages to get noseGAE to work.
12 |
13 | Run the test suite with the nosetests (or nosetests-2.5) command from the root directory.
14 | """
--------------------------------------------------------------------------------
/javascripts/app.js:
--------------------------------------------------------------------------------
1 | var LWL;
2 |
3 | if (!LWL) LWL = {};
4 |
5 | /*
6 | Text used in the help tooltips for the form
7 | */
8 | LWL.help = {
9 | body :"The lyrics you would like to share",
10 | artist :"The artist who wrote these lyrics",
11 | song :"The song in which these lyrics appear",
12 | album :"Optional. The album where the song appears",
13 | ASIN :"Optional. The Amazon ASIN code for the album or song",
14 | }
15 |
16 |
17 | LWL.initFormHelp = function() {
18 |
19 | for (fieldname in LWL.help) {
20 | $("#"+fieldname+"-field").attr('title', LWL.help[fieldname]);
21 | $("#"+fieldname+"-field").inputHintBox({
22 | className:'tooltip',
23 | source:'attr',
24 | attr:'title',
25 | incrementLeft:100,
26 | incrementTop:37,
27 | attachTo:'#wrapper'});
28 | }
29 |
30 | }
--------------------------------------------------------------------------------
/templates/lyric.html:
--------------------------------------------------------------------------------
1 |
2 |
{{ lyric.body|linebreaks }}
3 |
4 |
5 | from "{{ lyric.song }}"
6 |
7 | {% if lyric.album %}
8 | {% if lyric.ASIN %}
9 | on {{ lyric.album }}
10 | {% else %}
11 | on {{ lyric.album }}
12 | {% endif %}
13 | {% endif %}
14 |
15 | by {{ lyric.artist }}
16 | shared by {{ lyric.user }}
17 | {{ lyric.date|timesince }} ago
18 |
19 |
element will be created
34 | * with @className as class.
35 | *
36 | * If user click on the box to select some text (for copy/paste for example), box will not disappear.
37 | *
38 | * If you need to make the box appear in more left, use incrementLeft, same for top - incrementTop,
39 | * you can use - sign if you want to decrement.
40 | **/
41 |
42 | /**
43 | * Example, you have a shiny div and you want to use it as a Shell for hint messages
44 | *
45 | *
46 | *
47 | *
48 | *
49 | *
50 | * You have a inputs like:
51 | *
52 | *
53 | *
54 | * Here is an example of js to use:
55 | * $('.titleHintBox').inputHintBox({div:$('#shiny_box'),div_sub:'.shiny_box_body',source:'attr',attr:'title',incrementLeft:12,incrementTop:-12});
56 | **/
57 |
58 | /**
59 | * Provide a hint box near input as a absolute positioned div.
60 | * @name InputHintBox
61 | * @cat Plugins/Forms
62 | * @type $
63 | * @param Map options Optional settings
64 | * @option jQueryDom @div box to show, if this is set then className do not apply
65 | * @option String @div_sub css selector, use this when you need to write the Dynamic html into a element Inside the @div box,
66 | example: .body, this will search for .body in context of @div
67 | * @option String @className This class will be added to the dynamic created div box. Default: "input_hint_box"
68 | * @option String @source Source of box message text html: attr | html, Default: "attr"
69 | * @option String @attr If @source = "attr" then html will be taken from the attribute named @attr. Default: "title"
70 | * @option String @html If @source = "html" them html will be taken from @html
71 | * @option Integer @incrementLeft This value will be incremented to the left property of the absolute positioned hint box. Default: 10
72 | * @option Integer @incrementTop This value will be incremented to the top property of the absolute positioned hint box. Default: 10
73 | * @option String @attachTo Hint box will be appended to this. Default: "body"
74 | */
75 |
76 | (function($) {
77 | $.fn.inputHintBox = function(options) {
78 | options = $.extend({}, $.inputHintBoxer.defaults, options);
79 |
80 | this.each(function(){
81 | new $.inputHintBoxer(this,options);
82 | });
83 | return this;
84 | }
85 |
86 | $.inputHintBoxer = function(input, options) {
87 | var $guideObject,$input = $guideObject = $(input), box, boxMouseDown = false;
88 |
89 | //$guideObject - in left of this object hint box will be positioned
90 |
91 | // If @type=radio then it must be inside a label and we should put the hint box in the right side of the label
92 | if ( ($input.attr('type') == 'radio' || $input.attr('type') == 'checkbox') && $input.parent().is('label') ) {
93 | $guideObject = $( $input.parent() );
94 | }
95 |
96 |
97 | function init() {
98 | var boxHtml = options.html != ''?options.html:
99 | options.source == 'attr'?$input.attr(options.attr): '';
100 |
101 | if (typeof boxHtml === "undefined") boxHtml = '';
102 | box = options.div != '' ? options.div.clone() : $("").addClass(options.className);
103 | box = box.css('display','none').addClass('_hintBox').appendTo(options.attachTo);
104 |
105 | if (options.div_sub == '') box.html(boxHtml);
106 | else $(options.div_sub,box).html(boxHtml);
107 |
108 | $input.focus(function() {
109 | $('body').mousedown(global_mousedown_listener);
110 | show();
111 | }).blur(function(){
112 | prepare_hide();
113 | });
114 | }
115 |
116 | // This is evaluated each time to prevent probs with elements with display none
117 | function align() {
118 | var offsets = $guideObject.position(),
119 | left = offsets.left + $guideObject.width() + options.incrementLeft + 5 + ($.browser.safari?5:($.browser.msie?10:($.browser.mozilla?6:0))),
120 | top = offsets.top + options.incrementTop + ($.browser.msie?14:($.browser.mozilla?8:0));
121 | box.css({position:"absolute",top:top,left:left});
122 | }
123 |
124 | function show() {
125 | align();
126 | box.fadeIn('fast');
127 | }
128 |
129 | function prepare_hide() {
130 | // We want to allow user to select and copy/paste content from the box
131 | // So delay a bit to see where user click
132 | // $('body').click(global_click_listener);
133 | // if (boxMouseDown) return;
134 | // $.inputHintBoxer.mostRecentHideTimer = setTimeout(function(){hide()},300);
135 | hide();
136 | }
137 |
138 | var global_click_listener = function(e) {
139 | var $e = $(e.target),c='._hintBox';
140 | clearTimeout($.inputHintBoxer.mostRecentHideTimer);
141 | if ($e.parents(c).length == 0 && $e.is(c) == false) hide();
142 | };
143 |
144 | // Prevent hiding when selecting..
145 | // When user Select a text to select, a Mousedown is fired BEFORE blur of input
146 | // This why we need to know when a Mousedown is done to our object
147 | var global_mousedown_listener = function(e) {
148 | var $e = $(e.target),c='._hintBox';
149 | boxMouseDown = ($e.parents(c).length != 0 || $e.is(c) != false);
150 | }
151 |
152 | function hide() {
153 | $('body').unbind('click',global_click_listener);
154 | $('body').unbind('mousedown',global_mousedown_listener);
155 | align();
156 | box.fadeOut('fast');
157 | }
158 |
159 | init();
160 | return {}
161 | };
162 |
163 | $.inputHintBoxer.mostRecentHideTimer = 0;
164 |
165 | $.inputHintBoxer.defaults = {
166 | div: '',
167 | className: 'input_hint_box',
168 | source: 'attr', // attr or html
169 | div_sub: '', // Where to write
170 | attr: 'title',
171 | html: '',
172 | incrementLeft: 10,
173 | incrementTop: 0,
174 | attachTo: 'body'
175 | }
176 |
177 | })(jQuery);
--------------------------------------------------------------------------------
/javascripts/jquery-1.2.3.min.js:
--------------------------------------------------------------------------------
1 | /*
2 | * jQuery 1.2.3 - New Wave Javascript
3 | *
4 | * Copyright (c) 2008 John Resig (jquery.com)
5 | * Dual licensed under the MIT (MIT-LICENSE.txt)
6 | * and GPL (GPL-LICENSE.txt) licenses.
7 | *
8 | * $Date: 2008-02-06 00:21:25 -0500 (Wed, 06 Feb 2008) $
9 | * $Rev: 4663 $
10 | */
11 | (function(){if(window.jQuery)var _jQuery=window.jQuery;var jQuery=window.jQuery=function(selector,context){return new jQuery.prototype.init(selector,context);};if(window.$)var _$=window.$;window.$=jQuery;var quickExpr=/^[^<]*(<(.|\s)+>)[^>]*$|^#(\w+)$/;var isSimple=/^.[^:#\[\.]*$/;jQuery.fn=jQuery.prototype={init:function(selector,context){selector=selector||document;if(selector.nodeType){this[0]=selector;this.length=1;return this;}else if(typeof selector=="string"){var match=quickExpr.exec(selector);if(match&&(match[1]||!context)){if(match[1])selector=jQuery.clean([match[1]],context);else{var elem=document.getElementById(match[3]);if(elem)if(elem.id!=match[3])return jQuery().find(selector);else{this[0]=elem;this.length=1;return this;}else
12 | selector=[];}}else
13 | return new jQuery(context).find(selector);}else if(jQuery.isFunction(selector))return new jQuery(document)[jQuery.fn.ready?"ready":"load"](selector);return this.setArray(selector.constructor==Array&&selector||(selector.jquery||selector.length&&selector!=window&&!selector.nodeType&&selector[0]!=undefined&&selector[0].nodeType)&&jQuery.makeArray(selector)||[selector]);},jquery:"1.2.3",size:function(){return this.length;},length:0,get:function(num){return num==undefined?jQuery.makeArray(this):this[num];},pushStack:function(elems){var ret=jQuery(elems);ret.prevObject=this;return ret;},setArray:function(elems){this.length=0;Array.prototype.push.apply(this,elems);return this;},each:function(callback,args){return jQuery.each(this,callback,args);},index:function(elem){var ret=-1;this.each(function(i){if(this==elem)ret=i;});return ret;},attr:function(name,value,type){var options=name;if(name.constructor==String)if(value==undefined)return this.length&&jQuery[type||"attr"](this[0],name)||undefined;else{options={};options[name]=value;}return this.each(function(i){for(name in options)jQuery.attr(type?this.style:this,name,jQuery.prop(this,options[name],type,i,name));});},css:function(key,value){if((key=='width'||key=='height')&&parseFloat(value)<0)value=undefined;return this.attr(key,value,"curCSS");},text:function(text){if(typeof text!="object"&&text!=null)return this.empty().append((this[0]&&this[0].ownerDocument||document).createTextNode(text));var ret="";jQuery.each(text||this,function(){jQuery.each(this.childNodes,function(){if(this.nodeType!=8)ret+=this.nodeType!=1?this.nodeValue:jQuery.fn.text([this]);});});return ret;},wrapAll:function(html){if(this[0])jQuery(html,this[0].ownerDocument).clone().insertBefore(this[0]).map(function(){var elem=this;while(elem.firstChild)elem=elem.firstChild;return elem;}).append(this);return this;},wrapInner:function(html){return this.each(function(){jQuery(this).contents().wrapAll(html);});},wrap:function(html){return this.each(function(){jQuery(this).wrapAll(html);});},append:function(){return this.domManip(arguments,true,false,function(elem){if(this.nodeType==1)this.appendChild(elem);});},prepend:function(){return this.domManip(arguments,true,true,function(elem){if(this.nodeType==1)this.insertBefore(elem,this.firstChild);});},before:function(){return this.domManip(arguments,false,false,function(elem){this.parentNode.insertBefore(elem,this);});},after:function(){return this.domManip(arguments,false,true,function(elem){this.parentNode.insertBefore(elem,this.nextSibling);});},end:function(){return this.prevObject||jQuery([]);},find:function(selector){var elems=jQuery.map(this,function(elem){return jQuery.find(selector,elem);});return this.pushStack(/[^+>] [^+>]/.test(selector)||selector.indexOf("..")>-1?jQuery.unique(elems):elems);},clone:function(events){var ret=this.map(function(){if(jQuery.browser.msie&&!jQuery.isXMLDoc(this)){var clone=this.cloneNode(true),container=document.createElement("div");container.appendChild(clone);return jQuery.clean([container.innerHTML])[0];}else
14 | return this.cloneNode(true);});var clone=ret.find("*").andSelf().each(function(){if(this[expando]!=undefined)this[expando]=null;});if(events===true)this.find("*").andSelf().each(function(i){if(this.nodeType==3)return;var events=jQuery.data(this,"events");for(var type in events)for(var handler in events[type])jQuery.event.add(clone[i],type,events[type][handler],events[type][handler].data);});return ret;},filter:function(selector){return this.pushStack(jQuery.isFunction(selector)&&jQuery.grep(this,function(elem,i){return selector.call(elem,i);})||jQuery.multiFilter(selector,this));},not:function(selector){if(selector.constructor==String)if(isSimple.test(selector))return this.pushStack(jQuery.multiFilter(selector,this,true));else
15 | selector=jQuery.multiFilter(selector,this);var isArrayLike=selector.length&&selector[selector.length-1]!==undefined&&!selector.nodeType;return this.filter(function(){return isArrayLike?jQuery.inArray(this,selector)<0:this!=selector;});},add:function(selector){return!selector?this:this.pushStack(jQuery.merge(this.get(),selector.constructor==String?jQuery(selector).get():selector.length!=undefined&&(!selector.nodeName||jQuery.nodeName(selector,"form"))?selector:[selector]));},is:function(selector){return selector?jQuery.multiFilter(selector,this).length>0:false;},hasClass:function(selector){return this.is("."+selector);},val:function(value){if(value==undefined){if(this.length){var elem=this[0];if(jQuery.nodeName(elem,"select")){var index=elem.selectedIndex,values=[],options=elem.options,one=elem.type=="select-one";if(index<0)return null;for(var i=one?index:0,max=one?index+1:options.length;i=0||jQuery.inArray(this.name,value)>=0);else if(jQuery.nodeName(this,"select")){var values=value.constructor==Array?value:[value];jQuery("option",this).each(function(){this.selected=(jQuery.inArray(this.value,values)>=0||jQuery.inArray(this.text,values)>=0);});if(!values.length)this.selectedIndex=-1;}else
17 | this.value=value;});},html:function(value){return value==undefined?(this.length?this[0].innerHTML:null):this.empty().append(value);},replaceWith:function(value){return this.after(value).remove();},eq:function(i){return this.slice(i,i+1);},slice:function(){return this.pushStack(Array.prototype.slice.apply(this,arguments));},map:function(callback){return this.pushStack(jQuery.map(this,function(elem,i){return callback.call(elem,i,elem);}));},andSelf:function(){return this.add(this.prevObject);},data:function(key,value){var parts=key.split(".");parts[1]=parts[1]?"."+parts[1]:"";if(value==null){var data=this.triggerHandler("getData"+parts[1]+"!",[parts[0]]);if(data==undefined&&this.length)data=jQuery.data(this[0],key);return data==null&&parts[1]?this.data(parts[0]):data;}else
18 | return this.trigger("setData"+parts[1]+"!",[parts[0],value]).each(function(){jQuery.data(this,key,value);});},removeData:function(key){return this.each(function(){jQuery.removeData(this,key);});},domManip:function(args,table,reverse,callback){var clone=this.length>1,elems;return this.each(function(){if(!elems){elems=jQuery.clean(args,this.ownerDocument);if(reverse)elems.reverse();}var obj=this;if(table&&jQuery.nodeName(this,"table")&&jQuery.nodeName(elems[0],"tr"))obj=this.getElementsByTagName("tbody")[0]||this.appendChild(this.ownerDocument.createElement("tbody"));var scripts=jQuery([]);jQuery.each(elems,function(){var elem=clone?jQuery(this).clone(true)[0]:this;if(jQuery.nodeName(elem,"script")){scripts=scripts.add(elem);}else{if(elem.nodeType==1)scripts=scripts.add(jQuery("script",elem).remove());callback.call(obj,elem);}});scripts.each(evalScript);});}};jQuery.prototype.init.prototype=jQuery.prototype;function evalScript(i,elem){if(elem.src)jQuery.ajax({url:elem.src,async:false,dataType:"script"});else
19 | jQuery.globalEval(elem.text||elem.textContent||elem.innerHTML||"");if(elem.parentNode)elem.parentNode.removeChild(elem);}jQuery.extend=jQuery.fn.extend=function(){var target=arguments[0]||{},i=1,length=arguments.length,deep=false,options;if(target.constructor==Boolean){deep=target;target=arguments[1]||{};i=2;}if(typeof target!="object"&&typeof target!="function")target={};if(length==1){target=this;i=0;}for(;i-1;}},swap:function(elem,options,callback){var old={};for(var name in options){old[name]=elem.style[name];elem.style[name]=options[name];}callback.call(elem);for(var name in options)elem.style[name]=old[name];},css:function(elem,name,force){if(name=="width"||name=="height"){var val,props={position:"absolute",visibility:"hidden",display:"block"},which=name=="width"?["Left","Right"]:["Top","Bottom"];function getWH(){val=name=="width"?elem.offsetWidth:elem.offsetHeight;var padding=0,border=0;jQuery.each(which,function(){padding+=parseFloat(jQuery.curCSS(elem,"padding"+this,true))||0;border+=parseFloat(jQuery.curCSS(elem,"border"+this+"Width",true))||0;});val-=Math.round(padding+border);}if(jQuery(elem).is(":visible"))getWH();else
23 | jQuery.swap(elem,props,getWH);return Math.max(0,val);}return jQuery.curCSS(elem,name,force);},curCSS:function(elem,name,force){var ret;function color(elem){if(!jQuery.browser.safari)return false;var ret=document.defaultView.getComputedStyle(elem,null);return!ret||ret.getPropertyValue("color")=="";}if(name=="opacity"&&jQuery.browser.msie){ret=jQuery.attr(elem.style,"opacity");return ret==""?"1":ret;}if(jQuery.browser.opera&&name=="display"){var save=elem.style.outline;elem.style.outline="0 solid black";elem.style.outline=save;}if(name.match(/float/i))name=styleFloat;if(!force&&elem.style&&elem.style[name])ret=elem.style[name];else if(document.defaultView&&document.defaultView.getComputedStyle){if(name.match(/float/i))name="float";name=name.replace(/([A-Z])/g,"-$1").toLowerCase();var getComputedStyle=document.defaultView.getComputedStyle(elem,null);if(getComputedStyle&&!color(elem))ret=getComputedStyle.getPropertyValue(name);else{var swap=[],stack=[];for(var a=elem;a&&color(a);a=a.parentNode)stack.unshift(a);for(var i=0;i]*?)\/>/g,function(all,front,tag){return tag.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i)?all:front+">"+tag+">";});var tags=jQuery.trim(elem).toLowerCase(),div=context.createElement("div");var wrap=!tags.indexOf("",""]||!tags.indexOf("",""]||tags.match(/^<(thead|tbody|tfoot|colg|cap)/)&&[1,"