diff --git a/public/assets/jsmerchant/products.zip b/public/assets/jsmerchant/products.zip new file mode 100644 index 000000000..290c14a37 Binary files /dev/null and b/public/assets/jsmerchant/products.zip differ diff --git a/public/assets/jsmerchant/styles.css b/public/assets/jsmerchant/styles.css new file mode 100644 index 000000000..fa21cf202 --- /dev/null +++ b/public/assets/jsmerchant/styles.css @@ -0,0 +1,230 @@ +html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:baseline}body{line-height:1}ol,ul{list-style:none}table{border-collapse:collapse;border-spacing:0}caption,th,td{text-align:left;font-weight:normal;vertical-align:middle}q,blockquote{quotes:none}q:before,q:after,blockquote:before,blockquote:after{content:"";content:none}a img{border:none}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}html{background:#d3dd8a;-moz-background-size:80px 80px;-webkit-background-size:80px 80px;-o-background-size:80px 80px;background-size:80px 80px;background-image:-webkit-gradient(linear, 0% 0%, 100% 100%, color-stop(25%, rgba(255,255,255,0.1)), color-stop(25%, rgba(255,255,255,0)), color-stop(50%, rgba(255,255,255,0)), color-stop(50%, rgba(255,255,255,0.1)), color-stop(75%, rgba(255,255,255,0.1)), color-stop(75%, rgba(255,255,255,0)), color-stop(100%, rgba(255,255,255,0)));background-image:-webkit-linear-gradient(left top, rgba(255,255,255,0.1) 25%,rgba(255,255,255,0) 25%,rgba(255,255,255,0) 50%,rgba(255,255,255,0.1) 50%,rgba(255,255,255,0.1) 75%,rgba(255,255,255,0) 75%,rgba(255,255,255,0));background-image:-moz-linear-gradient(left top, rgba(255,255,255,0.1) 25%,rgba(255,255,255,0) 25%,rgba(255,255,255,0) 50%,rgba(255,255,255,0.1) 50%,rgba(255,255,255,0.1) 75%,rgba(255,255,255,0) 75%,rgba(255,255,255,0));background-image:-o-linear-gradient(left top, rgba(255,255,255,0.1) 25%,rgba(255,255,255,0) 25%,rgba(255,255,255,0) 50%,rgba(255,255,255,0.1) 50%,rgba(255,255,255,0.1) 75%,rgba(255,255,255,0) 75%,rgba(255,255,255,0));background-image:linear-gradient(left top, rgba(255,255,255,0.1) 25%,rgba(255,255,255,0) 25%,rgba(255,255,255,0) 50%,rgba(255,255,255,0.1) 50%,rgba(255,255,255,0.1) 75%,rgba(255,255,255,0) 75%,rgba(255,255,255,0))}body{font-family:"Helvetica Neue", Helvetica, Arial, sans;background:#fff;margin:50px auto 30px;color:#2d4159;padding:20px;width:85%;min-width:600px;max-width:1250px;min-height:380px;position:relative;line-height:1.25em;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;-moz-border-radius:6px;-webkit-border-radius:6px;-o-border-radius:6px;-ms-border-radius:6px;-khtml-border-radius:6px;border-radius:6px;-moz-background-clip:padding;-webkit-background-clip:padding;-o-background-clip:padding-box;-ms-background-clip:padding-box;-khtml-background-clip:padding-box;background-clip:padding-box}body:before{content:" ";z-index:-2;position:absolute;left:-4px;right:8px;top:8px;height:260px;background:#2b445c;-moz-border-radius:6px;-webkit-border-radius:6px;-o-border-radius:6px;-ms-border-radius:6px;-khtml-border-radius:6px;border-radius:6px;-moz-box-shadow:rgba(0, 0, 0, 0.15) -2px 3px 3px;-webkit-box-shadow:rgba(0, 0, 0, 0.15) -2px 3px 3px;-o-box-shadow:rgba(0, 0, 0, 0.15) -2px 3px 3px;box-shadow:rgba(0, 0, 0, 0.15) -2px 3px 3px;-moz-background-clip:padding;-webkit-background-clip:padding;-o-background-clip:padding-box;-ms-background-clip:padding-box;-khtml-background-clip:padding-box;background-clip:padding-box;-moz-transform:rotate(-2deg);-webkit-transform:rotate(-2deg);-o-transform:rotate(-2deg);-ms-transform:rotate(-2deg);transform:rotate(-2deg)}body:after{content:" ";position:absolute;top:0;bottom:0;left:0;right:0;bottom:2px;-moz-border-radius:6px;-webkit-border-radius:6px;-o-border-radius:6px;-ms-border-radius:6px;-khtml-border-radius:6px;border-radius:6px;background:#000;z-index:-1;-moz-box-shadow:rgba(0, 0, 0, 0.3) 0 2px 5px 1px;-webkit-box-shadow:rgba(0, 0, 0, 0.3) 0 2px 5px 1px;-o-box-shadow:rgba(0, 0, 0, 0.3) 0 2px 5px 1px;box-shadow:rgba(0, 0, 0, 0.3) 0 2px 5px 1px}#container{position:relative;z-index:1}.people .person:first-child:after{content:" ";background:#fff;position:absolute;left:0;right:0;top:90px;height:95%;min-height:270px;display:block;font-size:1.8em;z-index:-1}.people:before{content:"No one is here. You should probably fix that :)";text-align:center;line-height:240px;font-size:1.8em;position:absolute;top:100px;left:0;right:0;height:86%;min-height:260px;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;z-index:-1;background:#eee;color:#aaa;border:1px dashed #ddd;-moz-border-radius:1em;-webkit-border-radius:1em;-o-border-radius:1em;-ms-border-radius:1em;-khtml-border-radius:1em;border-radius:1em;text-shadow:#fff 1px 1px 0}.fb-reset,input[type=submit]{display:-moz-inline-box;-moz-box-orient:vertical;display:inline-block;vertical-align:middle;*vertical-align:auto;background:url('/images/button_bg.png?1303501257') repeat-x bottom left;margin:0;width:auto;line-height:1.3em;overflow:visible;cursor:pointer;text-decoration:none;font-size:1em;padding:0.1em 0.5em;border:0;-moz-user-select:none;-webkit-user-select:none;-o-user-select:none;-ms-user-select:none;-khtml-user-select:none;user-select:none}.fb-reset,input[type=submit]{*display:inline}.fb-reset::-moz-focus-inner,input[type=submit]::-moz-focus-inner{border:none;padding:0}.fb-reset:focus,input[type=submit]:focus{outline:none}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}.headings,h1{font-family:"Yanone Kaffeesatz", sans}a{color:#2d4159}a:hover{color:#233245}.flash{-moz-box-shadow:rgba(0, 0, 0, 0.5) 0 1px 3px;-webkit-box-shadow:rgba(0, 0, 0, 0.5) 0 1px 3px;-o-box-shadow:rgba(0, 0, 0, 0.5) 0 1px 3px;box-shadow:rgba(0, 0, 0, 0.5) 0 1px 3px;-moz-border-radius-bottomleft:6px;-webkit-border-bottom-left-radius:6px;-o-border-bottom-left-radius:6px;-ms-border-bottom-left-radius:6px;-khtml-border-bottom-left-radius:6px;border-bottom-left-radius:6px;-moz-border-radius-bottomright:6px;-webkit-border-bottom-right-radius:6px;-o-border-bottom-right-radius:6px;-ms-border-bottom-right-radius:6px;-khtml-border-bottom-right-radius:6px;border-bottom-right-radius:6px;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;background-image:-webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, rgba(0,0,0,0.4)), color-stop(100%, rgba(0,0,0,0.6)));background-image:-webkit-linear-gradient(rgba(0,0,0,0.4),rgba(0,0,0,0.6));background-image:-moz-linear-gradient(rgba(0,0,0,0.4),rgba(0,0,0,0.6));background-image:-o-linear-gradient(rgba(0,0,0,0.4),rgba(0,0,0,0.6));background-image:linear-gradient(rgba(0,0,0,0.4),rgba(0,0,0,0.6));border:3px solid rgba(255, 255, 255, 0.5);-moz-background-clip:padding;-webkit-background-clip:padding;-o-background-clip:padding-box;-ms-background-clip:padding-box;-khtml-background-clip:padding-box;background-clip:padding-box;text-shadow:rgba(0, 0, 0, 0.6) 0 -1px;color:#fff;margin:0 auto;position:absolute;top:-51px;left:0;padding:.4em .8em;width:100%;text-align:center;border-top:0}.flash:empty{height:0px;overflow:hidden;padding:0;border-width:0;-moz-box-shadow:none;-webkit-box-shadow:none;-o-box-shadow:none;box-shadow:none;background:none}h1{-moz-border-radius-topleft:6px;-webkit-border-top-left-radius:6px;-o-border-top-left-radius:6px;-ms-border-top-left-radius:6px;-khtml-border-top-left-radius:6px;border-top-left-radius:6px;-moz-border-radius-topright:6px;-webkit-border-top-right-radius:6px;-o-border-top-right-radius:6px;-ms-border-top-right-radius:6px;-khtml-border-top-right-radius:6px;border-top-right-radius:6px;background-image:-webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #42678d), color-stop(100%, #223447));background-image:-webkit-linear-gradient(#42678d,#223447);background-image:-moz-linear-gradient(#42678d,#223447);background-image:-o-linear-gradient(#42678d,#223447);background-image:linear-gradient(#42678d,#223447);-moz-box-shadow:rgba(255, 255, 255, 0.15) 0 1px inset, white 0 1px;-webkit-box-shadow:rgba(255, 255, 255, 0.15) 0 1px inset, white 0 1px;-o-box-shadow:rgba(255, 255, 255, 0.15) 0 1px inset, white 0 1px;box-shadow:rgba(255, 255, 255, 0.15) 0 1px inset, white 0 1px;margin:-20px -20px 15px;padding:20px;padding-right:180px;line-height:1em;font-weight:normal;color:#fff;position:relative;text-shadow:#192836 0 -2px;font-size:2.5em}h1:after{content:" ";position:absolute;left:0;right:0;height:15px;bottom:-16px;border-top:1px solid #e5e5e5;background-image:-webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, rgba(170,170,170,0.5)), color-stop(100%, rgba(255,255,255,0)));background-image:-webkit-linear-gradient(rgba(170,170,170,0.5),rgba(255,255,255,0));background-image:-moz-linear-gradient(rgba(170,170,170,0.5),rgba(255,255,255,0));background-image:-o-linear-gradient(rgba(170,170,170,0.5),rgba(255,255,255,0));background-image:linear-gradient(rgba(170,170,170,0.5),rgba(255,255,255,0));z-index:1}h2,h3,h4{color:#2d4159;font-size:1.3em;line-height:1.5em;margin:.2em 0 .5em}h1,h2,h3,h4,h5,h6{font-weight:bold}.top-button,h1 + p a[href$="new"],.actions a[href$='edit'],.actions a[data-method='delete']{position:absolute;top:26px;right:0px;display:inline-block;padding:4px 11px;font-size:.9em;-moz-box-shadow:rgba(255, 255, 255, 0.4) 0 -1px inset;-webkit-box-shadow:rgba(255, 255, 255, 0.4) 0 -1px inset;-o-box-shadow:rgba(255, 255, 255, 0.4) 0 -1px inset;box-shadow:rgba(255, 255, 255, 0.4) 0 -1px inset;-moz-border-radius:1.2em;-webkit-border-radius:1.2em;-o-border-radius:1.2em;-ms-border-radius:1.2em;-khtml-border-radius:1.2em;border-radius:1.2em;background-image:-webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffffff), color-stop(100%, #cccccc));background-image:-webkit-linear-gradient(#ffffff,#cccccc);background-image:-moz-linear-gradient(#ffffff,#cccccc);background-image:-o-linear-gradient(#ffffff,#cccccc);background-image:linear-gradient(#ffffff,#cccccc);text-shadow:#fff 0 1px;color:#999999;text-decoration:none}.top-button:hover,h1 + p a[href$="new"]:hover,.actions a[href$='edit']:hover,.actions a[data-method='delete']:hover{color:#555}.top-button:active,h1 + p a[href$="new"]:active,.actions a[href$='edit']:active,.actions a[data-method='delete']:active{background-image:-webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #d9d9d9), color-stop(100%, #f5f5f5));background-image:-webkit-linear-gradient(#d9d9d9,#f5f5f5);background-image:-moz-linear-gradient(#d9d9d9,#f5f5f5);background-image:-o-linear-gradient(#d9d9d9,#f5f5f5);background-image:linear-gradient(#d9d9d9,#f5f5f5)}#articles{margin:-15px -20px -20px;padding:0 20px 0;min-width:550px;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;position:relative;-moz-border-radius-bottomleft:6px;-webkit-border-bottom-left-radius:6px;-o-border-bottom-left-radius:6px;-ms-border-bottom-left-radius:6px;-khtml-border-bottom-left-radius:6px;border-bottom-left-radius:6px;-moz-border-radius-bottomright:6px;-webkit-border-bottom-right-radius:6px;-o-border-bottom-right-radius:6px;-ms-border-bottom-right-radius:6px;-khtml-border-bottom-right-radius:6px;border-bottom-right-radius:6px;border-right:1px solid #e2e2e2}#articles li{border-bottom:1px dashed #e5e5e5;padding:1em 0 .8em;margin-right:20px;font-size:1.2em;position:relative}#articles li:last-child{-moz-border-radius-bottomleft:6px;-webkit-border-bottom-left-radius:6px;-o-border-bottom-left-radius:6px;-ms-border-bottom-left-radius:6px;-khtml-border-bottom-left-radius:6px;border-bottom-left-radius:6px;border-bottom:0}#articles li > a:first-child{font-weight:bold;display:block;color:#2d4159;padding-bottom:.1em}#articles .actions{border-left:1px dotted #ddd;padding-left:8px;margin-left:3px;line-height:1em;position:relative;top:2px}#articles .actions a{-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=20)";filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=20);opacity:0.2}#articles .actions a:hover{-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=40)";filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=40);opacity:0.4}#articles .tag_list{display:inline-block;font-size:.8em;padding-top:.5em;color:#999}.actions{border-bottom:1px dashed #ddd;margin-bottom:.7em;padding-bottom:.7em;font-size:.9em;overflow:hidden;*zoom:1;text-align:right}.actions a{float:left;color:#999999;padding-right:.5em}.actions a[href$='edit'],.actions a[data-method='delete']{text-decoration:none;float:none;margin-left:.5em;padding:4px 12px 3px 8px}.actions a[href$='edit']:before,.actions a[data-method='delete']:before{-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=20)";filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=20);opacity:0.2;position:relative}.actions a[href$='edit']:hover:before,.actions a[data-method='delete']:hover:before{-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=60)";filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=60);opacity:0.6}.actions a[data-method='delete']{right:90px}.actions a[data-method='delete']:before{content:" ";display:inline-block;background:url('/images/destroy.png?1294445296') no-repeat;width:20px;height:16px;top:2px}.actions a[href$='edit']:before{content:" ";display:inline-block;background:url('/images/edit.png?1294445467') no-repeat;width:20px;height:16px;top:2px}.input,form input[type=text],form textarea{width:100%;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;-moz-border-radius:4px;-webkit-border-radius:4px;-o-border-radius:4px;-ms-border-radius:4px;-khtml-border-radius:4px;border-radius:4px;padding:.3em .4em;border:1px solid #ccc;background-color:#f5f5f5;color:#1f2d3e;font-size:1.3em}.input:focus,form input[type=text]:focus,form textarea:focus{outline:0;-moz-box-shadow:rgba(129, 177, 223, 0.7) 0 0 5px 1px inset;-webkit-box-shadow:rgba(129, 177, 223, 0.7) 0 0 5px 1px inset;-o-box-shadow:rgba(129, 177, 223, 0.7) 0 0 5px 1px inset;box-shadow:rgba(129, 177, 223, 0.7) 0 0 5px 1px inset;border-color:#81b1df;background-color:#fff}form input[type=text]{margin:0}form p{padding:0;margin-bottom:.6em}form label{color:#6c7c9a;font-size:.9em;font-weight:bold;line-height:1.5em;padding-bottom:.2em;display:inline-block;text-transform:uppercase}form textarea{height:12em;font-size:1.1em}input[type=submit]{-moz-box-shadow:rgba(255, 255, 255, 0.25) 0 1px 0 0 inset, rgba(0, 0, 0, 0.3) 0 1px 2px 0;-webkit-box-shadow:rgba(255, 255, 255, 0.25) 0 1px 0 0 inset, rgba(0, 0, 0, 0.3) 0 1px 2px 0;-o-box-shadow:rgba(255, 255, 255, 0.25) 0 1px 0 0 inset, rgba(0, 0, 0, 0.3) 0 1px 2px 0;box-shadow:rgba(255, 255, 255, 0.25) 0 1px 0 0 inset, rgba(0, 0, 0, 0.3) 0 1px 2px 0;-moz-border-radius:1em;-webkit-border-radius:1em;-o-border-radius:1em;-ms-border-radius:1em;-khtml-border-radius:1em;border-radius:1em;border:1px solid #111b24;border-bottom-color:#0a1016;background-image:-webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #49719a), color-stop(50%, #324e6a), color-stop(100%, #283e55));background-image:-webkit-linear-gradient(#49719a 0%,#324e6a 50%,#283e55 100%);background-image:-moz-linear-gradient(#49719a 0%,#324e6a 50%,#283e55 100%);background-image:-o-linear-gradient(#49719a 0%,#324e6a 50%,#283e55 100%);background-image:linear-gradient(#49719a 0%,#324e6a 50%,#283e55 100%);text-shadow:rgba(12, 23, 35, 0.5) 0 -1px 0px;font-family:'lucida grande', lucida, sans-serif;background-color:#36699b;color:white;font-size:11px;padding:.2em .9em .3em;font-size:1.2em}input[type=submit]:hover,input[type=submit]:focus{background-image:-webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #3d6082), color-stop(50%, #304a65), color-stop(100%, #23374a));background-image:-webkit-linear-gradient(#3d6082 0%,#304a65 50%,#23374a 100%);background-image:-moz-linear-gradient(#3d6082 0%,#304a65 50%,#23374a 100%);background-image:-o-linear-gradient(#3d6082 0%,#304a65 50%,#23374a 100%);background-image:linear-gradient(#3d6082 0%,#304a65 50%,#23374a 100%);-moz-box-shadow:rgba(255, 255, 255, 0.25) 0 1px 0 0 inset;-webkit-box-shadow:rgba(255, 255, 255, 0.25) 0 1px 0 0 inset;-o-box-shadow:rgba(255, 255, 255, 0.25) 0 1px 0 0 inset;box-shadow:rgba(255, 255, 255, 0.25) 0 1px 0 0 inset;background-color:#324e6a}input[type=submit]:active{background-image:-webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #2a4259), color-stop(50%, #2e4862), color-stop(100%, #304a65));background-image:-webkit-linear-gradient(#2a4259 0%,#2e4862 50%,#304a65 100%);background-image:-moz-linear-gradient(#2a4259 0%,#2e4862 50%,#304a65 100%);background-image:-o-linear-gradient(#2a4259 0%,#2e4862 50%,#304a65 100%);background-image:linear-gradient(#2a4259 0%,#2e4862 50%,#304a65 100%);border:1px solid #090e13;border-bottom-color:#05080b;-moz-box-shadow:#1c2b3a 0 0 0.6em 0.3em inset;-webkit-box-shadow:#1c2b3a 0 0 0.6em 0.3em inset;-o-box-shadow:#1c2b3a 0 0 0.6em 0.3em inset;box-shadow:#1c2b3a 0 0 0.6em 0.3em inset;background-color:#2c455d} + +form{ + width: 70%; +} + +div.sidebar{ + display: none; +} + +div.header p, div.main h1{ + display: none; +} + +div#account{ + width: 20%; + float: right; + margin-top: 85px; + text-align: right; + padding: 8px 10px 5px 5px; + + -moz-border-radius: 6px; + -webkit-border-radius: 6px; + -o-border-radius: 6px; + -ms-border-radius: 6px; + -khtml-border-radius: 6px; + border-radius: 6px; + background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #42678D), color-stop(100%, #223447)); + background-image: -webkit-linear-gradient(#42678D,#223447); + background-image: -moz-linear-gradient(#42678D,#223447); + background-image: -o-linear-gradient(#42678D,#223447); + background-image: linear-gradient(#42678D,#223447); + -moz-box-shadow: rgba(255, 255, 255, 0.15) 0 1px inset, white 0 1px; + -webkit-box-shadow: rgba(255, 255, 255, 0.15) 0 1px inset, white 0 1px; + -o-box-shadow: rgba(255, 255, 255, 0.15) 0 1px inset, white 0 1px; + box-shadow: rgba(255, 255, 255, 0.15) 0 1px inset, white 0 1px; + line-height: 1em; + font-weight: normal; + color: white; + position: relative; + text-shadow: #192836 0 -2px; + font-size: 1em; +} + +div#account span{ +} + +div#account a{ + display: block; + color: #AAF; + text-decoration: none; + font-style: italic; + padding: 5px 0px 5px 0px; +} + +div#account a:hover{ + color: #EEF; +} + + +#new_product{ + margin: 10px 10px; +} + +.low_stock{ + font-weight: bold; + color: #cccc00; +} + +.out_stock{ + color: #cc0033; + font-weight: bold; +} + +.in_stock{ + color: #009900; + font-weight: bold; +} + + +.wrapper a { color: #000; } +.wrapper a:visited { color: #666; } +.wrapper a:hover { color: #fff; background-color:#000; } + +.fieldWithErrors { + padding: 2px; + background-color: red; + display: table; +} + +#errorExplanation { + width: 400px; + border: 2px solid red; + padding: 7px; + padding-bottom: 12px; + margin-bottom: 20px; + background-color: #f0f0f0; +} + +#errorExplanation h2 { + text-align: left; + font-weight: bold; + padding: 5px 5px 5px 15px; + font-size: 12px; + margin: -7px; + background-color: #c00; + color: #fff; +} + +#errorExplanation p { + color: #333; + margin-bottom: 0; + padding: 5px; +} + +#errorExplanation ul li { + font-size: 12px; + list-style: square; +} + +.main table{ + border-collapse: collapse; +} + +.main table th{ + text-align: left; + background: #EEE; + padding: 6px 10px 6px 10px; +} + +.main table td{ + border-top: 1px solid #CCC; + border-bottom: 1px solid #CCC; + padding: 6px 10px 6px 10px; +} + +.main span.product_title{ + display: block; + font-weight: bold; + font-size: 120%; +} + +.footer{ + text-align: right; + padding: 10px; + font-size: .8em; + font-style: italic; +} + + +pre { + background-color: #eee; + padding: 10px; + font-size: 11px; +} + +a#new_product{ + position: absolute; + top: 16px; + right: 20px; + display: inline-block; + padding: 4px 11px; + font-size: .9em; + -moz-box-shadow: rgba(255, 255, 255, 0.4) 0 -1px inset; + -webkit-box-shadow: rgba(255, 255, 255, 0.4) 0 -1px inset; + -o-box-shadow: rgba(255, 255, 255, 0.4) 0 -1px inset; + box-shadow: rgba(255, 255, 255, 0.4) 0 -1px inset; + -moz-border-radius: 1.2em; + -webkit-border-radius: 1.2em; + -o-border-radius: 1.2em; + -ms-border-radius: 1.2em; + -khtml-border-radius: 1.2em; + border-radius: 1.2em; + background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, white), color-stop(100%, #CCC)); + background-image: -moz-linear-gradient(white,#CCC); + background-image: linear-gradient(white,#CCC); + text-shadow: white 0 1px; + color: #999; + text-decoration: none; +} + +a:hover#new_product{ + color: #333; + text-shadow: white 0 0px; +} + +div.field_with_errors{ + display: inline; +} + +div.field_with_errors label{ + color: #A00; +} + +div.field_with_errors input{ + background: #FCC; +} + +.error_messages { + width: 400px; + border: 2px solid #CF0000; + padding: 0px; + padding-bottom: 12px; + margin-bottom: 20px; + background-color: #f0f0f0; + font-size: 12px; +} + +.error_messages h2 { + text-align: left; + font-weight: bold; + padding: 5px 10px; + font-size: 12px; + margin: 0; + background-color: #c00; + color: #fff; +} + +.error_messages p { + margin: 8px 10px; +} + +.error_messages ul{ + list-style-type: disc; + margin-left: 40px; +} + +.error_messages ul li{ + font-weight: bold; +} \ No newline at end of file diff --git a/public/projects/jsblogger.html b/public/projects/jsblogger.html new file mode 100644 index 000000000..59305f001 --- /dev/null +++ b/public/projects/jsblogger.html @@ -0,0 +1,1165 @@ + + + + + + + + JSBlogger - Jumpstart Lab Curriculum + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Jumpstart Lab Curriculum

+ +
+ +
+ +
+
+

In this project you’ll create a simple blog system and learn the basics of Ruby on Rails including:

+ +

The project will be developed in five iterations below. The last page, Completed Iterations, has zip files of the complete project after each iteration:

+

I0: Up and Running

+

Part of the reason Ruby on Rails became popular quickly is that it takes a lot of the hard work off your hands, and that’s especially true in starting up a project. Rails practices the idea of “sensible defaults” and tries to, with one command, create a working application ready for your customization.

+

Setting the Stage

+

First we need to make sure everything is setup and installed. See the Preparation for Rails Projects page for instructions on setting up and verifying your Ruby, Rails, and add-ons.

+

With that done we need to create new project in RubyMine. Open RubyMine and…

+ +

RubyMine will then create a Rails application for you and automatically open the file database.yml. If you were connecting to an existing database you would enter the database configuration parameters here. Since we’re using SQLite3 and starting from scratch, we can leave the defaults. Rails will automatically create our database for us.

+

Then go to the RUN menu in the menubar and click RUN. The first time you do this it’ll display the Run/Debug Configurations window. The defaults are all fine, but I enable the Run Browser checkbox near the bottom to automatically open the site in my default browser. You can UNCHECK the Display settings before launching checkbox, if present, then hit RUN.

+

You should then see the server window open at the bottom of RubyMine with your Mongrel webserver starting up. Once it’s started RubyMine will open your default browser. If it doesn’t open for some reason, try loading the address http://localhost:3000/. You should see Rails’ “Welcome Aboard” page. Click the “About your application’s environment” link and it’ll display the versions of all your installed components.

+

Creating the Article Model

+

Our blog will be centered around “articles,” so we’ll need a table in the database to store all the articles and a model to allow our Rails app to work with that data. We’ll use one of Rails’ generators to create the required files. Switch to your terminal and enter the following:

+
+cd JSBlogger
+rails generate model Article
+
+

We’re running the generate script, telling it to create a model, and naming that model Article. From that information, Rails creates the following files:

+ +

With those files in place we can start developing!

+

Working with the Database

+

Rails uses migration files to perform modifications to the database. Almost any modification you can make to a DB can be done through a migration. The killer feature about Rails migrations is that they’re generally database agnostic. When developing applications I usually use SQLite3 as we are in this tutorial, but when I deploy to my server it is running PostgreSQL. Many others choose MySQL. It doesn’t matter — the same migrations will work on all of them! This is an example of how Rails takes some of the painful work off your hands. You write your migrations once, then run them against almost any database.

+

What is a migration? Let’s open /db/migrate/(some_time_stamp)_create_articles.rb and take a look. First you’ll notice that the filename begins with a mish-mash of numbers which is a timestamp of when the migration was created. Migrations need to be ordered, so the timestamp serves to keep them in chronologic order. Inside the file, you’ll see two methods: self.up and self.down.

+

Migrations are meant to be symmetric. Whatever a migration changes inside the self.up method should be undone with the self.down method. Frequently in development you’ll think you want the database to look one way, then realize you need something different. You can create a migration to make your changes to the DB, start building, and then revert as necessary.

+

Inside the self.up method you’ll see the generator has placed a call to the create_table method, passed the symbol :articles as a parameter, and created a block with the variable t referencing the table that’s created. We can tell t what kind of columns we want in the articles table. Well, what kind of fields does our Article need to have? Since migrations make it easy to add or change columns later, we don’t need to think of EVERYTHING right now, we just need a few to get us rolling. Here’s a starter set:

+ +

That’s it! You might be wondering, what is the “text” type? This is an example of relying on the Rails database adapters to make the right call. For some DBs, large text fields are stored as varchar, while other’s like Postgres use a text type. The database adapter will figure out the best choice for us depending on the configured database — we don’t have to worry about it.

+

So add these into your self.up so it looks like this:

+
+  def self.up
+    create_table :articles do |t|
+      t.string :title
+      t.text :body
+
+      t.timestamps
+    end
+  end
+
+

What is that t.timestamps doing there? The generator inserted that line. It will create two columns inside our table titled created_at and updated_at. Rails will manage these columns for us, so when an article is created its created_at and updated_at are automatically set. Each time we make a change to the article, the updated_at will automatically be…uhhh…updated. Very handy.

+

Now our migration is done. You might wonder, what about the self.down? Didn’t I say migrations need to be symmetric? If we added something to the self.up it is generally the case that we need to undo that same change in the self.down. However, when the migration is creating a table, the self.down can just drop that table regardless of what columns are inside of it. That’s what the generator has setup for us here, where it just says drop_table :articles.

+

Save that migration file, switch over to your terminal, and run this command:

+
+rake db:migrate
+
+

This command starts the rake program which is a ruby utility for running maintenance-like functions on your application (working with the DB, executing unit tests, deploying to a server, etc). We tell rake to db:migrate which means “look in your set of functions for the database (db) and run the migrate function.” The migrate action finds all migrations in the /db/migrate/ folder, looks at a special table in the DB to determine which migrations have and have not been run yet, then runs any migration that hasn’t been run.

+

In this case we had just one migration to run and it should print some output like this to your terminal:

+
+==  CreateArticles: migrating =================================================
+-- create_table(:articles)
+   -> 0.0012s
+==  CreateArticles: migrated (0.0013s) ========================================
+
+

It tells you that it is running the migration named CreateArticles. And the “migrated” line means that it completed without errors. As I said before, rake keeps track of which migrations have and have not been run. Try running rake db:migrate again now, and see what happens.

+

We’ve now created the articles table in the database and can start working on our Article model.

+

Working with a Model in the Console

+

Another awesome feature of working with Rails is the console. The console is a command-line interface to your application. It allows you to access and work with just about any part of your application directly instead of going through the web interface. This can simplify your development process, and even once an app is in production the console makes it very easy to do bulk modifications, searches, and other data operations. So let’s open the console now by going to your terminal and entering this:

+
+rails console
+
+

You’ll then just get back a prompt of >>. You’re now inside an irb interpreter with full access to your application. Let’s try some experiments…enter each of these commands one at a time and observe the results:

+
+puts Time.now
+Article.all
+Article.new
+
+

The first line was just to demonstrate that we can do anything we previously did inside irb now inside of our console. The second like referenced the Article model and called the all method which returns an array of all articles in the database — so far an empty array. The third line created a new article object. You can see that this new object had attributes id, title, body, created_at, and updated_at.

+

All the information about the Article model is in the file /app/models/article.rb, so let’s open that now.

+

Not very impressive, right? There are no attributes defined inside the model, so how does Rails know that an Article should have a title, a body, etc? It queries the database, looks at the articles table, and assumes that whatever columns that table has should probably be the attributes accessible through the model.

+

You created most of those in your migration file, but what about id? Every table you create with a migration will automatically have an id column which serves as the table’s primary key. When you want to find a specific article, you’ll look it up in the articles table by its unique ID number. Rails and the database work together to make sure that these IDs are unique, usually using a special column type in the DB like “serial”.

+

In your console, try entering Article.all again. Do you see the blank article that we created with the Article.new command? No? The console doesn’t change values in the database (in most cases) until we explicitly call the .save method on an object. Let’s create a sample article and you’ll see how it works. Enter each of the following lines one at a time:

+
+a = Article.new
+a.title = "Sample Article Title"
+a.body = "This is the text for my article, woo hoo!"
+a.save
+Article.all
+
+

Now you’ll see that the Article.all command gave you back an array holding the one article we created and saved. Go ahead and create 3 more sample articles.

+

Moving Towards a Web Interface – Setting up the Router

+

We’ve created a few articles through the console, but we really don’t have a web application until we have a web interface. Let’s get that started. We said that Rails uses an “MVC” architecture and we’ve worked with the Model, now we need a Controller and View.

+

When a Rails server gets a request from a web browser it first goes to the router. The router decides what the request is trying to do, what resources it is trying to interact with. The router dissects a request based on the address it is requesting and other HTTP parameters (like the request type of GET or PUT). Let’s open the router’s configuration file, /config/routes.rb.

+

Inside this file you’ll see a LOT of comments that show you different options for routing requests. Let’s remove everything except the first line (ActionController ...) and the final end. Then, in between those two lines, add resources :articles so your file looks like this:

+
+ActionController::Routing::Routes.draw do |map|
+  resources :articles
+end
+
+

This line tells Rails to do a lot of work. It declares that we have a resource named articles and the router should expect requests to follow the RESTful model of web interaction (REpresentational State Transfer). The details don’t matter to you right now, but just know that when you make a request like http://localhost:3000/articles/ the router will know you’re looking for a listing of the articles or http://localhost:3000/articles/new means you’re trying to create a new article.

+

Now that the router knows how to handle requests about articles, it needs a place to actually send those requests, the Controller.

+

Creating the Articles Controller

+

We’re going to use another Rails generator but your terminal has the console currently running. You have two options:

+ +

I like to have several terminal windows available to me when developing, so I’d always choose the first option.

+

In your terminal, enter this command:

+
+rails generate controller articles
+
+

The output shows that the generator created several files/folders for you:

+ +

Let’s open up the controller file, /app/controllers/articles_controller.rb. You’ll see that this is basically a blank class, beginning with the class keyword and ending with the end keyword. Any code we add to the controller must go between these two lines, so I like to insert a bunch of blank lines between them so I have room work work and push that final end farther down the page.

+

Defining the Index Action

+

The first feature we want to add is an “index” page. This is what the app will send back when a user requests http://localhost:3000/articles/ — following the RESTful conventions, this should be a list of the articles. So when the router sees this request come in, it tries to call the index action inside articles_controller.

+

Let’s first try it out by entering http://localhost:3000/articles/ into your web browser. You should get an error message that looks like this:

+
+Unknown action
+No action responded to index. Actions:
+
+

The router tried to call the index action, but the articles controller doesn’t have a method with that name. It then lists available actions, but there aren’t any. This is because our controller is still blank. Let’s add the following method inside the controller:

+
+  def index
+    @articles = Article.all
+  end  
+
+

What is that “at” sign doing on the front of @articles? That marks this variable as an “instance level variable”. We want the list of articles to be accessible from both the controller and the view that we’re about to create. In order for it to be visible in both places it has to be an instance variable. If we had just named it articles, that local variable would only be available within the index method of the controller.

+

Now refresh your browser. The error message changed, but you’ve still got an error, right?

+
+Template is missing
+Missing template articles/index.erb in view path app/views
+
+

Creating the Index View

+

The error message is pretty helpful here. It tells us that the app is looking for a (view) template in /app/views/articles/ but it can’t find one named index.erb. Rails has assumed that our index action in the controller should have a corresponding index.erb view template in the views folder. We didn’t have to put any code in the controller to tell it what view we wanted, Rails just figures it out.

+

Let’s create that view template now. In the left pane of your RubyMine window, expand the app folder so you can see views, then expand views. Right-click on the articles folder, select New then File and, in the popup, name the file index.html.erb.

+

Why did we choose index.html.erb instead of the index.erb that the error message said it was looking for? Putting the HTML in the name makes it clear that this view is for generating HTML. In later versions of our blog we might create an RSS feed which would just mean creating an XML view template like index.xml.erb. Rails is smart enough to pick the right one based on the browser’s request, so when we just ask for http://localhost:3000/articles/ it will find the index.html.erb and render that file.

+

Now you’re looking at a blank file. Enter in this view template code which is a mix of HTML and what are called ERB tags:

+
+<h1>All Articles</h1>
+
+<ul>
+  <% @articles.each do |article| %>
+    <li>
+      <b><%= article.title %></b><br/>
+      <%= article.body %>
+    </li>  
+  <% end %>
+</ul>
+
+

ERB is a templating language that allows us to mix Ruby into our HTML. There are only a few things to know about ERB:

+ +

Save the file and refresh your web browser. You should see a listing of the articles you created in the console. We’ve got the start of a web application!

+

I1: Form-based Workflow

+

We’ve created articles from the console, but that isn’t a viable long-term solution. The users of our app will expect to add content through a web interface. In this iteration we’ll create an HTML form to submit the article, then all the backend processing to get it into the database.

+

Creating the NEW Action and View

+

Previously we setup the resources :articles route in routes.rb, and that told Rails that we were going to follow the RESTful conventions for this model named Article. Following this convention, the URL for creating a new article would be http://localhost:3000/articles/new. Enter that into your browser and see what comes up.

+
+Unknown action
+No action responded to new. Actions: index
+
+

This is an error message we’ve seen before. The router went looking for an action named new inside the articles_controller and didn’t find it. For our convenience the message lists the actions that are available — the only one being the index we created in I0.

+

So first let’s create that action. Open /app/controllers/articles_controller.rb and add this method structure, making sure it’s inside the ArticlesController class, but outside the existing index method:

+
+  def new
+        
+  end
+
+

With that defined, refresh your browser and you should get this:

+
+Template is missing
+Missing template articles/new.erb in view path app/views
+
+

Again, an error message we saw in I0. Create a new file /app/views/articles/new.html.erb with these contents:

+
+<h1>Create a New Article</h1>
+
+

Refresh your browser and you should just see the heading “Create a New Article”.

+

Writing a Form

+

It’s not very impressive so far — we need to add a form to the new.html.erb so the user can enter in the article title and body. Because we’re following the RESTful conventions, Rails can take care of many of the details. Inside that erb file, enter this code below your header:

+
+<%= form_for(@article) do |f| %>
+  <p>
+    <%= f.label :title %><br />
+    <%= f.text_field :title %>
+  </p>
+  <p>
+    <%= f.label :body %><br />
+    <%= f.text_area :body %>
+  </p>
+  <p>
+    <%= f.submit 'Create' %>
+  </p>
+<% end %>
+
+

What is all that? Let’s look at it piece by piece:

+ +

Refresh your browser and you’ll see this:

+
+RuntimeError in Articles#new
+Showing app/views/articles/new.html.erb where line #3 raised:
+Called id for nil, which would mistakenly be 4 -- if you really wanted the id of nil, use object_id
+
+Extracted source (around line #3):
+1: <h1>Create a New Article</h1>
+2: 
+3: <%= form_for(@article) do |f| %>
+4:   <%= f.error_messages %>
+5: 
+
+

What’s it trying to tell us. In our new.html.erb on line #3 there was an error about “Called id for nil”. As we learned in the Ruby in 100 minutes tutorial, nil is Ruby’s way of referring to nothingness. Somewhere in line #3 we’re working with an object that doesn’t exist.

+

And since there’s only one object in line #3, it makes it pretty obvious — the problem is that we started talking about a thing named @article without ever creating that thing. Rails uses some of the reflection techniques that we talked about earlier in order to setup the form. Remember in the console when we called Article.new to see what fields an Article has? Rails wants to do the same thing, but we need to create the blank object for it.

+

Go into your articles_controller.rb, and inside the new method, add this line:

+
+@article = Article.new
+
+

Then refresh your browser and your form should come up. Enter in a title, some body text, and click CREATE.

+

The CREATE Action

+

You’re old friend pops up again…

+
+Unknown action
+No action responded to create. Actions: index and new
+
+

When we loaded the form we accessed the new action, but when that form is submitted to the application, following the REST convention, it goes to a create action. We need to create that action. Inside your articles_controller.rb add this method (again, inside the ArticlesContoller class, but outside the other methods):

+
+  def create
+    @article = Article.new(params[:article])
+    @article.save!
+    redirect_to articles_path
+  end
+
+

This method says…

+ +

Go back in your browser so you get to the form with the sample data you entered and click CREATE. You should then bounce to the full articles list with your new article added.

+

Adding Navigation to the Index

+

Right now our article list is very plain and we end up typing in a bunch of URLs by hand. Let’s add some links. Open your /app/views/articles/index.html.erb and…

+ +

Refresh your browser and you should now see a list of just the article titles that are linked somewhere and a link at the bottom to “Create a New Article”. Test that this create link takes you to the new article form. Then go back to the article list and click one of the article titles.

+

Creating the SHOW Action

+

Tired of this error message yet? Go to your articles_controller.rb and add a method like this:

+
+def show
+
+end
+
+

Refresh the browser and you’ll get the “Template is Missing” error. Let’s pause here before creating the view template.

+

Look at the URL: http://localhost:3000/articles/1. When we added the link_to in the index and pointed it to the article_path for this article, the router created this URL. Following the RESTful convention, this URL goes to a SHOW method which would display the Article with ID number 1. Your URL might have a different number depending on which article title you clicked in the index.

+

So what do we want to do when the user clicks an article title? Find the article, then display a page with its title and body. We’ll use the number on the end of the URL to find the article in the database. The router will send us this number in the variable params[:id]. Inside the show method that we just created, add this line:

+
+@article = Article.find(params[:id])
+
+

Now create the file /app/views/articles/show.html.erb and add this code:

+
+<h2><%= @article.title %></h2>
+<p><%= @article.body %></p>
+<%= link_to "<< Back to Articles List", articles_path %>
+
+

Refresh your browser and your article should show up along with a link back to the index.

+

But You Never Make Mistakes!

+

We can create articles and we can display them, but when we eventually deliver this to less perfect people than us, they’re going to make mistakes. Right now there’s no way to edit an article once it’s been created. There’s also no way to remove an article. Let’s add those functions.

+

Look at your index.html.erb and change the whole <li> segment so it looks like this:

+
+    <li>
+      <b><%= link_to article.title, article_path(article) %></b><br/>
+      <i>Actions:
+      <%= link_to "edit", edit_article_path(article) %>,
+      <%= link_to "remove", article, :method => :delete,
+                   :confirm => "Remove the article '#{article.title}'?" %>      
+      </i>
+    </li>
+
+

The first link we added, for edit, is pretty similar to what we’ve done before — creating a link with the text “edit” pointing to the address edit_article_path, which is defined by the router, and editing the thing named article.

+

The second one is a little more complex. Web browsers don’t yet properly implement all the REST conventions, so Rails creates a hack for destroying objects. The details aren’t too important. So this link will have the text “remove”, will point to the article, and will use the HTTP method “delete”. We’ve also added a :confirm parameter. If a link has a :confirm, then Rails will generate some Javascript which will popup a box when the link is clicked that contains the text in the :confirm. Here we’re setting the message to check that the user wants to remove the article and including the article’s title in the message.

+

Refresh your browser and you should see “edit” and “remove” links for each article. Click the EDIT link for your first article.

+

Creating an Edit Action & View

+

The router is expecting to find an action in articles_controller.rb named edit, so let’s add this:

+
+  def edit
+    @article = Article.find(params[:id])
+  end
+
+

All the edit action is really going to do is find the article to be edited, then display the editing form. If you refresh after adding that edit action you’ll see the template missing error. Create a file /app/views/articles/edit.html.erb but hold on before you type anything. Below is what the edit form should look like:

+
+<h1>Edit an Article</h1>
+
+<%= form_for(@article) do|f| %>
+  <%= f.error_messages %>
+
+  <p>
+    <%= f.label :title %><br />
+    <%= f.text_field :title %>
+  </p>
+  <p>
+    <%= f.label :body %><br />
+    <%= f.text_area :body %>
+  </p>
+  <p>
+    <%= f.submit 'Update' %>
+  </p>
+<% end %>
+
+

In the Ruby and Rails communities there is a mantra of “Don’t Repeat Yourself” — but that’s exactly what I’ve done here. This view is basically the same as the new.html.erb — the only changes are the H1 and the name of the button. We can abstract this form into a single file called a partial, then reference this partial from both new.html.erb and edit.html.erb.

+

Create a file /app/views/articles/_form.html.erb and, yes, it has to have the underscore at the beginning of the filename. Go into your /app/views/articles/new.html.erb and CUT all the text from and including the form_for line all the way to its end. The only thing left will be your H1 line. Then add the following code at the bottom of that view:

+
+<%= render :partial => 'form' %>
+
+

Now go back to the _form.html.erb and paste the form code. Change the text on the submit button to say “Save” so it makes sense both when creating a new article and editing and existing one.

+

Then look at your edit.html.erb file, write an H1 header saying “Edit an Article”, then use the same code to render the partial named form.

+

Go back to your articles list and try creating a new article — it should work just fine. Try editing an article and you should see the form with the existing article’s data — it works OK until you click SAVE.

+

The router is looking for an action named update. Just like the new action sends its form data to the create action, the edit action sends its form data to the update action. In fact, within our articles_controller.rb, the update method will look very similar to create:

+
+  def update
+    @article = Article.find(params[:id])
+    @article.update_attributes(params[:article])
+    @article.save!
+    redirect_to article_path(@article)    
+  end
+
+

The only new bit here is the update_attributes method. This method works very similar to when we called the Article.new method and passed in the hash of form data. When we call update_attributes on the @article object and pass in the data from the form, it changes the values in the object to match the values submitted with the form. Then we save the object to the database and redirect to the articles list.

+

Now try editing and saving some of your articles.

+

Creating a Destroy Action

+

Next, click the REMOVE link for one article and hit OK. You can see that the router is expecting there to be a destroy action. Go into articles_controller.rb and add a destroy method like this:

+
+  def destroy
+    @article = Article.find(params[:id])
+    @article.destroy
+    redirect_to articles_path
+  end
+
+

Here we’re doing a find based on params[:id] like we did in the show action. We call that object’s destroy method, then redirect back to the articles list.

+

Try it out in your browser.

+

Adding a Flash

+

It would be nice, though, if we gave the user some kind of status message about the operation that took place. When we create an article the message might say “Article ‘the-article-title’ was created”, or “Article ‘the-article-title’ was removed” for the remove action. We can accomplish this with a special object called the flash.

+

Rails creates the object named flash, so we don’t need to do anything to set it up. We can start by integrating it into our index.html.erb by adding this line at the very top:

+
+<div class="flash"><p><%= flash[:message] %></p></div>
+
+

This just outputs the value stored in the flash object with the key :message. If you refresh your articles list you won’t see anything because we haven’t stored a message in there yet. Look at articles_controller.rb and add this line right after the save! line in your create method:

+
+flash[:message] = "Article '#{@article.title}' was created."
+
+

Then go to your articles list, create another sample article, and when you click create you should see the flash message at the top of your view.

+

Here’s something cool about how Rails handles the flash — hit your browser’s REFRESH button while looking at the articles list. See how the flash disappears? Once you display the message in a flash Rails clears it out. That’s why it’s perfect for status messages like this.

+

Similarly, add a flash message into your destroy method and confirm that it shows up when an article is removed. Then add one to your update method that’ll display when an article is edited.

+

And, finally, you’re done with I1!

+

I2: Adding Comments

+

Most blogs allow the reader to interact with the content by posting comments. Let’s add some simple comment functionality.

+

Designing the Comment Model

+

First, we need to brainstorm what a comment is…what kinds of data does it have…

+ +

With that understanding, let’s create a Comment model. Switch over to your terminal and enter this line:

+
+rails generate model Comment
+
+

We’ve already gone through what files this generator creates, we’ll be most interested in the migration file and the comment.rb.

+

Setting up the Migration

+

Open the migration file that the generator created, /db/migrate/some-timestamp_create_comments.rb. Inside the self.up you need to add one line for each of the pieces of data we just brainstormed. It’ll start off with these…

+
+t.integer :article_id
+t.string :author_name
+
+

Then keep adding lines that create strings named :author_email, :author_url, and a text field named :body.

+

Once that’s complete, go to your terminal and run the migration with rake db:migrate.

+

Relationships

+

The power of SQL databases is the ability to express relationships between elements of data. We can join together the information about an order with the information about a customer. Or in our case here, join together an article (in the articles table) with its comments (in the comments table). We do this by using foreign keys.

+

Foreign keys are a way of marking one-to-one and one-to-many relationships. An article might have zero, five, or one hundred comments. But a comment only belongs to one article. These objects have a one-to-many relationship — one article connects to many comments.

+

Part of the big deal with Rails is that it makes working with these relationships very easy. When we created the migration for comments we started with an integer field named article_id. The Rails convention is that, for a one-to-many relationship, the objects on the “many” end should have a foreign key referencing the “one” object. And that foreign key should be titled with the name of the “one” object, then an underscore, then “id”. So in this case one article has many comments, so each comment has a field named article_id which tracks which article they belong to. Similarly, a store’s customer might have many orders, so each order would have a customer_id specifying which customer they belong to.

+

Following this convention will get us a lot of functionality “for free.” Open your /app/models/comment.rb and add the middle line so it looks like this:

+
+class Comment < ActiveRecord::Base
+  belongs_to :article
+end
+
+

A comment relates to a single article, it “belongs to” an article. We then want to declare the other side of the relationship inside /app/models/article.rb like this:

+
+class Article < ActiveRecord::Base
+  has_many :comments
+end
+
+

How an article “has many” comments, and a comment “belongs to” an article. We have explained to Rails that these objects have a one-to-many relationship.

+

Testing in the Console

+

Let’s use the console to test how this relationship works in code. If you don’t have a console open, go to your terminal and enter rails console from your project directory. If you have a console open already, enter the command reload! to refresh any code changes.

+

Run the following commands one at a time and observe the output:

+
+a = Article.first
+a.comments
+Comment.new
+a.comments.new
+
+

When you called the comments method on object a, it gave you back a blank array because that article doesn’t have any comments. When you executed Comment.new it gave you back a blank Comment object with those fields we defined in the migration. But, if you look closely, when you did a.comments.new the comment object you got back wasn’t quite blank — it has the article_id field already filled in with the ID number of article a.

+

Try creating a few comments for that article like this:

+
+c = a.comments.new
+c.author_name = "Daffy Duck"
+c.author_url = "http://daffyduck.com"
+c.body = "I think this article is thhh-thhh-thupid!"
+c.save
+d = a.comments.create(:author_name => "Chewbacca", :body => "RAWR!")
+
+

For the first comment, c, I used a series of commands like we’ve done before. For the second comment, d, I used the create method. When you use new it doesn’t go to the database until you call save. With create you usually pass in the attributes then the object is created, those attributes set, and the object saved to the database all in one step.

+

Now that you’ve created a few comments, try executing a.comments again. Did your comments all show up? When I did it, only one comment came back. The console tries to minimize the number of times it talks to the database, so sometimes if you ask it to do something it’s already done, it’ll get the information from the cache instead of really asking the database — giving you the same answer it gave the first time. That can be annoying. To force it to clear the cache and lookup the accurate information, try this:

+
+reload!
+a = Article.first
+a.comments
+
+

So you’ll see that the article has comments — great. Now we need to integrate them into the article display.

+

Displaying Comments for an Article

+

We want to display any comments underneath their parent article. Because we’ve setup the relationships between those models, this is very easy. Open /app/views/articles/show.html.erb and add the following lines right before the link to the articles list:

+
+<h3>Comments</h3>
+<%= render :partial => 'comment', :collection => @article.comments %>
+
+

This says that we want to render a partial named “comment” and that we want to do it once for each element in the collection @article.comments. We saw in the console that when we call the .comments method on an article we’ll get back an array of its associated comment objects. So this render line will pass each element of that array one at a time into the partial named “comment”. Now we need to create the file /app/views/articles/_comment.html.erb and add this code:

+
+<div class="comment">
+  <h4>Comment by <%=h comment.author_name %></h4>
+  <p><%=h comment.body %></p>
+</div>
+
+

With that in place, try clicking on your articles and find the one where you created the comments. Did they show up? What happens when an article doesn’t have any comments?

+

Web-Based Comment Creation

+

Good start, but our users (hopefully) can’t get into the console to create their comments. We’ll need to create a web interface. We’ll go through some of the same steps that we did when creating the web interface for creating articles.

+

Let’s start with the form. The comment form should be embedded into the article’s show template. So let’s add this code right above the “Back to Articles List” in the articles show.html.erb:

+
+<%= render :partial => 'comment_form' %>
+
+

Obviously this is expecting a file /app/views/articles/_comment_form.html.erb, so create that and add this content for now:

+
+<h3>Post a Comment</h3>
+<p>(Comment form will go here)</p>
+
+

Look at an article in your browser to make sure that partial is showing up. Then we can start figuring out the details of the form.

+

Ok, now look at your articles_controller.rb in the new method. Remember how we had to create a blank Article object so Rails could figure out which fields an article has? We need to do the same thing before we create a form for the comment. But when we view the article and display the comment form we’re not running the article’s new method, we’re running the show method. So we’ll need to create a blank Comment object inside that show method like this:

+
+@comment = @article.comments.new
+
+

This is just like we did it in the console. Now we can create a form inside our _comment_form.html.erb partial like this:

+
+<h3>Post a Comment</h3>
+
+<%= form_for @comment do |f| %>
+  <%= f.hidden_field :article_id, :value => @article.id %>
+  <p>
+    <%= f.label :author_name %><br/>
+    <%= f.text_field :author_name %>
+  </p>
+  <p>
+    <%= f.label :author_email %><br/>
+    <%= f.text_field :author_email %>
+  </p>
+  <p>
+    <%= f.label :author_url %><br/>
+    <%= f.text_field :author_url %>
+  </p>
+  <p>
+    <%= f.label :body %><br/>
+    <%= f.text_area :body %>
+  </p>
+  <p>
+    <%= f.submit 'Submit' %>
+  </p>
+<% end %>
+
+

The only new thing here is the hidden field helper. This hidden field will hold the ID of the article to help when creating the comment object.

+

Save then refresh in your web browser and…well…you’ll get an error like this:

+
+NoMethodError in Articles#show
+Showing app/views/articles/_comment_form.html.erb where line #3 raised:
+undefined method `comments_path' for #<ActionView::Base:0x10446e510>
+
+

The form_for helper is trying to build the form so that it submits to comments_path, but we haven’t told the router anything about Comments yet. Open /config/routes.rb and add this line at the top:

+
+resources :comments
+
+

Then refresh your browser and your form should show up. Try filling out the comments form and click SUBMIT — you’ll get an error about uninitialized constant CommentsController.

+

Creating a Comments Controller

+

Just like we needed an articles_controller.rb to manipulate our Articles, we’ll need a comments_controller.rb. Switch over to your terminal to generate it with this line:

+
+rails generate controller comments index create destroy
+
+

What’s up with those extra parameters? Anything after the name of the controller (in this case “comments”) will cause Rails to create stubs for methods with those names. Now open your /app/controllers/comments_controller.rb and you’ll see it has the three method stubs already.

+

The one we’re interested in first is create. You can cheat by looking at the create method in your articles_controller.rb. For your comments_controller.rb, everything should be the same just replace article with comment. Then the redirect is a little different, use this:

+
+redirect_to article_path(@comment.article)
+
+

Test out your form to create another comment now — and it should work!

+

Cleaning Up

+

We’ve got some decent comment functionality, but there are a few things we should add and tweak.

+

Comments Count

+

Let’s make it so where the view template has the “Comments” header it displays how many comments there are, like “Comments (3)”. Open up your article’s show.html.erb and change the comments header so it looks like this:

+
+<h3>Comments (<%= @article.comments.count %>)</h3>
+
+

Form Labels

+

The comments form looks a little silly with “Author Name” and “Author URL” and such. It should probably say “Your Name” and “Your URL (optional)”, right? To change the text that the label helper prints out, you just pass in the desired text as a second parameter, like this:

+
+<%= f.label :author_name, "Your Name"  %>
+
+

Change your _comment_form.html.erb so it prints out “Your Name”, “Your Email Address”, “Your URL (optional)”, and “Your Comment”.

+

Add Timestamp to the Comment Display

+

We should add something about when the comment was posted. Rails has a really neat helper named distance_of_time_in_words which takes two dates and creates a text description of their difference like “32 minutes later”, “3 months later”, and so on. You can use it in your _comment.html.erb partial like this:

+
+<p>Posted <%= distance_of_time_in_words(comment.article.created_at, comment.created_at) %> later</p>
+
+

With that, you’re done with I2!

+

I3: Tagging

+

In this iteration we’ll add the ability to tag articles for organization and navigation.

+

First we need to think about what a tag is and how it’ll relate to the Article model. If you’re not familiar with tags, they’re commonly used in blogs to assign the article to one or more categories. For instance, if I write an article about a feature in Ruby on Rails, I might want it tagged with all of these categories: ruby, rails, programming, features. That way if one of my readers is looking for more articles about one of those topics they can click on the tag and see a list of my articles with that tag.

+

Understanding the Relationship

+

What is a tag? We need to figure that out before we can create the model. First, a tag must have a relationship to an article so they can be connected. A single tag, like “ruby” for instance, should be able to relate to many articles. On the other side of the relationship, the article might have multiple tags (like “ruby”, “rails”, and “programming” as above) – so it’s also a many relationship. Articles and tags have a many-to-many relationship.

+

Many-to-many relationships are tricky because we’re using an SQL database. If an Article “has many” tags, then we would put the foreign key article_id inside the tags table – so then a Tag would “belong to” an Article. But a tag can connect to many articles, not just one. We can’t model this relationship with just the articles and tags tables.

+

When we start thinking about the database modeling, there are a few ways to achieve this setup. One way is to create a “join table” that just tracks which tags are connected to which articles. Traditionally this table would be named articles_tags and Rails would express the relationships by saying that the Article model has_and_belongs_to_many Tags, while the Tag model has_and_belongs_to_many Articles.

+

Most of the time this isn’t the best way to really model the relationship. The connection between the two models usually has value of its own, so we should promote it to a real model. For our purposes, we’ll introduce a model named “Tagging” which is the connection between Articles and Tags. The relationships will setup like this:

+ +

Making Models

+

With those relationships in mind, let’s design the new models:

+ +

Note that there are no changes necessary to Article because the foreign key is stored in the Tagging model. So now lets generate these models in your terminal:

+
+rails generate model Tag name:string
+rails generate model Tagging tag_id:integer article_id:integer
+rake db:migrate
+
+

Expressing Relationships

+

Now that our model files are generated we need to tell Rails about the relationships between them. For each of the files below, add these lines:

+

In /app/models/article.rb:

+
+  has_many :taggings
+
+

In /app/models/tag.rb:

+
+  has_many :taggings
+
+

Then in /app/models/tagging.rb:

+
+  belongs_to :article
+  belongs_to :tag
+
+

After Rails had been around for awhile, developers were finding this kind of relationship very common. In practical usage, if I had an object named article and I wanted to find its Tags, I’d have to run code like this:

+
+tags = article.taggings.collect{|tagging| tagging.tag}
+
+

That’s a pain for something that we need commonly. The solution was to augment the relationship with “through”. We’ll add a second relationship now to the Article and Tag classes:

+

In /app/models/article.rb:

+
+  has_many :taggings
+  has_many :tags, :through => :taggings
+
+

In /app/models/tag.rb:

+
+  has_many :taggings
+  has_many :articles, :through => :taggings
+
+

Now if we have an object like article we can just ask for article.tags or, conversely, if we have an object named tag we can ask for tag.articles.

+

An Interface for Tagging Articles

+

The first interface we’re interested in is within the article itself. When I write an article, I want to have a text box where I can enter a list of zero or more tags separated by commas. When I save the article, my app should associate my article with the tags with those names, creating them if necessary.

+

Adding the text field will take place in the file /app/views/articles/_form.html.erb. Add in a set of paragraph tags underneath the body fields like this:

+
+  <p>
+    <%= f.label :tag_list %><br />
+    <%= f.text_field :tag_list %>
+  </p>
+
+

With that added, try to create an new article in your browser and your should see this error:

+
+NoMethodError in Articles#new
+Showing app/views/articles/_form.html.erb where line #14 raised:
+undefined method `tag_list' for #<Article:0x10499bab0>
+
+

An Article doesn’t have a thing named tag_list — we made it up. In order for the form to display, we need to add a method to the article.rb file like this:

+
+  def tag_list
+    return self.tags.join(", ")
+  end
+
+

Your form should now show up and there’s a text box at the bottom named “Tag list”. Enter content for another sample article and in the tag list enter ruby, technology. Click SAVE and you’ll get an error like this:

+
+ActiveRecord::UnknownAttributeError in ArticlesController#create
+unknown attribute: tag_list
+
+

What is this all about? Let’s start by looking at the form data that was posted when we clicked SAVE. This data is in the production.log file which should be in the “Console” frame at the bottom of the RubyMine window. Look for the line that starts “Processing ArticlesController#create”, here’s what mine looks like:

+
+Processing ArticlesController#create (for 127.0.0.1) [POST]
+  Parameters: {"article"=>{"body"=>"Yes, the samples continue!", "title"=>"My Sample", "tag_list"=>"ruby, technology"}, "commit"=>"Save", "authenticity_token"=>"xxi0A3tZtoCUDeoTASi6Xx39wpnHt1QW/6Z1jxCMOm8="}
+
+

The field that’s interesting there is the "tag_list"=>"technology, ruby". Those are the tags as I typed them into the form. The error came up in the create method, so let’s peek at /app/controllers/articles_controller.rb in the create method. See the first line that calls Article.new(params[:article])? This is the line that’s causing the error as you could see in the middle of the stack trace.

+

Since the create method passes all the parameters from the form into the Article.new method, the tags are sent in as the string "technology, ruby". The new method will try to set the new Article’s tag_list equal to "technology, ruby" but that method doesn’t exist because there is no attribute named tag_list.

+

There are several ways to solve this problem, but the simplest is to pretend like we have an attribute named tag_list. We can define the tag_list= method inside article.rb like this:

+
+  def tag_list=(tags_string)
+    
+  end
+
+

Just leave it blank for now and try to resubmit your sample article with tags. It goes through!

+

Not So Fast

+

Did it really work? It’s hard to tell. Let’s jump into the console and have a look.

+
+a = Article.last
+a.tags
+
+

I bet the console reported that a had [] tags — an empty list. So we didn’t generate an error, but we didn’t create any tags either.

+

We need to return to that tag_list= method in article.rb and do some more work. We’re taking in a parameter, a string like "tag1, tag2, tag3" and we need to associate the article with tags that have those names. The pseudo-code would look like this:

+ +

The first step is something that Ruby does very easily using the .split method. Go into your console and try "tag1, tag2, tag3".split. By default it split on the space character, but that’s not what we want. You can force split to work on any character by passing it in as a parameter, like this: "tag1, tag2, tag3".split(",").

+

Look closely at the output and you’ll see that the second element is " tag2" instead of "tag2" — it has a leading space. We don’t want our tag system to end up with different tags because of some extra (non-meaningful) spaces, so we need to get rid of that. Ruby’s String class has a strip method that pulls off leading or trailing whitespace — try it with " my sample ".strip. You’ll see that the space in the center is preserved.

+

So to combine that with our strip, try this code:

+
+"tag1, tag2, tag3".split(",").collect{|s| s.strip.downcase}
+
+

The .split(",") will create the list with extra spaces as before, then the .collect will take each element of that list and send it into the following block where the string is named s and the strip and downcase methods are called on it. The downcase method is to make sure that “ruby” and “Ruby” don’t end up as different tags. This line should give you back ["tag1", "tag2", "tag3"].

+

Now, back inside our tag_list= method, let’s add this line:

+
+tag_names = tags_string.split(",").collect{|s| s.strip}
+
+

So looking at our pseudo-code, the next step is to go through each of those tag_names and find or create a tag with that name. Rails has a built in method to do just that, like this:

+
+tag = Tag.find_or_create_by_name(tag_name)
+
+

Once we find or create the tag, we need to create a tagging which connects this article (here self) to the tag like this:

+
+self.taggings.build(:tag => tag)
+
+

The build method is a special creation method. It doesn’t need an explicit save, Rails will wait to save the Tagging until the Article itself it saved. So, putting these pieces together, your tag_list= method should look like this:

+
+  def tag_list=(tags_string)
+    tag_names = tags_string.split(",").collect{|s| s.strip.downcase}
+    tag_names.each do |tag_name|
+      tag = Tag.find_or_create_by_name(tag_name)
+      self.taggings.build(:tag => tag)
+    end
+  end
+
+

Testing in the Console

+

Go back to your console and try these commands:

+
+reload!
+a = Article.new(:title => "A Sample Article for Tagging!",:body => "Great article goes here", :tag_list => "ruby, technology")
+a.save
+a.tags
+
+

You should get back a list of the two tags. If you’d like to check the other side of the Article-Tagging-Tag relationship, try this:

+
+t = a.tags.first
+t.articles
+
+

And you’ll see that this Tag is associated with just one Article.

+

Adding Tags to our Display

+

According to our work in the console, articles can now have tags, but we haven’t done anything to display them in the article pages. Let’s start with /app/views/articles/show.html.erb. Right below the line that displays the article.title, add this line:

+
+Tags: <%= tag_links(@article.tags) %><br />
+
+

This line calls a helper named tag_links and sends the article.tags array as a parameter. We need to then create the tag_links helper. Open up /app/helpers/articles_helper.rb and add this method inside the module/end keywords:

+
+def tag_links(tags)
+
+end
+
+

The desired outcome is a list of comma separated tags, where each one links to that tag’s show action — the page where we’ll list all the articles with that tag.

+

A helper method has to return a string which will get rendered into the HTML. In this case we’ll use the collect method to create a list of links, one for each Tag, where the link is created by the link_to helper. Then we’ll return back the links connected by a comma and a space:

+
+  def tag_links(tags)
+    links = tags.collect{|tag| link_to tag.name, tag_path(tag)}
+    return links.join(", ")
+  end  
+
+

Refresh your view and…BOOM:

+
+NoMethodError in Articles#show
+Showing app/views/articles/index.html.erb where line #6 raised:
+undefined method `tag_path' for #<ActionView::Base:0x104aaa460>
+
+

The link_to helper is trying to use tag_path from the router, but the router doesn’t know anything about our Tag object. We created a model, but we never created a controller or route. There’s nothing to link to — so let’s generate that controller from your terminal:

+
+rails generate controller tags index show
+
+

Then we need to add it to our /config/routes.rb like this:

+
+resources :tags
+
+

Now refresh your view and you should see your linked tags showing up on the individual article pages.

+

Lastly, use similar code in /app/views/articles/index.html.erb to display the tags on the article listing page.

+

Avoiding Repeated Tags

+

Try editing one of your article that already has some tags. Save it and look at your article list. You’ll probably see that tags are getting repeated, which is obviously not what we want.

+

When we wrote our tag_list= method inside of article.rb, we were just thinking about it running when creating a new article. Thus we always built a new tagging for each tag in the list. But when we’re editing, we might get the string “ruby, technology” into the method while the Article was already linked to the tags “ruby” and “technology” when it was created. As it is currently written, the method will just “retag” it with those tags, so we’ll end up with a list like “ruby, technology, ruby, technology”.

+

There are a few ways we could fix this — the first thing I want to do is remove any repeated tags in the parameter list by using the Ruby method uniq:

+
+tag_names = tags_string.split(",").collect{|s| s.strip.downcase}.uniq
+
+

This is a good start but it doesn’t solve everything. We’d still get repeated tags each time we edit an article.

+

If we edit an article and remove a tag from the list, this method as it stands now isn’t going to do anything about it. Since we don’t have anything valuable in the Tagging object besides the connection to the article and tag, they’re disposible. We can just destroy all the taggings at the beginning of the method. Any tags that aren’t in the tags_string won’t get re-linked. This will both avoid removed tags and prevent the “double tagging” behavior. Putting that all together, here’s my final tag_list= method:

+
+  def tag_list=(tags_string)
+    self.taggings.destroy_all
+    tag_names = tags_string.split(",").collect{|s| s.strip.downcase}.uniq
+    tag_names.each do |tag_name|
+      tag = Tag.find_or_create_by_name(tag_name)
+      self.taggings.build(:tag => tag)
+    end
+  end
+
+

It prevents duplicates and allows you to remove tags from the edit form. Test it out and make sure things are working!

+

Listing Articles by Tag

+

The links for our tags are showing up, but if you click on them you’ll get our old friend, the “No action responded to show. Actions:” error. Open up your /app/controllers/tags_controller.rb and add a a show method like this:

+
+  def show
+    @tag = Tag.find(params[:id])
+  end  
+
+

Then create a file /app/views/tags/show.html.erb like this:

+
+<h1>Articles Tagged with <%= @tag.name %></h1>
+
+<ul>
+  <% @tag.articles.each do |article| %>
+    <li><%= link_to article.title, article_path(article) %></li>
+  <% end %>
+</ul>
+
+

Refresh your view and you should see a list of articles with that tag. Keep in mind that there might be some abnormalities from articles we tagged before doing our fixes to the tag_list= method. For any article with issues, try going to its edit screen, saving it, and things should be fixed up. If you wanted to clear out all taggings you could do Tagging.destroy_all from your console.

+

Listing All Tags

+

We’ve built the show action, but the reader should also be able to browse the tags available at http://localhost:3000/tags/. I think you can do this on your own. Create an index action in your tags_controller.rb and an index.html.erb in the corresponding views folder. Look at your articles_controller.rb and Article index.html.erb if you need some clues.

+

If that’s easy, try creating a destroy method in your tags_controller.rb and adding a destroy link to the tag list. If you do this, change the association in your tag.rb so that it says has_many :taggings, :dependent => :destroy. That’ll prevent orphaned Tagging objects from hanging around.

+

With that, a long Iteration 3 is complete!

+

I4: Installing Plugins

+

In this iteration we’ll learn how to take advantage of the many plugins and libraries available to quickly add features to your application. First we’ll work with paperclip, a library that manages file attachments and uploading.

+

Using the Gemfile to Setup a RubyGem

+

In the past Rails plugins were distributed a zip or tar files that got stored into your application’s file structure. One advantage of this method is that the plugin could be easily checked into your source control system along with everything you wrote in the app. The disadvantage is that it made upgrading to newer versions of the plugin, and dealing with the versions at all, complicated.

+

Most Rails plugins are now moving toward RubyGems. RubyGems is a package management system for Ruby, similar to how Linux distributions use Apt or RPM. There are central servers that host libraries, and we can install those libraries on our machine with a single command. RubyGems takes care of any dependencies, allows us to pick an options if necessary, and installs the library.

+

Let’s see it in action. If you have your server running in RubyMine, click the red square button to STOP it. If you have a console session open, type exit to exit. Then open up /Gemfile and look for the lines like this:

+
+  # gem 'bj'
+  # gem 'nokogiri'
+  # gem 'sqlite3-ruby', :require => 'sqlite3'
+
+

These lines are commented out because they start with the # character. By specifying a RubyGem with the gem command, we’ll tell the Rails application “Make sure this gem is loaded when you start up. If it isn’t available, freak out!” Here’s how we’ll require the paperclip gem, add this near those commented lines:

+
+  gem "paperclip"
+
+

When you’re writing a production application, you might specify additional parameters that require a specific version or a custom source for the library. With that config line declared, click the green arrow in RubyMine to startup your server. You should get an error like this:

+
+  Could not find gem 'paperclip (>= 0, runtime)' in any of the gem sources listed in your Gemfile.
+  Try running `bundle install`.
+
+

The last line is key — since our config file is specifying which gems it needs, the bundle command can help us install those gems. Go to your terminal and:

+
+bundle
+
+

It should then install the paperclip RubyGem with a version like 2.3.8. In some projects I work on, the config file specifies upwards of 18 gems. With that one bundle command the app will check that all required gems are installed with the right version, and if not, install them.

+

Now we can start using the library in our application!

+

Setting up the Database for Paperclip

+

We want to add images to our articles. To keep it simple, we’ll say that a single article could have zero or one images. In later versions of the app maybe we’d add the ability to upload multiple images and appear at different places in the article, but for now the one will show us how to work with paperclip.

+

First we need to add some fields to the Article model that will hold the information about the uploaded image. Any time we want to make a change to the database we’ll need a migration. Go to your terminal and execute this:

+
+rails generate migration add_paperclip_fields_to_article
+
+

That will create a file in your /db/migrate/ folder that ends in _add_paperclip_fields_to_article.rb. Open that file now.

+

Remember that the code inside the self.up method is to migrate the database forward, while the self.down should undo those changes. We’ll use the add_column and remove_column methods to setup the fields paperclip is expecting:

+
+class AddPaperclipFieldsToArticle < ActiveRecord::Migration
+    def self.up
+      add_column :articles, :image_file_name,    :string
+      add_column :articles, :image_content_type, :string
+      add_column :articles, :image_file_size,    :integer
+      add_column :articles, :image_updated_at,   :datetime
+    end
+
+    def self.down
+      remove_column :articles, :image_file_name
+      remove_column :articles, :image_content_type
+      remove_column :articles, :image_file_size
+      remove_column :articles, :image_updated_at
+    end
+end
+
+

The go to your terminal and run rake db:migrate. The rake command should show you that the migration ran and added columns to the database.

+

Adding to the Model

+

The gem is loaded, the database is ready, but we need to tell our Rails application about the image attachment we want to add. Open /app/models/article.rb and just below the existing has_many lines, add this line:

+
+has_attached_file :image
+
+

This has_attached_file method is part of the paperclip library. With that declaration, paperclip will understand that this model should accept a file attachment and that there are fields to store information about that file which start with image_ in this model’s database table.

+

Modifying the Form Template

+

First we’ll add the ability to upload the file when editing the article, then we’ll add the image display to the article show template. Open your /app/views/articles/_form.html.erb view template. We need to make two changes…

+

In the very first line, we need to specify that this form needs to accept “multipart” data. This is an instruction to the browser about how to submit the form. Change your top line so it looks like this:

+
+<% form_for(@article, :html => {:multipart => true}) do |f| %>
+
+

Then further down the form, right before the paragraph with the save button, let’s add a label and field for the file uploading:

+
+  <p>
+    <%= f.label :image, "Attach an Image" %><br />
+    <%= f.file_field :image %>
+  </p>
+
+

Trying it Out

+

If your server isn’t running, start it up with the green play button in RubyMine. Then go to http://localhost:3000/articles/ and click EDIT for your first article. The file field should show up towards the bottom. Click the Choose a File and select one of the small images that I’ve distributed to you. Click SAVE and you’ll return to the article index. Click the title of the article you just modified. What do you see? Did the image attach to the article?

+

When I first did this, I wasn’t sure it worked. Here’s how I checked:

+
    +
  1. Open a console session (rails console from terminal)
  2. +
  3. Find the ID number of the article by looking at the URL. In my case, the url was http://localhost:3000/articles/1 so the ID number is just 1
  4. +
  5. In console, enter a = Article.find(1)
  6. +
  7. Right away I see that the article has data in the image_file_name and other fields, so I think it worked.
  8. +
  9. Enter a.image to see even more data about the file
  10. +
+

Ok, it’s in there, but we need it to actually show up in the article. Open the /app/views/articles/show.html.erb view template. In between the line that displays the title and the one that displays the body, let’s add this line:

+
+<%= image_tag @article.image.url %>
+
+

Then refresh the article in your browser. Tada!

+

Improving the Form

+

When first working with the edit form I wasn’t sure the upload was working because I expected the file_field to display the name of the file that I had already uploaded. Go back to the edit screen in your browser for the article you’ve been working with. See how it just says “Choose File, no file selected” — nothing tells the user that a file already exists for this article. Let’s add that information in now.

+

So open that /app/views/articles/_form.html.erb and look at the paragraph where we added the image upload field. We’ll add in some new logic that works like this:

+ +

So, turning that into code…

+
+  <p>
+    <% if @article.image_file_name %>
+        <%= image_tag @article.image.url %><br/>
+    <% end %>
+    <%= f.label :image, "Attach a New Image" %><br />
+    <%= f.file_field :image %>
+  </p>
+
+

Test how that looks both for articles that already have an image and ones that don’t.

+

When you “show” an article that doesn’t have an image attached it’ll have an ugly broken link. Go into your /app/views/articles/show.html.erb and add a condition like we did in the form so the image is only displayed if it actually exists.

+

Now our articles can have an image and all the hard work was handled by paperclip!

+

Further Notes about Paperclip

+

Yes, a model (in our case an article) could have many attachments instead of just one. To accomplish this you’d create a new model, let’s call it “Attachment”, where each instance of the model can have one file using the same fields we put into Article above as well as an article_id field. The Attachment would then belong_to an article, and an article would have_many attachments.

+

Paperclip supports automatic image resizing and it’s easy. In your model, you’d add an option like this:

+
+as_attached_file :image, :styles => { :medium => "300x300>", :thumb => "100x100>" }
+
+

This would automatically create a “medium” size where the largest dimension is 300 pixels and a “thumb” size where the largest dimension is 100 pixels. Then in your view, to display a specific version, you just pass in an extra parameter like this:

+
+<%= image_tag @article.image.url(:medium) %>
+
+

If it’s so easy, why don’t we do it right now? The catch is that paperclip doesn’t do the image manipulation itself, it relies on a package called imagemagick. Image processing libraries like this are notoriously difficult to install. If you’re on Linux, it might be as simple as sudo apt-get install imagemagick. On OS X, if you have Mac Ports installed, it’d just be sudo port install imagemagick. On windows you need to download an copy some EXEs and DLLs. It can be a hassle, which is why we won’t do it during this class.

+

Installing HAML/SASS

+

Another plugin that I use in every project is actually two libraries in one. HAML is an alternative templating style to the default ERB (which you’ve been using, hence all the view templates ending in .erb). SASS is a library for writing CSS and it makes CSS much, much easier to work with.

+

Open your Gemfile and add a gem line for the gem haml. Go to your terminal and bundle and it should pull down the gem library for you. Stop (with the red square) then restart (green play button) your server within RubyMine. Both HAML and SASS are installed and ready to use.

+

Look in RubyMine’s left navigation pane for the folder /public/stylesheets/. Right click on this folder, click NEW, then DIRECTORY, and name it sass. Then right click on the sass folder, click NEW, FILE, then enter the name styles.sass

+

A Few Sass Examples

+

All the details about Sass can be found here: http://sass-lang.com/

+

We’re not focusing on CSS development, so here are a few styles that you can copy & paste and modify to your heart’s content:

+
+!primary_color = #AAA
+
+body
+  :background-color = !primary_color
+  :font
+    :family Verdana, Helvetica, Arial
+    :size 14px
+
+a
+  :color #0000FF
+  img
+    :border none
+
+.clear
+  :clear both
+  :height 0
+  :overflow hidden
+
+#container
+  :width 75%
+  :margin 0 auto
+  :background #fff
+  :padding 20px 40px
+  :border solid 1px black
+  :margin-top 20px
+
+#content
+  :clear both
+  :padding-top 20px
+
+

But our application isn’t setup to load that stylesheet yet. We need to make a change to our view templates.

+

Working with Layouts

+

We’ve created about a dozen view templates between our different models. We could go into each of those templates and add a line like this at the top:

+
+<%= stylesheet_link_tag 'styles' %>
+
+

Which would find the Sass file we just wrote. That’s a lame job, imagine if we had 100 view templates. What if we want to change the name of the stylesheet later? Ugh.

+

Rails and Ruby both emphasize the idea of “D.R.Y.” — Don’t Repeat Yourself. In the area of view templates, we can achieve this by creating a layout. A layout is a special view template that wraps other views. Look in your navigation pane for /app/views/layouts/, right click on that folder, click NEW and FILE then give it the name application.html.haml.

+

In this layout we’ll put the view code that we want to render for every view template in the application. Just so you can see what HAML looks like, I’ve used it to implement this layout. You’ll notice that HAML uses fewer marking characters than ERB, but you must maintain the proper whitespace/indentation. All indentations are two spaces from the containing element. Add this code to your application.html.haml:

+
+!!! Strict
+%html
+  %head
+    %title
+      JSBlogger
+    = stylesheet_link_tag 'styles'
+
+  %body
+    #container
+      #content
+        = yield
+
+

Now refresh your article listing page and you should see the styles take effect. Whatever code is in the individual view template gets inserted into the layout where you see the yield. Using layouts makes it easy to add site-wide elements like navigation, sidebars, and so forth.

+

NOTE: If you don’t see any change, look at your server log in RubyMine to see if there were any errors. At first I had a typo in one of the filenames so it wasn’t being picked up properly. You might also need to stop & restart your server if you didn’t do that after installing the haml gem.

+

Now that you’ve tried out three plugin libraries (Paperclip, HAML, and SASS), Iteration 4 is complete!

+

I5: Authentication

+

Authentication is an important part of almost any web application and there are several approaches to take. Thankfully some of these have been put together in plugins so we don’t have to reinvent the wheel.

+

The “flavor-of-the-week” is one named AuthLogic and I wrote up an iteration using it for the JSMerchant tutorial, but I think it is a little complicated for a Rails novice. You have to create several different models, controllers, and views manually. The documentation is kind of confusing, and I don’t think my tutorial is that much better.

+

So, instead, we’ll use the most common authentication plugin, restful_authentication.

+

Installing restful_authentication

+

In the previous iteration we installed libraries using RubyGems. This is the preferred way of managing plugins, but restful_authentication isn’t available through RubyGems. We’ll have to use the older method of installing a plugin.

+

At your terminal, enter the following:

+
+ruby git clone git://github.com/technoweenie/restful-authentication.git restful_authentication
+
+

If you aren’t able to pull down the code from GitHub, you’ll need to download the code in a zip file from github, then extract the zip into /vendor/plugins/restful_authentication/.

+

Once you’ve installed the plugin, you can test that it’s available with this command at your terminal:

+
+rails generate
+
+

In the output is a section title “Installed Generators”. It should have a line that says Plugins (vendor/plugins): authenticated. If it’s there, you’re ready to go!

+

Running the Generator

+

This plugin makes it easy to get up an running because one generator creates most of the code you’ll need. Run this from your terminal:

+
+rails generate authenticated user sessions
+
+

Take a look at the output and you’ll see it created about 16 files and added a few routes to the routes.rb file. The generator has a note that you shouldn’t forget to add the routes to routes.rb, but it’s already done that for you.

+

Let’s look at the CreateUsers migration that the generator created before we migrate the database. If you wanted your User models to have any additional information (like “deparment_name” or “favorite_color”) you could add columns for that. For our purposes these fields look alright and, thanks to the flexibility of migrations, if we want to add columns later it’s easy. So go to your terminal and enter:

+
+rake db:migrate
+
+

Creating a First Account

+

First, stop then restart your server in RubyMine to make sure it’s picked up the plugin. Then go to http://localhost:3000/users/new and the new user form should popup.

+

Go ahead and create yourself an account. If you’re successful it will just bounce you to the http://localhost:3000/ root page. There isn’t any information about our login status in our views, though, so it’s hard to tell if we’re really logged in.

+

Let’s open /app/views/layouts/application.html.haml and add a little footer so the whole %body% chunk looks like this:

+
+  %body
+    #container
+      #content
+        = yield
+        %hr
+        %h6
+          - if current_user
+            = "Logged in as #{current_user.login}"
+            = link_to "(logout)", logout_path
+          - else
+            = link_to "(login)", login_path
+
+

The go to http://localhost:3000/articles/ and you’ll get this error:

+
+NameError in Articles#index
+Showing app/views/layouts/application.html.haml where line #14 raised:
+undefined local variable or method `current_user' for #<ActionView::Base:0x103147400>
+
+

We tried to use the current_user helper that comes with the restful_authentication plugin, but Rails isn’t recognizing it. We need to do one more setup step. Open /app/controllers/application_controller.rb, and underneath the protect_from_forgery line, add this: include AuthenticatedSystem.

+

Now refresh your browser and your articles list should come up along with the new footer at the bottom.

+

An Aside on the Site Root

+

It’s annoying me that we keep going to http://localhost:3000/ and seeing the Rails starter page. Let’s make the root show our articles index page.

+

First, delete the file /public/index.html. Files in the public directory will take precedence over routes in our application, so as long as that file exists we can’t route the root address anywhere.

+

Second, open /config/routes.rb and right above the other routes add in this one:

+
+match '/' => 'articles#index'
+
+

Now visit http://localhost:3000 and you should see your article list.

+

Securing New Users

+

It looks like we can create a new user, but right away I want to make some changes. We’re just going to use one layer of security for the app — a user who is logged in has access to all the commands and pages, while a user who isn’t logged in can only post comments and try to login. But that scheme will breakdown if just anyone can go to this URL and create an account, right?

+

Let’s add in a protection scheme like this to the new users form:

+ +

That way when the app is first setup we can create an account, then new users can only be created by a logged in user.

+

We can create a before_filter which will run before the new and create actions of our users_controller.rb. Open that controller and on the second line you’ll see include AuthenticatedSystem. You can remove that since we put it in the application_controller and, in its place, put all this code:

+
+  before_filter :zero_users_or_authenticated, :only => [:new, :create]
+
+  def zero_users_or_authenticated
+    unless User.all.size == 0 || current_user
+      redirect_to root_path
+      return false
+    end      
+  end
+
+

The first line declares that we want to run a before filter named zero_or_authenticated when either the new or create methods are accessed. Then we define that filter, checking if there are either zero registered users OR if there is a user already logged in. If neither of those is true, we redirect to the root path (our articles list) and return false. If either one of them is true this filter won’t do anything, allowing the requested user registration form to be rendered.

+

With that in place, try accessing /users/new when you logged in and when your logged out. If you want to test that it works when no users exist, try this at your console:

+
+User.destroy_all
+
+

Then try to reach the registration form and it should work! Create yourself an account if you’ve destroyed it.

+

Securing the Rest of the Application

+

The first thing we need to do is sprinkle before_filters on most of our controllers:

+ +

Now our app is pretty secure, but we should hide all those edit, destroy, and new article links from unauthenticated users.

+

Open /app/views/articles/index.html.erb and find the section where we output the “Actions”. Wrap that whole section in an if clause like this:

+
+<% if current_user %>
+
+<% end %>
+
+

Look at the article listing in your browser when you’re logged out and make sure those links disappear. Then use the same technique to hide the “Create a New Article” link.

+

If you look at the show view template, you’ll see that we never added an edit link! Add that link now, but protect it to only show up when a user is logged in.

+

Your basic authentication is done, and Iteration 5 is complete!
+

+

I6: Extras

+

Here are some ideas for extension exercises:

+ +
+
+ + + + + + + + + + + + diff --git a/public/projects/jscontact.html b/public/projects/jscontact.html new file mode 100644 index 000000000..7890f767e --- /dev/null +++ b/public/projects/jscontact.html @@ -0,0 +1,1723 @@ + + + + + + + + JSContact - Jumpstart Lab Curriculum + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Jumpstart Lab Curriculum

+ +
+ +
+ +
+
+

In this advanced Rails project, you’ll create a contact manager. The tools that you will use include the following:

+ +

This project assumes you have already completed the general Ruby setup and I’m using Ruby 1.9.2. We’ll rely on the Bundler system to install other gems for us along the way.

+

In addition, I recommend you use the RubyMine IDE 3.1 available from JetBrains here.

+

We’ll use an iterative approach to develop one feature at a time. Here goes!

+

I0: Up and Running

+

Let’s lay the groundwork for our project. In your terminal, switch to the directory where you’d like your project to be stored. I’ll use ~/Projects.

+

Run the rails -v command and you should see your current Rails version, mine is 3.0.9. Let’s create a new Rails project:

+
+  rails new JSContact
+
+

Then cd into your project directory and open the project in your editor of choice.

+

Veering Off the “Golden Path” with RSpec

+

With our project created, we will veer off the Rails defaults for both the testing and JavaScript libraries. First, let’s setup RSpec as our testing framework. Open your project’s Gemfile and add this dependency:

+
+  gem 'rspec-rails'
+
+

Then, in your terminal window that’s in the project directory, run bundle:

+
+  bundle
+
+

Now rspec-rails is available, but we still need to do some setup to make it all work. Running this generator will perform the setup for you:

+
+  rails generate rspec:install
+
+

You should see output like this:

+
+  create  .rspec
+  create  spec
+  create  spec/spec_helper.rb
+
+

Now your project is set to use RSpec and the generators will use RSpec by default. You still have Rails’ default test folder hanging around — let’s delete that with:

+
+  rm -rf test
+
+

Or on Windows try:

+
+  rd /r test
+
+

Now you’re free of Test::Unit and ready to rock with RSpec.

+

Replacing Prototype with jQuery

+

Go back to your Gemfile and add this dependency:

+
+  gem 'jquery-rails'
+
+

Run bundle again and then run this generator from your project directory:

+
+  rails generate jquery:install
+
+

As it installs the files you’ll get a question about the conflicting rails.js file — enter y to overwrite it:

+
+      remove  public/javascripts/controls.js
+      remove  public/javascripts/dragdrop.js
+      remove  public/javascripts/effects.js
+      remove  public/javascripts/prototype.js
+    fetching  jQuery (1.5)
+      create  public/javascripts/jquery.js
+      create  public/javascripts/jquery.min.js
+    fetching  jQuery UJS adapter (github HEAD)
+    conflict  public/javascripts/rails.js
+Overwrite /Users/jcasimir/Projects/JSContact/public/javascripts/rails.js? (enter "h" for help) [Ynaqdh] y
+       force  public/javascripts/rails.js
+
+

The jquery-rails gem does everything for you… well, almost. Your layout needs to know which JavaScript libraries you want to load.

+

To tell it to use jQuery, open /config/application.rb and uncomment this line:

+
+  config.action_view.javascript_expansions[:defaults] = %w(jquery rails)
+
+

It’s currently the third section from the bottom, but YMMV.

+

Startup Your Local Server

+

Open a second terminal window, cd into your project directory, then start your server with:

+
+  rails server
+
+

This will, by default, use the Webrick server which is slow as molasses. Hit Ctrl-C to stop it. Some of the alternative servers are mongrel and unicorn. Here’s how to setup unicorn.

+

Add this the dependency to your Gemfile:

+
+  gem 'unicorn'
+
+

Run bundle from your project directory and wait for the installation to complete. Start the server like this:

+
+  unicorn
+
+

Load http://0.0.0.0:8080 in your browser and you should see the Rails splash screen. Click the “About Your Application” link and you should see all your library versions. If your database were not installed properly or inaccessible, you’d see an error here.

+

Setup Git Repository

+

I’ll assume that you’ve already setup Git on your system. From within your project directory, create a new repository with:

+
+  git init .
+
+

Then add all of the files in your current working directory to the repository and commit it:

+
+  git add .
+  git commit -m "Initial project generated"
+
+

At this point if you’re using GitHub, you could add that remote and push to it. For purposes of this tutorial, we’ll just manage the code locally.

+

Ship It

+

Next let’s integrate Heroku.

+

If you don’t already have one, you’ll need to create a Heroku account. The heroku gem will ask for your username and password the first time you run heroku create, but after that you’ll be using an SSH key for authentication.

+

Open up that Gemfile and add this dependency:

+
+  gem 'heroku'
+
+

Run the bundle command again and now you’ll have command-line access to Heroku.

+
+  heroku create
+
+

After running that command, you’ll get back the URL where the app is accessible. Try loading the URL in your browser and you’ll see the generic Heroku splash screen. It’s not running your code yet so push your project up like this:

+
+  git push heroku master
+
+

It’ll take a minute, then you should see a message that your application was successfully deployed like this:

+
+  -----> Launching... done
+         http://jscontact.heroku.com deployed to Heroku
+
+

Refresh your browser and you should see the Rails splash screen. Click “About your application’s environment” again, and…wait, what happened? When your app is running on Heroku it’s in production mode. This diagnostic info isn’t available when the server is running in production, so you’ll see a default error message. Don’t worry, everything should be running fine.

+

Dependency Cleanup

+

We’ve added several gems to our Gemfile, but several of them are only useful for development or testing. It doesn’t make sense, then, to have Heroku install and load them in production. It costs RAM and slows down our deployment. The Bundler system can handle this with environment blocks. I also like to remove all the comments from the Gemfile, leaving me with just this:

+
+  source 'http://rubygems.org'
+
+  gem 'rails', '3.0.9'
+  gem 'sqlite3'
+
+  group :development, :test do
+    gem 'rspec-rails'
+    gem 'jquery-rails'
+    gem 'heroku'
+    gem 'unicorn'
+  end
+
+

Now the RSpec, Heroku, Unicorn, and Unicorn gems will only be loaded in development and test environments. If you’re wondering, the jQuery gem doesn’t have any role in production because we’ve already stored the jQuery JavaScript files into our public directory using the generator. Similarly, though we’re deploying to Heroku, the heroku gem is only used during development. Save your new Gemfile and ship it:

+
+  git add .
+  git commit -m "Added dev and test blocks to Gemfile"
+  git push heroku master
+
+

Now we’re ready to actually build our app!

+

I1: Building People

+

We’re building a contact manager, so let’s start with modeling people. Since this is an advanced tutorial we won’t slog through the details of implementing a Person model, controller, and views. Instead we’ll take advantage of scaffolding tools.

+

A Feature Branch

+

But first, let’s make a feature branch in git:

+
+  git branch build_people
+  git checkout build_people
+
+

Now all our changes will be made on the build_people branch. As we finish the iteration we’ll merge the changes back into master and ship it.

+

Nifty Scaffold

+

I want to use the scaffold generator to create a model named Person, but I don’t care for some of the conventions used in the default Rails scaffold generator. Instead, I prefer the nifty-generators created by RailsCasts author Ryan Bates.

+

In the development section of your Gemfile, add a dependency for "nifty-generators", run bundle from the command prompt, then run this generator, answering yes to the conflict:

+
+  rails generate nifty:layout
+
+

That sets us up to use his “nifty” scaffolding. Let’s then generate a scaffolded model named Person that just has a first_name and last_name:

+
+  rails generate nifty:scaffold Person first_name:string last_name:string
+
+

The first line of the output shows Ryan sneaking the "mocha" gem into our Gemfile. Let’s move the gem dependency into the development/test block we already have and run bundle and then run the migration with rake db:migrate.

+

The generators created test-related files for us. They saw that we’re using RSpec and created corresponding controller and model test files. Let’s run those tests now:

+
+  rake
+
+

Assuming those test’s pass use git add . to add all current changes to your repository and commit them with a short message like git commit -m "Setup nifty_scaffold and used it to generate Person model"

+

Try creating a few sample people at http://localhost:8080/people through your browser.

+

Starting with Testing

+

Models are the place to start your testing. The model is the application’s representation of the data layer, the foundation of any functionality. In the same way we’ll build low-level tests on the models which will be the foundation of our test suite.

+

Open spec/models/person_spec.rb and you’ll see this:

+
+require File.dirname(__FILE__) + '/../spec_helper'
+
+describe Person do
+  it "should be valid" do
+    Person.new.should be_valid
+  end
+end
+
+

The describe block will wrap all of our tests or “examples” in RSpec parlance. Each it block is an example. Add a second one like this:

+
+  require File.dirname(__FILE__) + '/../spec_helper'
+
+  describe Person do
+    it "should be valid" do
+      Person.new.should be_valid
+    end
+
+    it "should not be valid without a first name"
+  end
+
+

Now go to your terminal, enter the command rake, and you should see output like this:

+
+  ..........*
+
+  Pending:
+    Person should not be valid without a first name
+      # Not Yet Implemented
+      # ./spec/models/person_spec.rb:8
+
+  Finished in 0.23891 seconds
+  11 examples, 0 failures, 1 pending
+
+

Awesome! We can see that it found the spec we wrote, "should not be valid without a first_name", tried to execute it, and found that the test wasn’t yet implemented. We have one example, zero failures, and one implementation pending. We’re ready to start testing!

+

Testing for Data Presence

+

First we’ll implement the existing example to actually check that first_name can’t be blank. Make your person_spec.rb look like this:

+
+  it "should not be valid without a first name" do
+    person = Person.new(:first_name => nil)
+    person.should_not be_valid
+  end
+
+

Run your tests with rake and you should now get this:

+
+..........F
+
+Failures:
+
+  1) Person should not be valid without a first name
+     Failure/Error: person.should_not be_valid
+       expected valid? to return false, got true
+     # ./spec/models/person_spec.rb:10:in `block (2 levels) in <top (required)>'
+
+Finished in 0.23345 seconds
+11 examples, 1 failure
+
+

The test failed because it expected a person with no first name to be invalid, but instead it was valid. We can fix that by adding a validation inside the model:

+
+  class Person < ActiveRecord::Base
+    attr_accessible :first_name, :last_name
+
+    validates :first_name, :presence => true
+  end  
+
+

If you run rake here you’ll see that the tests are still failing. Huh? Try running it like this:

+
+  rspec spec/models/person_spec.rb
+
+

Now the first test fails because it’s blank Person isn’t valid. Rewrite the test like this:

+
+it "should be valid" do
+  person = Person.new(:first_name => "Sample", :last_name => "Person")
+  person.should be_valid
+end
+
+

Now both tests will pass if you run rspec spec/models/person_spec.rb. Run just rake and the second test will still fail. This doesn’t make any sense!

+

Open up the generated /spec/controllers/people_controller_spec.rb. On about line 46 there should be this instruction:

+
+  Person.any_instance.stubs(:valid?).returns(true)
+
+

The Nifty Generator has created these controller tests using the Mocha gem for stubbing out method calls. He has stubbed calls to the valid? method to simplify the controller tests. But, these stubs are affecting our later model tests giving odd results.

+

When we run the model test directly, everything works. When we run just rake, it’s running these controller tests, setting up the mocking, then our model test is breaking. We’ll come back to controller testing later, but for now let’s comment out the whole /spec/controllers/people_controller_spec.rb.

+

Then run rake and your tests should pass.

+

Following that example for :first_name, let’s add a validation for :last_name to the model then write a test checking that a person is not valid without a last name.

+

Run rake and make sure you get 0 failures.

+

Experimenting with Our Tests

+

Go into the person.rb model and temporarily remove :last_name from the validates_presence_of line. Run your tests with rake. What happened?

+

This is what’s called a false positive. The is not valid without a last_name test is passing, but not for the right reason. Even though we’re not validating last_name, the test is passing because the model it’s building doesn’t have a valid first_name either. That causes the validation to fail and our test to pass. We need to improve the object created in the test so that the other attributes are valid and only the one being tested is invalid. Let’s refactor.

+

First, just below the describe line of our person_spec, let’s add this code which will get executed before each of our examples:

+
+  before(:each) do
+    @person = Person.new(:first_name => "John",
+                           :last_name => "Doe")
+  end
+
+

Then update your first name and last name tests to a format like this:

+
+  it "is not valid without a first_name" do
+    @person.first_name = nil
+    @person.should_not be_valid
+  end
+
+

Run your tests and now the is not valid without a last name test should fail. Read the output that RSpec gives you to help find the problem. In this case, it’s easy — just add :last_name back where you removed it from the validation in person.rb.

+

Checking the Checkers

+

The before(:each) clause we wrote will make writing test examples a lot easier, but we had better write a test to ensure that what we’re calling @person is actually valid! Write an example that just asserts that @person.should_be valid.

+

Run the tests to make sure they work and you should now have 3 examples, 0 failures.

+

Ship It

+

Hop over to your command prompt and let’s work with git. First, ensure that everything is committed on our branch:

+
+  git status
+  git add .
+  git commit -m "Finished implementing basic person functionality"
+
+

Then this branch is done. Let’s go back to the master branch and merge it in:

+
+  git checkout master
+  git merge build_people
+
+

Now it’s ready to send to Heroku and run our migrations:

+
+  git push heroku master
+  heroku rake db:migrate
+
+

Open up your production app in your browser and you should be able to create sample people.

+

I2: Phone Numbers

+

We’ve created a few tests to demonstrate the validations we already had in place. We wrote the validations first then the tests second. This process is better than no testing, but it isn’t Test Driven Development. Now we’ll experiment with writing the tests first, then writing just enough code to make the tests pass.

+

A Feature Branch

+

Let’s again make a feature branch in git:

+
+  git branch build_phone_numbers
+  git checkout build_phone_numbers
+
+

Now all our changes will be made on the build_phone_numbers branch. As we finish the iteration we’ll merge the changes back into master and ship it.

+

Modeling The Objects

+

First, let’s think about the data relationship. A person is going to have multiple phone numbers, and a phone number is going to attach to one person. In the database world, this is a one-to-many relationship, one person has many phone numbers.

+

One-to-Many Relationship

+

The way this is traditionally implemented in a relational database is that the “many” table holds a foreign key pointing back to the row from the “one” table that it belongs to. So we might have a person with ID number 6, then there would be phone numbers that have a foreign key person_id with the value 6.

+

Test First: A Person Should Have Phone Numbers

+

With that understanding, let’s write a test. We just want to check that a person is capable of having phone numbers. In your person_spec.rb let’s add this test:

+
+  it "should have an array of phone numbers" do
+    @person.phone_numbers.class.should == Array    
+  end
+
+

Run rake and make sure the test fails with undefined method 'phone_numbers'. Now we’re ready to create a PhoneNumber model and corresponding association in the Person model.

+

Scaffolding the Phone Number Model

+

We’ll use the nifty_scaffold generator again to save us a little time. For now we’ll keep the phone number simple, it’ll just have a number column that holds the number and a person_id column that refers to the owning person model. Generate it with this command at the terminal:

+
+rails generate nifty:scaffold PhoneNumber number:string person_id:integer
+
+

This will generate a new /spec/controllers/phone_number_spec.rb which you should comment out.

+

Run rake db:migrate to execute the generated migration. Run rake again and make sure the test isn’t passing yet.

+

Setting Relationships

+

Next open the person.rb model and add the association has_many :phone_numbers. Run rake and your tests should all pass.

+

Try it out yourself by going into the console via rails console and adding a phone number manually:

+
+p = Person.first
+p.phone_numbers.create(:number => '2024605555')  
+
+

Validating Phone Numbers

+

Right now the phone number is just stored as a string, so maybe the user enters a good-looking one like “2024600772” or maybe they enter “please-don’t-call-me”. Let’s add some validations to make sure the phone number can’t be blank.

+

Go into phone_number_spec.rb and mimic some of the same things we did in person_spec. We can start off by writing a before(:each) block to setup our PhoneNumber object. Enter this just below the describe line:

+
+  before(:each) do
+    @phone_number = PhoneNumber.new()
+  end
+
+

Let’s also change the "should be valid" test to use that variable:

+
+  it "should be valid" do
+    @phone_number.should be_valid
+  end
+
+

Then write a test ensuring that a PhoneNumber is connected to a Person:

+
+  it "should be associated with a person" do
+    @phone_number.should respond_to(:person)
+  end
+
+

Run your tests with rake and that last one will fail.

+

Go into the phone_number.rb model and add the relationship belongs_to :person. Run rake again and they they should all pass.

+

We want to start working on valid formats for a phone number, so let’s write a failing test first that checks if a phone number can be blank:

+
+  it "should not be valid without a number" do
+    @phone_number.number = nil
+    @phone_number.should_not be_valid
+  end
+
+

Run rake and you should have one failing test. Once you see red, go into the phone_number.rb model and add a validation checking the existence of the number, run your tests again, and make sure they’re green.

+

A PhoneNumber shouldn’t be allowed to float out in space, so let’s require that it be attached to a Person:

+
+  it "should not be valid without a person" do
+    @phone_number.person = nil
+    @phone_number.should_not be_valid
+  end
+
+

Run rake and your new test should fail. Add a validation that checks the presence of person. Then run rake.

+

Still have one test failing? Look carefully at which test is failing. It’s now the "should_be_valid" test. We need to modify our before block to actually make a valid PhoneNumber. Try this:

+
+  before(:each) do
+    @person = Person.create(:first_name => "Sample", :last_name => "Name")
+    @phone_number = @person.phone_numbers.create(:number => "2024605555")
+  end
+
+

A lot of work for two validations, but these are an important part of our testing base. If somehow one of the validations got deleted accidentally, we’d know it right away.

+

Commit

+

Since your tests are passing it’s a good time to commit your changes to the git repository:

+
+  git add .
+  git commit -m "Built phone numbers with simple validations"
+
+

Building a Web Display

+

We can create phone numbers in the console but that’s not very useful, let’s work on the views and forms. We’ll make use of the simpleform gem to help our forms.

+

Visit localhost:8080/people and you’ll see any sample people you’ve created so far. Let’s add a column that displays their phone numbers. Open the app/views/people/index.html.erb view. In that view:

+ +

Refresh your browser and it should give you an error message about the undefined helper method print_numbers.

+

When do we write helpers?

+

We should write a helper to encapsulate view-related code that involves logic. A partial template is for reusing view chunks that are mostly HTML, helpers are useful for view chunks that are mostly computation.

+

We want to output a comma-separated list of the person’s phone numbers. It’s a perfect candidate for a helper.

+

Testing Helpers

+

Many developers don’t test helpers, but I find they’re one of the most common failure points in production applications. Devs don’think they need to write tests for them because they’re “just little presentation methods” but then you see weird presentation artifacts in the application as the helpers get changed.

+

Writing tests for helpers is super easy, here’s how we can do it.

+

First, create a folder /spec/helpers/. Within that folder, create a file named phone_numbers_helper_spec.rb and open it. Start off with this frame:

+
+  require 'spec_helper'
+
+  describe PhoneNumbersHelper do
+  end
+
+

Run rake and, if you look closely you’ll see your new helper spec file in the executed command.

+

When we wrote our model spec, it made sense to drop right into an it example. There was a clear “it”, the model. In the helper spec, though, we want to set a context of an individual method named “print_numbers”. Then within that context we’ll write examples of how the method should operation.

+

We add the context by nesting another describe block inside the existing one like this:

+
+  require 'spec_helper'
+
+  describe PhoneNumbersHelper do
+    describe "print_numbers" do
+      # Examples go here
+    end
+  end
+
+

Now, inside the inner describe block, let’s write an example. Name it should output a comma-separated list of phone numbers. Within that example, make two PhoneNumber objects, pass them as an array into print_numbers and check that the output is the first number, a comma, a space, then the second number. Or, in code…

+
+describe "print_numbers" do
+  it "should output a comma-separated list of phone numbers" do
+    number_a = PhoneNumber.new(:number => "1234567")
+    number_b = PhoneNumber.new(:number => "7654321")
+    phone_numbers = [number_a, number_b]
+    print_numbers(phone_numbers).should == "1234567, 7654321"
+  end
+end
+
+

Run rake and the example will fail complaining that the method print_numbers doesn’t exist. Now you get to implement it!

+

Writing the print_numbers helper

+

Open the /app/helpers/phone_numbers_helper.rb file and add a method named print_numbers. It should take in one parameter which is an array of PhoneNumber objects, then use collect to gather the number from each of them. Finally, join the collected numbers by a comma and a space, returning the result.

+

When that works, run rake and your example should pass.

+

Considering other cases for print_numbers

+

One of the common visual defects I see on the web are unnecessary trailing commas. If we pass an array with just one PhoneNumber, will we get back “thenumber” or “thenumber,”? It had better be the first one, so let’s write a test!

+

I wrote an example named "should output just a phone number" and called print_numbers passing in an array of just a single number and validated the output. I ran rake and it succeeded.

+

There’s a documentation issue, though. The "should output just a single number" name only makes sense in the context when there is only one PhoneNumber object. It’s time to next a new describe block.

+

Refactor your examples to express more reasonable contexts. Here is my final structure with the actual test bodies removed — figure that part on your own:

+
+  require 'spec_helper'
+
+  describe PhoneNumbersHelper do
+    describe "print_numbers" do
+      describe "when there is more than one phone number" do
+        before(:each)
+
+        it "should output a comma-separated list of phone numbers"
+      end
+
+      describe "when there is only one phone number" do
+        it "should output a just the phone number"
+      end
+    end
+  end
+
+

Why Devs Hate Testing Helpers

+

The presentation layer is probably the most likely to change while an application is being built. We’ve written great tests that exercise a simple helper, then requirements change.

+

Now, instead of just a comma separated list, we need to implement an unordered (bullet) list where each number is its own bullet.

+

Let’s start with the tests. In the "when there is more than one phone number" context the before block still makes sense. The only thing that needs to change is the .should== , we need it to equal "<ul><li>thenumber</li><li>secondnumber</li></ul>" where firstnumber and secondnumber are your sample values.

+

Once that test fails for the right reason, you can try changing the helper itself. Use the content_tag helper to generate your ul and li elements. For example content_tag :li, "Number1" would output <li>Number1</li>. If you see that your strings are getting escaped, try tacking .html_safe onto the end.

+

Once it’s working for the first test you need to deal with the "should output just the phone number" test. What should you see when there’s just one number? Implement the test and verify it works.

+

This process is probably very frustrating and that’s ok. When you have a robust test suite it can exercise your app better than a full-time QA person. But building them up is not easy.

+

Commit

+

Your tests should all be passing, so check-in those changes!

+

Building a Web-based Workflow

+

Up to this point we’ve created phone numbers through the console. Let’s build up a web-based workflow.

+

What You Got From Scaffolding

+

When the scaffold generator ran it gave us a controller and some view templates. Check out the new form by loading up /phone_numbers/new in your browser.

+

It’s not that bad, but it’s not good enough.

+

Person-centric Workflow

+

Here’s what the customer wants:

+

“When I am looking at a single person’s page, I click an add link that takes me to the page where I enter the phone number. I click save, then I see the person and their updated information”

+

Go back to a person’s show page like /people/1. Our show page doesn’t even list the existing phone numbers. Let’s add that in real quick. Use your existing print_numbers helper to output the bullet list of the person’s numbers.

+

Then add a link under the list that says Add a Phone Number and points to the new_phone_number_path. See that the link displays in your browser and try clicking it.

+

Passing in the Person

+

When you look at the form you’ll see the Person text field, expecting the user to add the person_id manually. Obviously that’s ridiculous, let’s pass it in from the previous link.

+

When you write a path like new_phone_number_path() you can pass in extra data. Anything you put into the parentheses will be sent as GET-style parameters in the URL. Back on the person’s show page, let’s change the link to it includes the person’s ID:

+
+<%= link_to "Add a New Phone Number", new_phone_number_path(:person_id => @person.id ) %>
+
+

Go back to the show page in your browser, refresh, and click the link. See how the person_id is now embedded in the URL? We’re part way there, but the value isn’t in the “Person” text box yet.

+

Utilizing the Person ID Parameter

+

To make this work, we need to open app/controllers/phone_numbers_controller.rb and find the new action. By default it’s just creating a blank PhoneNumber by calling PhoneNumber.new. Instead, rewrite the method like this:

+
+  def new
+    @phone_number = PhoneNumber.new(:person_id => params[:person_id])
+  end
+
+

Then refresh your form and the person_id should be filled in.

+

Hiding the Person ID Input

+

Since our user doesn’t need to change the person_id, we should make it hidden. Open app/views/phone_numbers/_form.html.haml. Change the :person_id from using text_field to hidden_field. Refresh the form and the text box will disappear.

+

You can then rip out the label and the paragraph tags.

+

Processing the Form Data

+

Fill in the form with a phone number and click the save button. It should save then take you to the PhoneNumber show page. That’s the default scaffold behavior, but our customer wanted to return to the phone number’s person’s page. Open up /app/controllers/phone_numbers_controller.rb and look at the create action. When the phone number successfully saves, redirect to the phone number’s attached person.

+

Go back to the form, make another phone number, and you should end up on the person’s show page. Test the whole work-flow from clicking the link, entering the number, and arriving back on the show page.

+

Commit

+

Check those changes into the git repository.

+

Editing Numbers

+

We all make mistakes — let’s make those numbers editable. We can insert a simple edit link next to each number in the bullet list. Should we dive into the helper?

+

Helper vs. Partial

+

Let’s imagine what this helper is going to output when we add the links. It’ll have a wrapping UL, then LIs for each phone number which contain both the phone number itself and a link.

+

We said that helpers are good for computation-style tasks. We’re not doing any computation here, and as we add more markup with the links it becomes a bigger and bigger pain to write the helper and the tests.

+

It’s time to pull our helper and replace it with a partial. We’ll need a new approach to testing. Let’s open the phone_numbers_helper_spec.rb and comment the whole thing out. Then go into phone_numbers_helper.rb and comment it out too. On a real project, I’d delete each.

+

How do you test partials?

+

We don’t want to lose the value of testing, so we need a way to test the person’s show view. We want to see that the rendered HTML has edit links for each phone number along with the number itself.

+

When we want to test the output HTML we’re talking about an integration test. My favorite way to build those is using the Capybara gem with RSpec. Let’s get Capybara setup by opening your Gemfile and adding this line:

+
+  gem "capybara"
+
+

Then run bundle from your command line and it’ll install the gem.

+

Setup an Integration Test

+

Create a new folder named /spec/integration/. In that folder let’s make a file named people_views_spec.rb. Then here’s how I’d write the examples:

+
+  require 'spec_helper'
+  require 'capybara/rspec'
+
+  describe "the views for people", :type => :request do
+    before(:all) do
+      @person = Person.create(:first_name => "John", :last_name => "Doe")
+      number_a = @person.phone_numbers.create(:number => "1234567")
+      number_b = @person.phone_numbers.create(:number => "7654321")
+    end
+
+    describe "when looking at a single person" do
+      before(:all) do
+        visit person_path(@person)
+      end
+
+      it "should have edit links for each phone number" do        
+        @person.phone_numbers.each do |phone_number|
+          page.should have_link("edit", :href => edit_phone_number_path(phone_number))
+        end
+      end
+    end
+  end
+
+

With that in place, run rake and the example should fail as the edit links aren’t present.

+

Replacing the Helper with a Partial

+

Open the /views/people/show.html.erb template and replace…

+
+  <%= print_numbers(@person.phone_numbers) %>
+
+

With a call to render and a partial…

+
+  <%= render :partial => 'phone_numbers' %>
+
+

If you refresh your browser it’ll crash because there is no partial named “phone_numbers”.

+

Writing a Phone Numbers Partial

+

Now create a file named /views/people/_phone_numbers.html.erb. In that template, render a UL tags that contain LIs for each phone number attached to person. The LI should contain the number and a link that has the text “edit” and points to the edit_phone_number_path for that phone number.

+

Check it out in your browser and, when you think it’s right, run rake and your integration tests should pass.

+

Editing Workflow

+

Click one of the edit links and you’ll jump to the edit form. This form is simplified to just the number because it uses the /view/phone_numbers/_form.html.erb that we modified previously.

+

Change the phone number, click the update button, and the processing will work fine. But it redirects you to the phone number’s show page. Instead, modify the phone_numbers_controller.rb so it redirect’s you to that phone number’s person’s show page.

+

Fixing the Index

+

We were using that helper on the index view, too. Open up /views/people/index.html.erb and replace the call to the helper with a call to render.

+

Open the index page in the browser and boom, it’ll crash. The partial is expecting to access a session variable @person, but that doesn’t exist in the index view.

+

Instead we need to pass in the phone numbers, just like we did with the helper. Here’s the Rails 3 way to do it:

+
+  <%= render :partial => 'phone_numbers', :object => person.phone_numbers %>
+
+

Whatever you pass in as the object argument will get assigned to the local variable with the same name as the partial itself. Then in the partial change the iteration line to use that local like this…

+
+  <% phone_numbers.each do |phone_number| %>
+
+

Now the index works fine, but the show will be broken. Make similar changes to the render call in the show template to pass in the phone numbers. Then they should both work.

+

Commit

+

Check those changes into the git repository.

+

Destroying Phone Numbers

+

Lastly the customer wants a delete link for each phone number. Follow a similar process to…

+ +

Then, if that’s too easy try this challenge:

+

Write an integration test that destroys one of the phone numbers then ensure’s that it’s really gone from the database. You’ll need to use Capybara features like…

+ +

Phone Numbers are Done…For Now!

+

Wow, that was a lot of work, right? Just to list some phone numbers? Test-Driven Development (TDD) is really slow when you first get started, but after a few years you’ll have the hang of it! I wish I was joking.

+

Let’s Ship

+

Hop over to your command prompt and let’s work with git. First, ensure that everything is committed on our branch:

+
+  git status
+  git add .
+  git commit -m "Finished implementing phone number functionality"
+
+

Then this branch is done. Let’s go back to the master branch and merge it in:

+
+  git checkout master
+  git merge build_phone_numbers
+
+

Now it’s ready to send to Heroku and run our migrations:

+
+  git push heroku master
+  heroku rake db:migrate
+
+

Open up your production app in your browser and it should be rockin’!

+

I3: Email Addresses

+

What good is a contact manager that doesn’t track email addresses? We can take most of the ideas from PhoneNumber and apply them to EmailAddress. This iteration is going to be largely independent because you’ve seen it all before.

+

Start a Feature Branch

+

Let’s practice good source control and start a feature branch:

+
+  git branch create_email_addresses
+  git checkout create_email_addresses
+
+

Now we’re ready to work!

+

Writing a Test: A Contact Has Many Email Addresses

+

In your person_spec.rb refer to the existing example “should have an array of phone numbers” and create a similar example for email addresses. Verify that the test fails when you run rake.

+

Creating the Model

+

Use the nifty:scaffold generator to scaffold a model named EmailAddress which has a string field named address and an integer field named person_id.

+

By the way, whenever you use the rails generate command and mess up, just go up a line in your terminal and change rails generate to rails destroy, leaving all the other parameters. The files previously generated will be removed.

+

Remember how earlier we commented out the generated phone_numbers_controller_spec.rb? Let’s do the same thing with email_addresses_controller_spec.rb.

+

Run rake db:migrate then ensure that your test still isn’t passing with rake.

+

Setting Relationships

+

Open the Person model and declare a has_many relationship for email_addresses. Open the EmailAddress model and declare a belongs_to relationship with Person.

+

Now run your tests with rake and they should all pass. Since you’re green it be a good time to commit your changes.

+

Adding Model Tests and Validations for Email Addresses

+

Let’s add some quality controls to our EmailAddress model.

+ +

If you’re green, go ahead and check in those changes.

+

Completing Email Addresses

+

Now let’s shift over to the integration tests.

+

Displaying Email Addresses

+

Before you play with displaying addresses, create a few of them manually in the console.

+ +

Tests should be green here, so check in your changes. Then continue…

+ +

Check in your changes.

+

Create Email Address Link

+ +

Email Address Creation Workflow

+ +

When you’re green, check in your changes.

+

Email Address Editing Workflow

+

Try writing a similar test sequence to exercise the edit functionality:

+ +

Make it green, then check it in.

+

Email Address Deletion Workflow

+

Then, finally, deletion:

+ +

When you’re green, check it in.

+

Ship it!

+

Let’s ship this feature:

+ +

I4: Tracking Companies

+

Our app can track people just fine, but what about companies? What’s the difference between a company and person? The main one, for now, is that a person has a first name and last name, while a company will just have one name.

+

Thinking about the Model

+

As you start to think about the model, it might trigger your instinct for inheritance. The most common inheritance style in Single Table Inheritance (STI) where you would store both people and companies into a table named contacts, then have a model for each that stores data in that table.

+

STI has always been controversal, and every time I’ve used it, I’ve regretted it. For that reason, I ban STI!

+

Instead we’ll build up companies in the most simplistic way: duplicating a lot of code. Once we see where things are duplicated, we’ll extract them out and get the code DRY. A robust test suite will permit us be aggressive in our refactoring.

+

In the end, we’ll have clean, simple code that follows The Ruby Way.

+

Start a Feature Branch

+

It’s always a good practice to develop on a branch:

+ +

Starting up the Company Model

+

Use the nifty:scaffold generator to create a Company that just has the attribute name. After you run the generator, remember to comment out or delete the controller tests.

+

Run rake db:migrate to update your database.

+

Run rake to make sure your tests are green, then check your code into git.

+

Company Phone Numbers

+

Then we want to add phone numbers to the companies. We already have a PhoneNumber model, a form, and some views. We can reuse much of this…with one problem.

+

Let’s think about the implementation second though. Write your tests first.

+

Starting with Model

+

Open up the company_spec.rb and you’ll see the generated "should be valid" example. Take inspiration from the person_spec.rb and…

+ +

Moving Towards Phone Numbers

+

Now we’re rolling with some tests, so we should just bring everything over from person_spec to company_spec, right? I wouldn’t.

+

Just bring over and adapt the "should have an array of phone numbers" example. Run it and it’ll fail.

+

Now we get to think about implementation. To solve this for Person, we said that the PhoneNumber would belongs_to a Person and the Person would has_many :phone_numbers. Try it again here:

+ +

So we’re good — a Company has a method named phone_numbers and it returns an array.

+

Wait a Minute…

+

It should feel like something’s not right here. Let’s write a new spec that better exercises the relationship.

+
+  it "should respond with its phone numbers after they're created" do
+    phone_number = @company.phone_numbers.build(:number => "2223334444")
+    @company.phone_numbers.should include(phone_number)
+  end
+
+

Run that example and it will fail, thankfully. The phone_numbers method will not find the phone number because the relationships are lying.

+

When we say that a PhoneNumber belongs_to :company we imply that the phone_numbers table has a column named company_id. This is not the case, it has a person_id but no company_id.

+

We could add another column for company_id, but that would imply that a single number could be attached to one Person and one Company. That doesn’t make sense for our contact manager.

+

What we want to do is to abstract the relationship. We’ll say that a PhoneNumber belongs_to a contact, and that contact could be a Person or a Company.

+

Setup for Polymorphism

+

Our tests are still red so we’re allowed to write code. To implement a polymorphic join, the phone_numbers table needs to have the column person_id replaced with contact_id. Then we need a second column named contact_type where Rails will store the class name of the associated contact.

+

We need a migration. Use rails generate migration to create a migration that does the following to the phone_numbers table:

+ +

Then run the migration. Bye-bye, sample phone number data!

+

Build the Polymorphic Relationship

+

Run your tests and feel comforted by them BLOWING UP! If you significantly change your database structure like this and you don’t cause a bunch of tests to fail, be concerned about your test coverage.

+

Let’s start with the Person model, since that had passing tests before the migration. The current relationship says…

+
+  has_many :phone_numbers    
+
+

We just need to tell the relationship which “polymorphic interface” to use:

+
+  has_many :phone_numbers, :as => :contact    
+
+

The tests aren’t any better. We need to tell the PhoneNumber about the polymorphism too. Change these:

+
+  belongs_to :person
+  belongs_to :company
+
+

To this:

+
+  belongs_to :contact, :polymorphic => true
+
+

Now the models are properly associated, but my tests still look terrible. Maybe they need some revision.

+

Revising Phone Number Tests

+

Looking at the phone_number_spec.rb, you’ll see many references to Person. They likely need some tweaking.

+

I have an example "should not be valid without a person" example. That should be rebuilt like this:

+
+  it "should not be valid without a contact" do
+    @phone_number.contact = nil
+    @phone_number.should_not be_valid
+  end
+
+

It still fails because the validates_presence_of is looking for a :person. Change it to :contact and see where the tests stand.

+

I’m back to almost all green with just two failing tests.

+

Fixing a Person/PhoneNumber Integration Test

+

I have one integration test failing when it tries to exercise the delete functionality from the person’s show page. It’s failing with…

+
+  NoMethodError: undefined method `person' for #<PhoneNumber:0x000001046a4000>
+  ./app/controllers/phone_numbers_controller.rb:39:in `destroy'  
+
+

Open up the controller and I find it’s the redirect that’s crashing. I currently have this:

+
+  redirect_to @phone_number.person, :notice => "Successfully destroyed phone number."
+
+

This kind of issue is why we do TDD. A human tester is unlikely to have caught this crash, but the automated tests save our butts. It’s a simple fix, thanks to Rails’ built in redirect intelligence:

+
+  redirect_to @phone_number.contact, :notice => "Successfully destroyed phone number."
+
+

Rails will automatically figure out what class type contact is and go to the appropriate show page for that object.

+

And Finally, Company Phone Numbers

+

Now there’s just one red test, the one that set us down this path: "Company should respond with its phone numbers after they're created".

+

Open up the Company model and change the has_many :phone_numbers to reflect the polymorphism.

+

See green, breathe a sigh of relief, and check-in your code.

+

Integration tests for People

+

Check out the people_views_spec.rb and there are several examples that would apply to companies, too.

+

Create a companies_views_spec.rb and bring over anything related to phone numbers. Refactor the before block and copied tests to reflect companies.

+

Implementing Lists, Links, and Partials

+

After brining over the tests and updating them to exercise @company, I have two failures:

+ +

I peek at the web interface, and that’s not nearly enough. Other things I’m missing and don’t have examples for:

+ +

You’ve got this. Use your existing examples and code to guide you, write these two additional tests, and make all four pass!

+

And when it’s feeling easy…

+ +

You’ll need to rebuild the new action in PhoneNumbersController. Here’s how I’d do it:

+
+  def new
+    if params[:person_id]
+      contact = Person.find params[:person_id]
+    else
+      contact = Company.find params[:company_id]
+    end
+    @phone_number = contact.phone_numbers.new
+  end
+
+

You’ll also run into a gotcha with the PhoneNumber model. Change the attr_accesible line from:

+
+  attr_accessible :number, :person_id
+
+

To this:

+
+  attr_accessible :number, :contact_id, :contact_type
+
+

That will allow the contact attributes to be set by mass assignment, like we use in the create action for PhoneNumber.

+

Check It In!

+

When you’ve got everything passing and feel good about your coverage, check your code into the git repository.

+

Companies and Email Addresses

+

At this point you should feel comfortable writing both model specs and integration tests. Take the same approach you just used to write model specs for the Company related to email addresses.

+

Implement the polymorphism for EmailAddress to make it work, cleaning up any EmailAddress tests along the way.

+

Once you’re green, add integration tests for the company’s show page to exercise email address functionality. Edit and add to the views and controllers to make it all work.

+

Here are the steps I took:

+ +

That’s TDD for you. Now before you take a nap, take a look at the web interface. Specifically the show view of a company. More work to do…

+ +

Poke around in the web interface and I think we’ve got everything working.

+

Ship It

+ +

I5: Removing Code

+

Russ Olsen, in his book “Eloquent Ruby,” has a great line: “The code you don’t write never stops working.”

+

Our app has entirely too much code. It works, so we can’t call it “bad,” but we can up the code quality and drop the maintenance load by refactoring.

+

Refactoring is the process of taking working code and making it work better. That might mean reducing complexity, isolating dependencies, removing duplication — anything that leaves the external functionality the same while cleaning up the internals. In my opinion, the art of refactoring is the difference between people who write computer programs and people who are programmers. But there’s a catch to refactoring…

+

“Don’t change anything that doesn’t have [good test] coverage, otherwise you aren’t refactoring — you’re just changing [stuff].”

+

Our app has solid test coverage since most of it was written through TDD. That coverage gives us permission to refactor.

+

Start a feature branch

+

You’re on your master branch. Create an check out a branch named removing_code where we’ll make all our changes.

+

Helper Hitlist

+

Running the generators is great, but they create a lot of code. There are several files we can delete right away.

+

One thing you’ll see in many Rails projects is a helpers folder full of blank files. One of the things I hate about helpers is the naming convention from the generators is to create one helper for each model. Instead, you most often want your helper files grouping related functions, like a TimeAndDateHelper or CurrencyHelper.

+

Many developers get tricked into thinking helper classes should be tied to models and every model should have a helper class. That’s simply not true.

+

Look through the helpers folder and delete any that don’t have any methods. Even the PhoneNumbersHelper with its print_numbers method — we’re not using the helper anymore so let’s delete the whole file.

+

Run your tests and, if you’re green, check in the changes. To make sure git removes deleted files, use this:

+
+  git add -A .
+
+

Revising Controllers

+

Open up the phone_numbers_controller.rb. There are all the default actions here. Do we ever show a single phone number? Do we use the index to view all the phone numbers separate from their contacts? No. So let’s delete those actions.

+

Implementing a Before Filter

+

The remaining edit, update, and destroy methods all start with the same line. That’s not very DRY. We can take advantage of Rails’ before_filter system to execute a piece of code before each action is triggered.

+

There are a couple opinions on how to implement before_filters, here’s the way I think you should do it. Up at the top of the controller, just after the class line, add this:

+
+  before_filter :lookup_phone_number
+
+

Then go down to the bottom of the controller file. We want to add a private method to the controller like this:

+
+  private
+    def lookup_phone_number
+      @phone_number = PhoneNumber.find(params[:id])
+    end
+
+

Note that private doesn’t have an end line. Any method defined after the private keyword in a Ruby class will be private to instances of that object. Then within the lookup_phone_number method we just have the same line that was common to the three actions. Delete the line from the three actions and run your tests.

+

Some of them should fail. Look at the error messages and backtrace to see the issue.

+

Scoping a Before Filter

+

This lookup_phone_number method is being run before every action in the controller, but we only removed the line from update, edit, and destroy. When the new action is executed this line is raising and exception because there is no params[:id] data.

+

We want this before filter to only run for certain actions. Go back to the top of the controller and change…

+
+  before_filter :lookup_phone_number
+
+

…to run only for the listed actions…

+
+  before_filter :lookup_phone_number, :only => [:edit, :update, :destroy]
+
+

Or, you can even use the reverse logic:

+
+  before_filter :lookup_phone_number, :except => [:new, :create]
+
+

Run your tests and they should be passing.

+

Continuing the Before Filters

+

You can use the exact same pattern to implement before_filter actions in…

+ +

Go do those now and make sure your tests are still green. If the controller uses the show method, then it’s probably much shorter to use the except syntax on the before_filter.

+

Removing Blank Controller Actions?

+

As you remove the lookup line from actions like edit, it’s likely that your action is now totally blank. You can, then, remove the method from the controller. The before_filter will still be activated and the view template renders so things will work great.

+

But I don’t think it’s worth the developer cost. Not having the method in the file then having it work in the app is confusing. I’d recommend you just leave the stub.

+

Playing with ApplicationController

+

Did you notice that all those controllers inherit from ApplicationController? We’ve now got a private method in each controller that’s almost exactly the same. Could we condense them all into one method in the parent class?

+

Here’s what the method would have to do:

+ +

It’s a bit of metaprogramming that took me some experimenting, and here’s what I ended up with in my ApplicationController:

+
+  def find_resource
+    class_name = params[:controller].singularize
+    klass = class_name.camelize.constantize
+    self.instance_variable_set "@" + class_name, klass.find(params[:id])
+  end
+
+

Then I call that method from before_filter lines in each controller. The before_filter call could be pulled up here, but I think that makes the individual controllers too opaque.

+

Removing Unused Views for EmailAddresses and PhoneNumbers

+

We pulled out controller methods for both our phone_numbers and email_addresses controllers, hop over to their view folders and delete any unnecessary views.

+

Run your tests and make sure they’re still passing. If you’re green, check in your code.

+

Removing Model Duplication

+

If you look at the Person and Company models you’ll see there are two lines exactly duplicated in each:

+
+  has_many :phone_numbers, :as => :contact  
+  has_many :email_addresses, :as => :contact  
+
+

Essentially each of these models shares the concept of a “contact,” but we decided not to go down the dark road of STI. Instead, we should abstract their common code into a module. Right now it’s only two lines, but over time the common code will likely increase. The module will be the place for common code to live.

+

There are many opinions about where modules should live in your application tree. In this case we’re going to create a Contact module and it’s almost like a model, so let’s drop the file right into the models folder.

+ +
+  module Contact
+    
+  end
+
+ +
+  include Contact
+
+

Run your tests and everything will be broken. When we write a module we need to distinguish between three types of code:

+ +

The normal Ruby syntax to accomplish these jobs is a little ugly. In Rails 3 there’s a new feature library that cleans up the implementation of modules. We use it like this:

+
+  module Contact
+    extend ActiveSupport::Concern
+
+    included do
+    end
+
+    module ClassMethods
+    end
+
+    module InstanceMethods
+    end
+  end
+
+

Any code defined inside the included block will be run on the class when the module is included. Any methods defined in the ClassMethods submodule will be defined on the including class. And methods defined in the InstanceMethods submodule will be attached to instances of the class.

+

Where should your two has_many lines go? Figure it out on your own and use your tests to prove that it works. When you’re green, check it in.

+

Cutting Down View Redundancy

+

Do you remember copying and pasting some view code? I told you to do it, so don’t feel guilty.

+

Relocating the Phone Numbers Partial

+

If you look in views/companies/_phone_numbers.html.erb you’ll find the exact same code as in views/people/_phone_numbers.html.erb.

+

Here’s how to remove the duplication:

+ +
+  <%= render :partial => "phone_numbers", :object => company.phone_numbers %>
+
+

To have the folder in the partial name like this:

+
+  <%= render :partial => "phone_numbers/phone_numbers", :object => company.phone_numbers %>
+
+

That should bring you back to green. Then…

+ +

Relocating the Email Addresses Partial

+

Then repeat the exact same process for the email addresses partial.

+

Check It In

+

If your tests are green, check in the code and remember to use the -A flag on your add so git removes the deleted files.

+

Simplifying Views

+

Our views have a ton of markup in them and the output is ugly! Let’s cut it down.

+

Companies & People Index

+

Open the views/companies/index.html.erb and…

+ +

Run your tests and everything should be green. We dramatically changed the markup, but our tests still run fine. Good integration tests aren’t bound too closely to the HTML, they focus on content and functionality — not tags.

+

Now go through the same process for views/people/index.html.erb just using the word person instead of company where appropriate. Also combine the first name and last name into a single h4.

+

If everything is green then check it in.

+

Company & Person Show

+

Let’s make a similar set of changes to views/companies/show.html.erb

+ +

Run your tests and they should be green. Then, repeat the process for views/people/show.html.erb

+

When you’re green, check it in.

+

Company & Person Edit

+

Just a few small changes to the edit template:

+ +

PhoneNumber & Email Address New/Edit

+

Open the email_addresses/new.html.erb and change the title line from

+
+  <% title "New Email Address" %>
+
+

To this:

+
+  <% title "New Email Address for #{@email_address.contact}" %>
+
+

View it in your browser and…what is that? You probably see something like this:

+
+  New Email Address for #<Person:0x00000103226e70>
+
+

That person-like thing is what you get when you call the to_s method on an object that doesn’t have a to_s. This is the version all objects inherit from the Object class.

+

Testing a to_s Method

+

We want to write some code in our models, but we don’t have permission to do that without a failing test. Pop open the person_spec.rb file. Then add an example like this:

+
+  it "should convert to a string with last name, first name" do
+    @person.to_s.should == "Doe, John"
+  end  
+
+

The value on the right side will obviously depend on what value you setup in the before block. Run the test and it’ll go red.

+

Implementing a to_s

+

Now you get to open models/person.rb and define a to_s method like this:

+
+  def to_s
+    "#{last_name}, #{first_name}"
+  end
+
+

That should make your test pass. Go through the same process writing a test for the to_s of Company then implementing the to_s method.

+

Did It Work?

+

Flip over to your browser and you’ll see that the title on the new email address page should look much better. It isn’t making a test go green, though, and that makes me feel guilty. We’ve knowingly spent time implementing untested code.

+

Let’s write a quick integration test. In the email_addresses_views_spec we have a context "when looking at the new email address form". Within that, add this example:

+
+  it "should show the contact's name in the title" do
+    page.should have_selector("h1", :text => "#{@person.last_name}, #{@person.first_name}")
+  end
+
+

It’ll pass because you’ve already implemented the to_s in person.rb. Try a little “Comment Driven Development”:

+ +

Now in that same integration spec, you have a context named "when looking at the edit email address form". Implement a similar example there checking for the contact’s name in the h1, then change the view template to make it work.

+

More Form Tests & Tweaks

+

Implement the same technique on…

+ +

You probably want to create a phone_numbers_views_spec.rb and write the integration tests there before changing the view templates.

+

While you’re in there, I’m sure you’ll be tempted to write more integration tests for your phone numbers. Use the “Comment Driven Development” style to create the red/green cycle.

+

Making Use of the to_s Method

+

Lastly, consider searching your other views and simplifying calls to @company.name or @person.first_name with @person.last_name to just use the implicit to_s.

+

Write Less Markup

+

Writing HTML by hand is not so fun and mixing in ERB only makes it more frustrating. To be honest with you, I hate writing ERB. Let’s check out an alternative templating language named HAML.

+

HAML was created as a response to this question: “If we adopt whitespace a significant and assume we’re outputting HTML, what can we NOT write?” HAML uses indentation hierarchy to substitute for open/close tags and generally means writing significantly fewer characters on each template.

+

Get HAML Installed

+

Open up your Gemfile, add the dependency on the "haml" gem, save it and run bundle from the command prompt. Restart your web server so it loads the new library.

+

Refactor a View

+

Let’s see the difference by rebuilding an existing view. Open up your views/companies/index.html.erb. Create a second file in the same directory named views/companies/index.html.haml.

+

My ERB template looks like this:

+
+  <% title "Companies" %>
+
+  <div class="companies">
+    <% @companies.each do |company| %>
+      <div class="company">
+        <h4><%= company.name %></h4>
+        <%= render :partial => 'email_addresses/email_addresses', :object => company.email_addresses %>
+        <%= render :partial => 'phone_numbers/phone_numbers', :object => company.phone_numbers %>
+        <ul class="actions">
+          <%= link_to "Show", company %>
+          <%= link_to "Edit", edit_company_path(company) %>
+          <%= link_to "Destroy", company, :confirm => 'Are you sure?', :method => :delete %>
+        </ul>
+      </div>
+    <% end %>
+  </div>
+
+  <p><%= link_to "New Company", new_company_path %></p>
+
+

Copy that code and paste it into your new .haml page and we’ll strip it down. If your ERB template is properly indented like that, then the hard work is done for you. Here’s how we manually convert it to HAML:

+ +

Now you’ve got HAML! Rewriting my template reduced it from 657 bytes to 540 bytes, from 71 words down to 53 words. That’s a good savings since they output the exact same thing. Run your tests and everything should be cool.

+

Here’s my completed index.html.haml for reference.

+
+  - title "Companies" 
+
+  .companies
+    - @companies.each do |company| 
+      .company
+        %h4= company.name
+        = render :partial => 'email_addresses/email_addresses', :object => company.email_addresses 
+        = render :partial => 'phone_numbers/phone_numbers', :object => company.phone_numbers 
+        %ul.actions
+          = link_to "Show", company 
+          = link_to "Edit", edit_company_path(company) 
+          = link_to "Destroy", company, :confirm => 'Are you sure?', :method => :delete 
+
+  %p= link_to "New Company", new_company_path
+
+

Go ahead and delete the old ERB template. You don’t have to rebuild existing templates unless you want to, but we’ll build things in HAML moving forward.

+

Ship It

+

We’re done with this iteration and your tests are green — it’s time to ship it.

+

Make sure everything’s checked in on your feature branch, checkout master, merge in the branch, then push it to Heroku. Check out the results in your browser. Look at the beauty of those minus signs and many deleted files.

+

I6: Supporting Users

+

What’s the point of a web application if only one person can use it? Let’s make our system support multiple users. There are three pieces to making this happen:

+ +

Background on Authentication

+

There have been about a dozen popular methods for authenticating Rails applications over the past five years.

+

The most popular right now is Devise because it makes it very easy to get up and running quickly. The downside is that the implementation uses very aggressive Ruby and metaprogramming techniques which make it very challenging to customize.

+

In the past I’ve been a fan of AuthLogic because it takes a very straightforward model/view/controller approach, but it means you have to write a lot of code to get it up and running.

+

As we learn more about constructing web applications there is a greater emphasis on decoupling components. It makes a lot of sense to depend on an external service for our authentication, then that service can serve this application along with many others.

+

Why OmniAuth?

+

The best application of this concept is the OmniAuth. It’s popular because it allows you to use multiple third-party services to authenticate, but it is really a pattern for component-based authentication. You could let your users login with their Twitter account, but you could also build our own OmniAuth provider that authenticates all your companies apps. Maybe you can use the existing LDAP provider to hook into ActiveDirectory or OpenLDAP, or make use of the Google Apps interface?

+

Better yet, OmniAuth can handle multiple concurrent strategies, so you can offer users multiple ways to authenticate. Your app is just built against the OmniAuth interface, those external components can come and go.

+

Starting a Feature Branch

+

Before we start writing code, let’s create a branch in our repository. Here’s a one-liner to create a branch and check it out:

+
+  git checkout -b adding_authentication
+
+

Now you’re ready to write code.

+

Getting Started with OmniAuth

+

The first step is to add the dependency to your Gemfile:

+
+  gem "omniauth"
+
+

Then run bundle from your terminal.

+

OmniAuth runs as a “Rack Middleware” which means it’s not really a part of our app, it’s a thin layer between our app and the client. To instantiate and control the middleware, we need an initializer. Create a file /config/initializers/omniauth.rb and add the following:

+
+  Rails.application.config.middleware.use OmniAuth::Builder do
+    provider :twitter, "EZYxQSqP0j35QWqoV0kUg", "IToKT8jdWZEhEH60wFL94HGf4uoGE1SqFUrZUR34M4"
+  end
+
+

What is all that garbage? Twitter, like many API-providing services, wants to track who’s using it. They accomplish this by distributing API accounts. Specifically, they use the OAuth protocol which requires a “comsumer key” and a “consumer secret.” If you want to build an application using the Twitter API you’ll need to register and get your own credentials. For this tutorial, I’ve registered a sample application and given you my key/secret above.

+

Trying It Out

+

You need to restart your server so the new library and initializer are picked up. In your browser go to http://127.0.0.1:8080/auth/twitter and, after a few seconds, you should see a Twitter login page. Login to Twitter using any account, then you should see a Routing Error from your application. If you’ve got that, then things are on the right track.

+

If you get to this point and encounter a 401 Unauthorized message there is more work to do. You’re probably using your own API key and secret. You need to go into the settings on Twitter for your application, and add http://127.0.0.1 as a registered callback domain. I also add http://0.0.0.0 and http://localhost while I’m in there. Now give it a try and you should get the Routing Error

+

Handling the Callback

+

The way this authentication works is that your app redirects to the third party authenticator, the third party processes the authentication, then it sends the user back to your application at a “callback URL”. Twitter is attempting to send the data back to your application, but your app isn’t listening at the default OmniAuth callback address, /auth/twitter/callback. Let’s add a route to listen for those requests.

+

Open /app/config/routes.rb and add this line:

+
+  match '/auth/:provider/callback', :to => 'sessions#create'
+
+

Re-visit http://localhost:8080/auth/twitter, it will process your already-existing Twitter login, then redirect back to your application and give you Uninitialized Constant SessionsController. Our router is attempting to call the create action of the SessionsController, but that controller doesn’t exist yet.

+

Creating a Sessions Controller

+

Let’s use a generator to create the controller from the command line:

+
+  rails generate controller sessions
+
+

Then open up that controller file and add code so it looks like this:

+
+  class SessionsController < ApplicationController
+    def create
+      render :text => debug request.env["omniauth.auth"]
+      debugger
+    end
+  end
+
+

Revisit /auth/twitter and, once it redirects to your application, you should see a bunch of information provided by Twitter about the authenticated user! Now we just need to figure out what to do with all that.

+

Creating a User Model

+

Even though we’re using an external service for authentication, we’ll still need to keep track of user objects within our system. Let’s create a model that will be responsible for that data.

+

As you saw, Twitter gives us a ton of data about the user. What should we store in our database? The minimum expectations for an OmniAuth provider are three things:

+ +

Let’s start with just those three in our model. From your terminal:

+
+  rails generate model User provider:string uid:string name:string
+
+

Then update the database with rake db:migrate.

+

Creating Actual Users

+

How you create users might vary depending on the application. For the purposes of our contact manager, we’ll allow anyone to create an account automatically just by logging in with the third party service.

+

Hop back to the SessionsController. I believe strongly that the controller should have as little code as possible, so we’ll proxy the User lookup/creation from the controller down to the model like this:

+
+  def create
+    @user = User.find_or_create_by_auth(request.env["omniauth.auth"])
+  end
+
+ +

Now the User model is responsible for figuring out what to do with that big hash of data from Twitter. Open that model file and add this method:

+
+  def self.find_or_create_by_auth(auth_data)
+    user = self.find_or_create_by_provider_and_uid(auth_data["provider"], auth_data["uid"])
+    if user.name != auth_data["user_info"]["name"]
+      user.name = auth_data["user_info"]["name"]
+      user.save
+    end    
+    return user
+  end
+
+

To walk through that step by step…

+ +

Now, back to SessionsController, let’s add a redirect action to send them to the companies_path after login:

+
+  def create
+    @user = User.find_or_create_by_auth(request.env["omniauth.auth"])
+    redirect_to companies_path, :notice => "Logged in as #{@user.name}"
+  end
+
+

Now visit /auth/twitter and you should eventually be redirected to your Companies listing and the flash message at the top will show a message saying that you’re logged in.

+

UI for Login/Logout

+

That’s exciting, but now we need links for login/logout that don’t require manually manipulating URLs. Anything like login/logout that you want visible on every page goes in the layout.

+

Open /app/views/layouts/application.html.erb and you’ll see the framing for all our view templates. Let’s add in the following just below the flash messages:

+
+  <div id="account">
+    <% if current_user %>
+      <span>Welcome, <%= current_user.name %></span>
+      <%= link_to "logout", logout_path, :id => "login" %>
+    <% else %>
+      <%= link_to "login", login_path, :id => "logout" %>
+    <% end %>
+  </div>
+
+

If you refresh your browser that will all crash for several reasons.

+

Accessing the Current User

+

It’s a convention that Rails authentication systems provide a current_user method to access the user. Let’s create that in our ApplicationController with these steps:

+ +
+  private
+    def current_user
+      @current_user ||= User.find(session[:user_id]) if session[:user_id]
+    end  
+
+

By defining the current_user method as private in ApplicationController, that method will be available to all our controllers because they inherit from ApplicationController. In addition, the helper_method line makes the method available to all our views. Now we can access current_user from any controller and any view!

+

Refresh your page and you’ll move on to the next error, undefined local variable or method `login_path'.

+

Convenience Routes

+

Just because we’re following the REST convention doesn’t mean we can’t also create our own named routes. The view snipped we wrote is attempting to link to login_path and logout_path, but our application doesn’t yet know about those routes.

+

Open /config/routes.rb and add two custom routes:

+
+  match "/login" => redirect("/auth/twitter"), :as => :login
+  match "/logout" => "sessions#destroy", :as => :logout  
+
+

The first line creates a path named login which just redirects to the static address /auth/twitter which will be intercepted by the OmniAuth middleware. The second line creates a logout path which will call the destroy action of our SessionsController.

+

With those in place, refresh your browser and it should load without error.

+

Implementing Logout

+

Our login works great, but we can’t logout! When you click the logout link it’s attempting to call the destroy action of SessionsController. Let’s implement that.

+ +

Now try logging out and you’ll probably end up looking at the Rails “Welcome Aboard” page. Why isn’t your root_path taking affect?

+

If you have a file in /public that matches the requested URL, that will get served without ever triggering your router. Since Rails generated a /public/index.html file, that’s getting served instead of our root_path route. Delete the index.html file from public, and refresh your browser.

+

NOTE: At this point I observed some strange errors from Twitter. Stopping and restarting my server, which clears the cached data, got it going again.

+

Testing…?

+

We haven’t written tests for login/logout. Here are some excuses:

+ +

Let’s make a mental note that we want to try writing tests for the authentication parts of our app later and move on. We’ll get some pieces of it going in the next iteration.

+

Ship It

+

Hop over to a terminal and add your files, commit your changes, merge the branch, and push it to Heroku.

+

I7: Adding Ownership

+

We’ve got users, but they all share the same contacts. That, obviously, won’t work. We need to rethink our data model to attach contacts to a User. It’d be tempting to add a user_id column to every table in our database, but let’s see if that’s really necessary.

+

Back Into Testing

+

Let’s start with some tests. Open up user_spec.rb and add this example:

+
+  it "should have associated people" do
+    @user.people.should be_instance_of(Array)
+  end  
+
+

If you run rake that test will fail because there is no @user setup. We’ll need a before block.

+

Setting up a Factory

+

So far each of our test files has been making the objects it’ll need for the tests. If now decide that a Person had a required attribute of title, we’d have to update several spec files to create the objects properly.

+

This duplication makes our tests more fragile than they should be. We need to introduce a factory.

+

The most common libraries for test factories are FactoryGirl and Machinist. Each of them has hit a rough patch of maintenance, though, which guided me towards a third option.

+

Let’s use Fabrication which is more actively maintained. Open up your Gemfile and add a dependency on "fabrication" in the test/development environment. Run bundle to install the gem.

+

We can also change the behavior of Rails generators to create fabrication patterns instead of normal fixtures. Open up /config/application.rb, scroll to the bottom, and just below the javascript_expansions add this block:

+
+  config.generators do |g|
+    g.test_framework      :rspec, :fixture => true
+    g.fixture_replacement :fabrication
+  end  
+
+

Using Fabrication

+

Now we need to make our fabricator. Create a folder /spec/fabricators/ and in it create a file named user_fabricator.rb. In that file add this definition:

+
+  Fabricator(:user) do
+    name "Sample User"
+    provider "twitter"
+    uid {Fabricate.sequence(:uid)}
+  end
+
+

Then go back to user_spec.rb and add this before block:

+
+  before(:each) do
+    @user = Fabricate(:user)
+  end
+
+

Now rake your tests and it should fail for the reason we want — that people is undefined for a User.

+

Testing Associations

+

Open your User model and express a has_many relationship to people. Run rake and your example will still fail because it’s looking for a user_id column on the people table. We’ll need to add that.

+

Generate a migration to add the integer column named “user_id” to the people table. Run the migration, run your examples again, and they should pass.

+

Working on the Person

+

Let’s take a look at the Person side, so open the person_spec.rb. First, let’s refactor the before block to use a Fabricator. Create the /spec/fabricators/person_fabricator.rb file and add this definition:

+
+  Fabricator(:person) do
+    first_name "John"
+    last_name "Doe"
+  end  
+
+

Then in the before block of person_spec, use the fabricator like this:

+
+  before(:each) do
+    @person = Fabricate(:person)
+  end  
+
+

Run rake and make sure the examples are still passing.

+

Testing that a Person Belongs to a User

+

Add an example checking that the @person is the child of a User. Run rake and see it fail.

+

Then add the belongs_to association in Person, run the tests, and see if they pass.

+

My test looks like this:

+
+  it "should be the child of a User" do
+    @person.user.should be_instance_of(User)
+  end
+
+

This is really testing two things: that the person responds to the method call user and that the response is a User. My test is failing because the response is nil.

+

Revising the Person Fabricator

+

We need to work more on the fabricator. When we create a Person, we need to attach it to a User. It’s super easy because we’ve already got a fabricator for User. Open the person_fabricator.rb and add the line user! so you have this:

+
+  Fabricator(:person) do
+    first_name "John"
+    last_name "Doe"
+    user!
+  end
+
+

Now when a Person is fabricated it will automatically associate with a user. Run rake and your tests should pass.

+

More From the User Side

+

Let’s check that when a User creates People they actually get associated. Try this example in user_spec:

+
+  it "should build associated people" do
+    person_1 = Fabricate(:person)
+    person_2 = Fabricate(:person)
+    [person_1, person_2].each do |person|
+      @user.people.should_not include(person)
+      @user.people << person
+      @user.people.should include(person)
+    end
+  end
+
+

Run rake and it’ll pass because we’re correctly setup the association on both sides.

+

Now for Companies

+

Write a similar test for companies:

+
+  it "should build associated companies" do
+    company_1 = Fabricate(:company)
+    company_2 = Fabricate(:company)
+    [company_1, company_2].each do |company|
+      @user.companies.should_not include(company)
+      @user.companies << company
+      @user.companies.should include(company)
+    end
+  end
+
+

Run rake and it’ll fail for several reasons. Work through them one-by-one until it’s passing. Here’s how I did it:

+ +

With that, the tests should pass.

+

Refactoring the Interface

+

The most important part of adding the User and associations is that when a User is logged in they should only see their own contacts. How can we ensure that?

+

Writing an Integration Test

+

Let’s write integration tests to challenge this behavior. Create a new context within "the views for people" in people_views_spec.rb like this:

+
+  describe "when logged in as a user" do
+    before(:all) do
+      @user = Fabricate(:user)
+    end
+  end
+
+

Then within that context we can check that they see their people:

+
+  it "should display people associated with this user" do
+    @person_1 = Fabricate(:person)
+    @user.people << @person_1
+    visit(people_path)
+    page.should have_link(@person_1.to_s)
+  end
+
+

Run that and it should pass.

+

The Negative Case

+

Now let’s make sure they don’t see other user’s contacts. Here’s an example that should work.

+
+  it "should display not display people associated with another user" do
+    @user_2 = Fabricate(:user)
+    @person_2 = Fabricate(:person)
+    @user_2.people << @person_2
+    visit(people_path)
+    page.should_not have_link(@person_2.to_s)
+  end
+
+

We create a second user, attach them to a second person, then visit the listing. Run rake and this test will fail because the index is still showing all the people in the database.

+

Scoping to the Current User

+

Open up the PeopleController and look at the index action. It’s querying for Person.all, but we want it to only display the people for current_user. Change the action so it looks like this:

+
+  def index
+    @people = current_user.people
+  end
+
+

Then run rake and you’ll find your test is crashing because current_user is nil. Our tests aren’t logging in, so there is no current_user.

+

Faking a Login

+

We need to have our tests “login” to the system. We don’t want to actually connect to the login provider, we want to mock a request/response cycle.

+

Here’s one way to do it. Create a folder /spec/support if you don’t have one already. In there create file named omniauth.rb. In this file we can define methods that will be available to all specs in the test suite. Here’s how we can fake the login:

+
+  def login_as(user)
+    OmniAuth.config.test_mode = true
+    OmniAuth.config.mock_auth[:twitter] = {
+        "provider" => user.provider,
+        "uid" => user.uid,
+        "user_info" => {"name"=>user.name}
+    }  
+    visit(login_path)
+  end
+
+

Now we can call the login_as method from any spec, passing in the desired User object, then the system will believe they have logged in.

+

Building More Fabricators

+

We’re working towards a refactoring of the integration tests. Let’s build up some fabricators that they’ll use.

+

email_address_fabricator.rb

+
+  Fabricator(:email_address) do
+    address "sample@sample.com"
+  end
+
+

phone_number_fabricator.rb

+
+  Fabricator(:phone_number) do
+    number "2223334444"
+  end
+
+

person_fabricator.rb

+
+  Fabricator(:person) do
+    first_name "John"
+    last_name "Doe"
+    user!
+  end
+
+  Fabricator(:person_with_details, :from => :person) do
+    email_addresses!(:count => 2){|person, i| Fabricate(:email_address, :address => "sample_#{i}@sample.com", :contact => person)}
+    phone_numbers!(:count => 2){|person, i| Fabricate(:phone_number, :number => "#{i.to_s*10}", :contact => person)}
+  end
+
+

user_fabricator.rb

+
+  Fabricator(:user) do
+    name "Sample User"
+    provider "twitter"
+    uid {Fabricate.sequence(:uid)}
+  end
+
+  Fabricator(:user_with_people, :from => :user) do
+    people!(:count => 3){|user, i| Fabricate(:person, :first_name => "Sample", :last_name => "Person #{i}", :user => user) }
+  end
+
+  Fabricator(:user_with_people_with_details, :from => :user) do
+    people!(:count => 3){|user, i| Fabricate(:person_with_details, :first_name => "Sample", :last_name => "Person #{i}", :user => user) }
+  end
+
+

Refactoring people_views_spec

+

Now that we want to scope down to just people attached to the current user we’ll need to make some changes to people_views_spec. Here is the structure we want for these tests. I’ve removed the example bodies, but you have most of them built already. Reorganize them to fit into this structure.

+
+  require 'spec_helper'
+  require 'capybara/rspec'
+
+  describe "the views for people", :type => :request do
+    describe "when logged in as a user" do
+      before(:all) do
+        @user = Fabricate(:user_with_people_with_details)
+        login_as(@user)
+      end
+
+      describe "when looking at the list of people" do
+        before(:each) do
+          visit people_path
+        end
+
+        it "should display people associated with this user"
+        it "should not display people associated with another user"
+      end
+
+      describe "when looking at a single person" do
+        before(:each) do
+          @person = @user.people.first
+          visit person_path(@person)
+        end
+
+        it "should have delete links for each email address"
+        it "should have an add email address link"
+        it "should go to the new email address form when the link is clicked"
+        it "should display each of the email addresses"
+        it "should have edit links for each phone number"
+        it "should have delete links for each phone number"
+        it "should show the person after deleting a phone number"
+        it "should show the person after deleting an email address"
+      end
+    end
+  end
+
+

And Now, Companies

+

You’ve done good work on people, but now we need to scope companies down to just the logged in user and refactor the tests as necessary.

+

Follow the same processes and go to it!

+

Breathe, Ship

+

That was a tough iteration but thinking about the tests helped up find the trouble spots. If your tests are green, check in your code, checkout the master branch, merge your feature branch, and ship it to Heroku!

+
+
+ + + + + + + + + + + + diff --git a/public/projects/jsmerchant.html b/public/projects/jsmerchant.html new file mode 100644 index 000000000..5b10235a6 --- /dev/null +++ b/public/projects/jsmerchant.html @@ -0,0 +1,1181 @@ + + + + + + + + JSMerchant - Jumpstart Lab Curriculum + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Jumpstart Lab Curriculum

+ +
+ +
+ +
+
+

In this project you’ll build an e-commerce site for a small grocery that wants to sell products directly to customers over the web. The project will be built in discreet iterations:

+ +

Each of these iterations will start and end with a working product.

+

I0: Up and Running

+

Part of the reason Ruby on Rails became popular quickly is that it takes a lot of the hard work off your hands, and that’s especially true in starting up a project. Rails practices the idea of “sensible defaults” and tries to, with one command, create a working application ready for your customization.

+

Setting the Stage

+

First we need to make sure everything is setup and installed. See the Preparation for Rails Projects page for instructions on setting up and verifying your Ruby, Rails, and add-ons.

+

Generate the Project

+

Let’s lay the groundwork for our project. In your terminal, switch to the directory where you’d like your project to be stored. I’ll use ~/Projects.

+

Run the rails -v command and you should see your current Rails version. The most recent is 3.0.9. Let’s create a new Rails project:

+
+  rails new JSMerchant
+
+

Then cd into your project directory and open the project in your editor of choice, I’ll use TextMate.

+

Booting the Server

+

From your project directory we need to run the bundler system to setup our dependency libraries. At the command prompt enter:

+
+  bundle
+
+

Once that completes your app is ready to go. Start it up with this instruction:

+
+  rails server
+
+

Then try loading the address http://localhost:3000/. You should see Rails’ “Welcome Aboard” page. Click the “About your application’s environment” link and it’ll display the versions of all your installed components.

+

Using Scaffolds

+

Rails makes it really easy to begin modeling your data using scaffolding. The built in scaffolds are fine, but I prefer to use the nifty-generators created by RailsCasts author Ryan Bates.

+

In the development section of your Gemfile, add a dependency for "nifty-generators", run bundle from the command prompt again to install the library.

+

Nifty Layout

+

After it finishes, run this generator, answering yes to the conflict:

+
+  rails generate nifty:layout
+
+

That sets us up to use his “nifty” scaffolding. In the course of generating our the layout scaffold, the NiftyGenerators package inserted a new dependency in the Gemfile. Run bundle again to set it up.

+

Nifty Scaffold

+

We’ll start by thinking about a “product” in our store. What attributes does a product have? What type of data are each of those attributes? We don’t need to think of EVERYTHING up front, here’s a list to get us started:

+ +

Why make price an Decimal? If you make it a regular float, you’re going to run into mathematic inconsistencies later on. Prices aren’t real floats because the number of places after the decimal don’t change — it’s always two. We’ll have to add this column into the migration manually.

+

Ok, time to finally generate your scaffold. Enter this command:

+
+rails generate nifty:scaffold Product title:string price:decimal description:text image_url:string
+
+

Reading that line out loud would sound like “run the generator named nifty:scaffold and tell it to create an object named Product that has a title which is a string, a price that is a decimal, a description that is text, and a image_url that is a string” The generator will then create about 30 files and directories for you based on this information.

+

Setting up the Database

+

Now, in your browser, go to http://localhost:3000/products. Hopefully you get an error screen that starts off like this:

+
+ActiveRecord::StatementInvalid in ProductsController#index
+SQLite3::SQLException: no such table: products: SELECT * FROM "products" 
+
+

The second line really tells us what the problem is — no such table: products. Our database doesn’t have a products table yet. Look in the /db/migrate folder of your project, and open the file that ends create_products.rb.

+

This file is called a migration. It’s Rails’ way of working with your database to create and modify tables in your database. It has two sections, self.up which is what it does to create some change in the DB, and self.down which is what it would do to undo those changes. In the case of our generated CreateProducts migration, the self.up section has code to create a table named products, then create the title, price, description, and image_url columns with the types we specified. The self.down just drops the whole table.

+

This method of modifying the database was one of the big new ideas in Rails. It used to be a big pain to keep your development database structure in sync with your production database and with the development machines of others on your team. Migrations take care of the complication for us.

+

We need to add extra options to our price column. Inside the self.up, modify the price line so it looks like this:

+
+  t.decimal :price, :precision => 8, :scale => 2
+
+

We want a column named price with type decimal. We give it two additional options: the precision controls how many digits the number can have in total. The scale controls how many digits come after the decimal. So this column will have a maximum value of 999999.99, which would be some expensive groceries.

+

Now you need to run this migration so it actually creates the products table in the database. In your Terminal, enter the following:

+
+rake db:migrate
+
+

You should see output explaining that it created the table named products.

+

Creating Sample Products

+

Now go back to your web browser and refresh the page. You should get a page that says “Listing Products”, click the “New Product” link and it’ll bring up a very simple form. Enter the following data:

+ +

Click Create. If everything looks good on the next page, click the Back link. Click the “New Product” link and enter the second product:

+ +

Create it and look at your products listing. Now you have a web store! (NOTE: The images will be missing for now, that’s ok!)

+

How does Rails do that Magic?

+

Let’s take a peek at how this is all working. Browse your project files and look at the following files:

+ +
+  def index
+    @products = Product.all
+  end
+
+ +
+<h1>Listing products</h1>
+
+<table>
+  <tr>
+    <th>Title</th>
+    <th>Price</th>
+    <th>Description</th>
+    <th>Image_url</th>
+  </tr>
+
+<% @products.each do |product| %>
+  <tr>
+    <td><%= product.title %></td>
+    <td><%= product.price %></td>
+    <td><%= product.description %></td>
+    <td><%= product.image_url %></td>
+    <td><%= link_to 'Show', product %></td>
+    <td><%= link_to 'Edit', edit_product_path(product) %></td>
+    <td><%= link_to 'Destroy', product, :confirm => 'Are you sure?', :method => :delete %></td>
+  </tr>
+<% end %>
+</table>
+
+

Now you’ve got a database-driven web application running and Iteration 0 is complete.

+

Iteration 1: Basic Product Listings

+

So now you might be impressed with yourself. You’ve got a web store just about done, right? Real stores have a ton of information …pictures, data, reviews, categories not to mention the ability to actually buy something. We’ll get there — for now let’s make our store look a little more respectable.

+

Working with Layouts

+

In the first iteration I lied to you about how the view step works. When you’re looking at the products listing, Rails isn’t just grabbing the index.html.erb. It’s also looking in the /app/views/layouts/ folder for a layout file. Expand that directory in your editor and you’ll see that the scaffold generator created a file named application.html.erb for us.

+

You can think of a layout like a wrapper that is put around each of your view templates. Layouts allow us to put all the common HTML in one place and have it used by all our view templates. For instance, it’d be a pain to have to write out the whole <head> section in each of our ERB templates. Open the layout file and you’ll see a lot of the HTML framework is already in the layout file.

+

If you’re on Amazon each of their pages has a certain look and feel. They have common navigation, stylesheets, titling, etc. The layout is where we take care of these common elements. In the <head> section of the layout it currently has line with a <title> — change it so it reads like this:

+
  
+  <title>FoodWorks - Products: <%= controller.action_name %></title>  
+
+

Hit save, switch back to your browser with the product listing, and hit refresh. The title of your browser tab should now say “FoodWorks – Products: index”. Then click the New product link. The window title should now say “FoodWorks – Products: new”. Even though the index and new actions have different view templates they share the same layout, so the change we made shows up in both places. Let’s add a little more to the layout file…

+ +
+<body>
+<p style="color: green"><%= flash[:notice] %></p>
+<div class="wrapper">
+    <div class="header">
+      <h1>FoodWorks</h1>
+      <p>Your Online Grocery</p>
+    </div>
+
+    <div class="sidebar">
+      (sidebar)  
+    </div>
+
+    <div class="main">
+      <%= yield %>
+    </div>
+ 
+    <div class="footer">
+      FoodWorks Online Grocery<br/>
+      A Rails Jumpstart Project
+    </div>
+</div>
+</body>
+
+

Now save your layout and refresh the products listing. It should look a little prettier, but we haven’t changed much about the content yet.

+

Editing a View Template

+

Let’s open the /app/views/products/index.html.erb view template so we can make some changes.

+ +
+  <p><%= link_to "Create a New Product", new_product_path, :id => "new_product" %></p>
+
+ +
+<% @products.each do |product| %>
+  <tr>
+    <td><%= image_tag "/images/products/#{product.image_url}" %></td>
+    <td><span class="product_title"><%= product.title %></span><%= product.description %></td>
+    <td><%= product.price %></td>
+  </tr>
+<% end %>
+
+

Try looking at the page in your web browser. Rework the table’s THs to match up with the new TDs.

+

Download the sample images for products here: products.zip. Move the images into public/images/products/.

+

Save that and check out your products index at http://localhost:3000/products.

+

Fixing the Price Display

+

When you look at the products index the price is a little ambiguous. It doesn’t have a currency marker. Let’s clean it up with a view helper.

+

A helper, in Rails, is code that helps you manipulate data as part of the presentation. We want to make a helper where we can send in a number like “2.25” and it gives us back “$2.25”, the format our shoppers are anticipating.

+ +
+  def print_price(price)
+    "$" + price
+  end
+
+

Go back to your index.html.erb file and change <%= product.price %> to <%= print_price(product.price) %>. Save it and refresh your products display. Notice anything?

+

If you have a price that ends in a zero, like your Green Grapes at $2.00, you’ll see that the trailing zeros are being cutoff. That’s no good.

+

Look at your print_price helper again. What’s the right fix? One approach is to use the format method in Ruby like this:

+
+  def print_price(price)
+    format("$%.2f", price)
+  end
+
+

That works great, check it out in your browser. The only objection is that it doesn’t internationalize. When we want to start selling groceries in the EU, we’ll need to rewrite this code.

+

Rails has a built-in helper method for exactly this purpose named number_to_currency. Its full description is here: http://api.rubyonrails.org/classes/ActionView/Helpers/NumberHelper.html#method-i-number_to_currency

+

We can use it just like this:

+
+  def print_price(price)
+    number_to_currency price
+  end
+
+

Check out the results in your browser; your store is starting to look good!

+

Linking to the Product Page

+

When you’re on the index page there’s no way to get to the “show” page for an individual product. Go back to the index.html.erb and use the link_to helper to insert links to the show page. link_to requires two parameters: what the link should say and where it should point. To trigger a show action, point the link towards product_path(product).

+

Working on the New Product Page

+

Click the Create a New Product link on your index page. Rails will access your products_controller, find the new method, run that code, then load the new.html.erb view template. Open that ERB file and you’ll see there isn’t much there.

+

Change the title to an H1 that reads “Create a New Product.” Then to manipulate the form itself we’ll need to jump into _form.html.erb.

+ +

Create a new product and enter in this information EXACTLY:

+ +

Click Create then look at the page you get back.

+

Validating the Price

+

What’s up with the price? Is that what you expected?

+

We tried to put “$2.99” to the database, but it’s expecting a number, not something with a dollar sign and a period. How does it end up with $0?

+

The data is coming in from our form as a string, then Rails is trying to coerce it into a decimal. Open rails console and try calling "$2.99".to_f to convert it to a float. This is similar to the coercion that happens when we convert it to a decimal. We get zero!

+

How can we help the user not make this mistake? Adding a validation.

+ +

Now you should see some great things that Rails does for “free”. It keeps us on the product creation screen, it tells us that there was a problem, it tells us what the problem is, then it highlights both the label and the form field in red. Makes it pretty clear, right? Take off the dollar sign so your price just says “2.99” and click Create again.

+

This is an opportunity to make things easier on our users. Typing a dollar sign in the price is perfectly reasonable from their perspective. We can handle it transparently on the server side.

+

When the form is being processed it passed the hash of values from the form to Product.new. ActiveRecord then goes through the values in that hash and stores them into the new record. It doesn’t directly manipulate the object’s attributes, it uses setter methods kind of like this:

+
+  def price=(input)
+    self.price = input
+  end
+
+

We don’t see that method in our code because ActiveRecord generates it on the fly at load time. But we can override the method if we want to do something special, like delete a dollar sign:

+
+  def price=(input)
+    input.delete!("$")
+    super
+  end
+
+

The first line does an in-place delete (delete! as opposed to delete), removing any dollar signs. Then, with the value changed, super invokes the original price= method.

+

Try it out in your console and browser. Set prices like “2.99” and “$3.99” — they should both work as expected.

+

Now, go ahead and create a few more sample products. Look at the folder of images that you copied over to see what’s available. Make at least 5 products for your store.

+

Iteration 1 is complete!

+

Iteration 2 – Handling Stock

+

Any good store needs to manage stock. When customers are shopping they should be able to see the current stock. When people buy something, the stock goes down. Administrators should be able to arbitrarily change the stock count.

+

Modifying the Database

+

Anytime we’re tracking new data we’ll need to modify the database. Jump over to your Terminal and generate a migration with this command:

+
+rails generate migration add_stock_to_products
+
+

After it generates, open the migration (look in /db/migrate) and in the self.up add the line below.

+
+add_column :products, :stock, :integer, :default => 0
+
+

Then in the down add the opposite:

+
+  remove_column :products, :stock
+
+

Run the migration with rake db:migrate then the column exists in your database.

+

Adding to the Products Listing

+

Let’s open up the view for our products index (/app/views/products/index.html.erb) and add in a column after Price for Stock in the THs. Down in the TDs, write this:

+
+<td><%= print_stock(product.stock) %></td>
+
+

So that’s expecting to use the helper method named print_stock. Here’s the logic we want to implement:

+ +

Go into the products_helper.rb and create a method named print_stock then fill in the blank lines with the stock messages:

+
+def print_stock(stock)
+  if stock > 0
+    
+  else
+    
+  end
+end
+
+

With the helper implemented refresh your products index and you should see all products out of stock.

+

Making the Stock Editable

+

Hop into the Show page for your first project then click the Edit link.

+

This edit form only shows the original fields that were there when we ran the scaffold generation, but it’s easy to add in our stock. Open the form template at /app/views/products/_form.html.erb

+

Using the title and price as examples, write a paragraph for the stock including the label and a text field. Refresh your web browser and you should see the stock field available for editing.

+

Since it’s just a raw text field, let’s add some validation to the product.rb model to make sure we don’t put something crazy in for the stock. Check out the Rails API documents here http://ar.rubyonrails.org/classes/ActiveRecord/Validations/ClassMethods.html and figure out how to write ONE validation that makes sure:

+ +

Once your validation is implemented, give it a try with illegal values for Stock like -50, hello!, and 5.5. Is it failing?

+

Dealing with attr_accessible

+

Your validation probably isn’t working, right? Your form isn’t changing the value of stock, but it also isn’t showing a validation error. What’s up?

+

Take a look in the Product model again. See the first line about attr_accessible? This method all declares which of the model’s attributes allow “mass-assignment.” Mass-Assignment in Rails is when we use this style of setting attributes:

+
+  p = Product.new(:title => "Apples", :price => "0.49", :description => "They're apples.")
+
+

Typically applications use mass-assignment when processing form data in a create or update action. When you use attr_accessible, only the listed attributes can be set using this style. Our issue is that stock isn’t listed, so it’s being ignored during the mass-assignment.

+

Add stock to the attr_accessible list and retry your good and bad stock values.

+

With those validations implemented, add stock to most of your products so we can do some shopping.

+

Iteration 2 is complete!

+

Iteration 3: Designing Orders

+

We’ve got products, but until we have a way to create orders then our store isn’t going to make any money.

+

Brainstorming the Data Structure

+

Let’s think about what an “order” is:

+ +

Looking at this from the database perspective, we’ll need two objects:

+ +

With that in mind, go to your Terminal and generate some scaffolding and migrate the database:

+
+rails generate nifty:scaffold OrderItem product_id:integer order_id:integer quantity:integer
+rails generate nifty:scaffold Order customer_id:integer status:string
+rake db:migrate
+
+

The Data Models

+

From there we need to build up our two data models and express their relationships.

+

The OrderItem Model

+

Jump into /app/models/order_item.rb and we’ll work on relationships. Since the OrderItem holds a foreign key referencing an Order, we’d say that that it belongs_to the Order. Add the relationship to the model like this:

+
+class OrderItem < ActiveRecord::Base
+  attr_accessible :product_id, :order_id, :quantity
+  belongs_to :order
+end
+
+

There’s also a relationship between an OrderItem and a Product. A Product is going to be ordered many times, we hope, but an OrderItem is only going to connect with a single Product. This is a “One-to-Many” relationship where one Product is going to have many OrderItems and an OrderItem is going to belong_to a product.

+

Add the second belongs_to relationship in order_item.rb

+

Then, let’s add a validation to make sure that no OrderItem gets created without an order_id and a product_id. Use validates_presence_of

+

The Order Model

+

Next, let’s turn our attention to /app/models/order.rb . We said previously that an OrderItem would belong_to an Order. Now we have to decide, should an Order have only one OrderItem or multiple?

+

We definitely want people ordering as many products as they want, so an Order should have many OrderItems! Add it like this:

+
+class Order < ActiveRecord::Base
+  has_many :order_items
+end
+
+

Products to Order Items

+

We won’t actually need it for our application, but as long as a Product is related to an OrderItem, you should go ahead and add the has_many association in product.rb.

+

Implementing an Order Workflow

+

That was the easy part. Now it’s going to get tricky.

+

Add to Cart Links

+

Let’s open the index view for our products (/app/views/products/index.html.erb). How do we want the shopping process to work? Each product description should have an “Add to Cart” link that adds that item to the customer’s cart. Once they click that the customer should be taken to their order screen where they can set quantities and checkout.

+

First we’ll create the “Add to Cart” link. In the <th> lines near the top of the index add <th>Buy</th>. Then in the TDs, underneath the stock line, let’s add a new cell:

+
+<td><%= link_to "Add to Cart", new_order_item_path(:product_id => product.id) %></td>
+
+

This says "create a link with the text ‘Add to Cart’ which links to the new order_item path and sends in a parameter named product.id with the value of the ID for this product.

+

Reload the index in your browser and you new links should show up.

+

Tracking an Order

+

When you click the “Add to Cart” link you see a plain scaffolding form prompting the user for the product id, order id, and quantity. That’s not acceptable.

+

When the user clicks “Add to Cart”, the system should:

+ +

The first part is tricky because HTTP is a stateless protocol. There isn’t any continuity between requests, so it’s hard to identify a user. But in situations like this one, we need to keep track of users across many page clicks.

+

We’ll take advantage of Rails’ session management here. The session will allow us to track data about a user across multiple requests.

+

Managing Orders in a Before Filter

+

Open your /app/controllers/products_controller.rb file. This controller is what handles the “operations” of a web request. The request starts at the router (or routes.rb file), gets sent to the correct controller, then the controller interacts with the models and views. Think of the controller as the “coach” — it calls all the shots. At the top of the controller file, just below the class line, add this code:

+
+  before_filter :load_order
+
+  def load_order
+    begin
+      @order = Order.find(session[:order_id])
+    rescue ActiveRecord::RecordNotFound
+      @order = Order.create(:status => "unsubmitted")
+      session[:order_id] = @order.id
+    end
+  end
+
+

First, we’re declaring a before_filter. This first line tells Rails “before every request to this controller, run the method named load_order”.

+

We then define the method named load_order. Rails provides us access to the user session through the session hash. This method tries to find the Order with the :order_id in the session and stores it into the variable named @order. If the session hash does not have a key named :order_id or the order has been destroyed, Rails will raise an ActiveRecord::RecordNotFound error. The rescue statement watches for this error and, if it occurs, creates a new Order, stores it into the variable @order, and saves the ID number into session[:order_id].

+

With that code in place refresh your Products index and…nothing looks different. Rails has silently created an Order and saved the ID number into a cookie in your browser. To verify that an order got created, open a second web browser tab and load http://localhost:3000/orders/ where you should see a single unsubmitted order.

+

Improving load_order

+

In general we should avoid using exceptions for control flow. Our implementation of load_order is expecting an exception to be raised under a somewhat normal circumstance: a new user arrives on the site. We should redesign this method to not rely on begin/rescue/end.

+

ActiveRecord provides us with dynamic finders and initializers for all our models. For a Product, we can do any of the following:

+
+  Product.find 2
+  Product.find_by_title "Green Grapes"
+  Product.find_by_image_url "purple_grapes.jpg"
+
+ +

We can call a class method find_by_ then give it the name of any model attribute to search for a match. We can also use find_all_by_ to fetch an array of all matches.

+

How does that help us here? Well, Rails takes it a step further:

+
+  Product.find_or_initialize_by_id(5)
+  Product.find_or_initialize_by_title "Apples"
+
+

The find_or_initialize_by_ method will first attempt to find a match in the database for the specified attribute and value. If it finds one, that object will be returned. If it doesn’t find a match, it will build one in memory and return the new object.

+

This object will not yet have been saved to the database. If we wanted to build it and save it in one step, we could use find_or_create_by. Returning to our load_order method, we can implement the technique like this:

+
+  def load_order
+    @order = Order.find_or_initialize_by_id(session[:order_id], :status => "unsubmitted")
+    if @order.new_record?
+      @order.save!
+      session[:order_id] = @order.id
+    end
+  end
+
+

Note that the :status => "unsubmitted" will only set the status if it is initializing a new object, it will not change the value of records found in the database.

+

From there we look to see if it is a new_record?, which is true when the record has not yet been stored to the DB. If it is new, save it to the DB so it gets an ID, the store that ID into the user’s session.

+

Now Add the Product to the Order

+

Go back to your products list and click the “Add to Cart” link for one of your products. You still see the blank form, right? We think the @order is setup by the before_filter, but we’re not seeing its ID here.

+

Looking at the OrderItemsController, the link is triggering the new action. In Rails’ implementation of the REST pattern, the new action shows the form and the create action processes the form data.

+

In our use case, we can actually just skip the form. When the user clicks the “Add to Cart” link we know their order_id from the session, we know the product_id based on which link they clicked, and we’ll assume that they want to start with a quantity of 1.

+

So let’s get rid of the new action from the controller. While we’re cutting code, we won’t need the show or index actions either, so delete them!

+

We need the products index to trigger the create action. Look at the routes table (by running rake routes from the terminal) and see that we need a POST request to order_items_path. To generate a POST we have to use a form, but our product listing uses links for the “Add to Cart”. Open the index template and change the link_to to a button_to like this:

+
+  <td><%= button_to "Add to Cart", order_items_path(:product_id => product.id) %></td>
+
+

Refresh your browser, click an “Add to Cart” button, and you should see a page complaining about validation errors.

+

Rewriting the Create Action

+

Look at the server log for the last request. You should see “Started POST” then look at the parameters line. See the product_id in there? We have all the information we need to rewrite the create action. The create currently looks like this:

+
+  def create
+    @order_item = OrderItem.new(params[:order_item])
+    if @order_item.save
+      redirect_to @order_item, :notice => "Successfully created order item."
+    else
+      render :action => 'new'
+    end
+  end  
+
+

The first line is trying to create an OrderItem from form parameters, but they’re not there. Instead we can build the OrderItem through the relationship with the order. Wait, do we have an @order setup for this controller?

+

Relocating the load_order Method

+

The load_order before filter exists in the ProductsController but not in the OrderItemsController. We hate to copy/paste code, so how can it be shared between both controllers?

+

If you look at the first line of each controller, they inherit from the ApplicationController class. Cut the load_order definition from ProductsController and move it over to ApplicationController.

+

Now it’s available to both controllers, so in the OrderItemsController you can call before_filter :load_order

+

Building the OrderItem

+

Now that we have access to @order, we can build the order_item through the relationship like this:

+
+  def create
+    @order_item = @order.order_items.new(:quantity => 1, :product_id => params[:product_id])
+    if @order_item.save
+      redirect_to @order, :notice => "Successfully created order item."
+    else
+      render :action => 'new'
+    end
+  end  
+
+

The order_id will be set by the relationship, then we explicitly set the quantity to one and the product_id comes from the request parameters. Then, if the item is successfully saved, redirect to the order.

+

Go back to your products index page, click an “Add to Cart” button.

+

Displaying an Order

+

You should now be redirected to http://localhost:3000/orders/1 which is your current order. You’re looking at the order, you think some products have been added, but we’re not doing anything to display them yet.

+

Add this snippet somewhere on the page:

+
+  <p>
+    Item Count: <%= @order.order_items.count %>
+  </p>
+
+

Reload the page and you should see the Item Count display 1. Go back to your products listing and click “Add to Cart” to add more items. You should see this counter increasing each time.

+

Our shopping experience has a long way to go, but it’s getting there. Iteration 3 is complete!

+

Iteration 4: Improving the Orders Interface

+

We’ll continue working on our /app/views/orders/show.html.erb so open it in your editor and load an order in your web browser.

+

Reworking the Order’s show View

+

Replace the code in the show.html.erb with this:

+
+  <h1>Your Order</h1>
+
+  <table>
+    <tr>
+      <th>Customer</th>
+      <td><%= @order.customer_id %></td>
+    </tr>
+    <tr>
+      <th>Status:</th>
+      <td><%= @order.status %></td>
+    </tr>
+    <tr>
+      <th>Items:</th>
+      <td><%= @order.order_items.count %></td>
+    </tr>
+    <tr>
+      <th>Items</th>
+      <th>Title</th>
+      <th>Quantity</th>
+      <th>Unit Price</th>
+      <th>Subtotal</th>
+    </tr>
+    <!-- more code will go here -->  
+  </table>
+
+

Save that then refresh your order view. The basics are there, but it still isn’t showing what items are in the order.

+

Displaying Individual Items in an Order

+

Remember when we declared that an Order has_many OrderItems? We did that so we could easily access an order’s items. Remove the line that says <!-- more code will go here --> and replace it with this:

+
+  <% @order.order_items.each do |item| %>
+
+  <% end %>
+
+

Inside those lines put whatever HTML and data you want rendered for each item in the @order. Follow the headings that already exist. You’ll need to create a new TR that contains…

+ +

Refresh your view and you should see your individual OrderItems listed out.

+

Displaying Product Images

+

Integrate images of the products into your order display into the first TD of the row. As you did in the products listing, the image can be inserted by using Rails’ image_tag helper method along with the path to the image (which the Product model stores). Try and figure this out on your own, but if necessary go look at your /app/views/products/index.html.erb file.

+

Order Calculations

+

We need to both calculate the subtotals for individual items and the total cost for the entire order.

+

OrderItem Subtotal

+

The subtotal is the easier part because it only involves one object, the OrderItem. We can ask an OrderItem to output its subtotal like this:

+
+  <%= print_price item.subtotal %>
+
+

If you refresh that will crash because it’s expecting item to have a method named subtotal. Open the OrderItem model and define a method named subtotal that returns the quantity times the product’s price.

+

When you think you’ve got it, refresh the view and check out your results. It’s not very convincing since our quantities are all 1 right now, but it’s a start.

+

Calculating the Order Total

+

Now we’re displaying the individual items and their costs, but we don’t have a total for the order. Let’s open the Order model (/app/models/order.rb) and create a method named total. Our OrderItem model already implements a subtotal method that gives us the total for that single item, so what we need to do is add up the subtotal from each of the order_items in this order.

+

I’m going to leave that up to you!

+

With that method written, go back to your order’s show view and add another line at the bottom with a TH that says “Order Total” and a TD that displays the total. Remember to use your print_price helper method to get the right formatting.

+

Manipulating the Order

+

We can add items to the order, but we can’t remove them!

+

Removing a Single Item from the Order

+

Let’s add a “remove” link for each item in the order.

+

Removing a single item from the order is actually pretty easy. To get a hint, let’s look at some scaffolding views that we aren’t actually using. Open up /app/views/order_items/index.html.erb. See how it makes the “Destroy” link? Let’s grab that whole line.

+

Move back to your order’s show template go to the area where you’re displaying the individual order items. Modify your table to make an appropriate space for a “remove” link and paste in the code we got from the other template. You can change the word “Destroy” to “Remove” be less dramatic. Also change order_item to item, since that’s what we called it in that view’s each block.

+

Click one of the delete links and it almost works. The delete action in OrderItemsController is being triggered, but after destroying the object it redirects to the index of OrderItemsController. Instead, make it redirect to the Order, then go back and try deleting another item.

+

Clearing All Items from an Order

+

Deleting a single item was easy, but deleting all of them? We have two options:

+
    +
  1. Create a custom empty action in OrdersController and have it loop through destroying the individual OrderItems
  2. +
  3. Just destroy the Order
  4. +
+

I like easy, so let’s pick #2!

+

On the show view for your Order, add a link like this:

+
+  <%= link_to "Empty Cart", @order, :confirm => 'Are you sure?', :method => :delete %>
+
+

Hop into the destroy action of OrdersController and change the redirect so it sends you back to products_path.

+

Then give it a try.

+

Destroying OrderItem Records

+

If you go to “http://localhost:3000/order_items/” you’ll see all the OrderItem records stored in the database. Go to your console and erase all the existing Order objects:

+
+  Order.destroy_all
+
+

Refresh the OrderItem listing and…they’re all still there? If an Order gets destroyed then we want the OrderItem objects to go too!

+

Go into Order model. Change the has_many :order_items line to this:

+
+  has_many :order_items, :dependent => :destroy
+
+

Now, when an Order is destroyed all the associated order items will get destroyed too. This does not remove the OrderItem objects that are already orphaned in the database. Kill those through the console:

+
+  OrderItem.destroy_all
+
+

Then, through the web interface, add a few items to a new Order. Destroy that Order, and check that the associated items are gone too. When it works, Iteration 4 is complete!

+

Iteration 5 – Dealing with Order Quantities

+

Our order screen is getting powerful, but there are still some features we should add around managing quantities.

+

No Double-Items

+

I’m sure you noticed that new order items are getting added to the cart each time the user click “Add to Cart”. We don’t want two listings for Green Grapes, we want one order_item with a quantity of two. No problem!

+

This is how it should work:

+ +

Look at the create method in your order_items_controller. The first line is always creating a new OrderItem, but we want to look for one with the matching product_id first. We could do this ourselves with and if statement, but let’s take advantage of Rails’ find_or_initialize_by like this:

+
+  @order_item = @order.order_items.find_or_initialize_by_product_id(params[:product_id])
+
+

We can give find_or_initialize_by any attribute name of the object, in this case product_id, and pass in the product_id we’re looking for. If there is a matching OrderItem already attached to this Order with this product_id, it’ll find it and give it back to us. If it doesn’t exist, one will be created.

+

Try using this in your browser. As you add products to your cart you should see that items to not get repeated, but the quantity is not yet going up when we add the same product twice.

+

Incrementing Repeated Items

+

If the finder is creating a new OrderItem, we want to set the quantity to 1. If it finds an existing OrderItem, we want to just increment it by one. We can do both of these in one instruction if the default OrderItem quantity is zero.

+

We set the default value in the database, and to change the database we’ll need a migration. From your command line:

+
+  rails generate migration add_default_quantity_to_order_items
+
+

Then open that migration and in the up method add this line:

+
+  change_column :order_items, :quantity, :integer, :default => 0
+
+

No down is necessary for this little tweak. Run the migration with rake db:migrate. If you’d like to see the results, create a new OrderItem from your console and you’ll see it starts with the quantity 0.

+

Now that the default value is set, your create action in OrderItemsController is easy. If it’s incrementing an existing item, then we add one to the existing value. If it’s a new record, we want to increment the counter from zero to one. Since zero is the existing value of a new record, we can simply add one to the quantity and not care if it’s a new record or an existing one.

+

Add a line to your create action that adds one to the order item’s quantity before the .save is called.

+

Try adding an item to your cart multiple times and you should see the quantities, subtotal, and total moving up!

+

Managing Quantities in the Order

+

We never made a way for customers to easily modify the quantity of each item in the order. Let’s implement that now. There are several ways we could do this from an interface perspective. We’ll implement an easy one.

+

Look at the show template for the order. Find the TD where we currently just print out the quantity for that item. Let’s change it to a link like this:

+
+<td><%= link_to item.quantity, edit_order_item_path(item) %></td>
+
+

That says “create a link with the text of the link displaying the item’s quantity and the link pointing to the edit_order_item action and tell it that we want to edit the item named item”.

+

Save and refresh your browser. Click one of the resulting links under quantity.

+

Cleaning Up the Order Items Edit

+

You are back to some ugly scaffolding code and this form is way too flexible. We don’t want people changing the product ID or the order ID, just the quantity. Open the form partial for order_item (/app/views/order_items/_form.html.erb) and make the following changes:

+ +

Refresh your page and make sure the form is displaying properly. Enter the quantity desired as 5 then click Update.

+

It worked, kind of. It saved the new quantity, but the controller tries to bounce you to the show template for order_item. What we’d really like is to bounce back to the show for the order. Open up /app/controllers/order_items_controller.rb. Scroll down to the update method, and change the redirect_to so it points to the order.

+

Go through and try changing the quantity again and you should return to the order with the updated quantities.

+

Remove Items with 0 Quantity

+

Sometimes instead of clicking “remove” to remove an item from an order, users will set the quantity to zero. Try doing this now with one of your existing orders. What happens?

+

To tell you the truth, I expected an error. We put in a validation that order_item couldn’t have a zero or negative quantity because that wouldn’t make any sense — right? Right? Nope, missed it. Open up your /app/models/order_item.rb and add a validation that ensures quantity is a number, an integer, and greater than zero.

+

Now go back to your order screen, edit an item’s quantity to zero, then click update. You should get an error message sending you back to the edit form saying that quantity must be greater than zero. This is good for our data integrity, but ugly for our users.

+

Now look at /app/controllers/order_items_controller.rb and specifically the update method. We want to short-circuit this process if the incoming params[:order_item][:quantity] is zero.

+

Restructure the logic with a conditional statement like this:

+ +

Test it out and confirm that setting the quantity to zero removes an item from the order.

+

More Intelligent Stock Checking

+

Now that we can make all these changes to the quantity being ordered it makes our current stock checking ineffective. If there’s at least one of an item in stock, our app will say it’s “in stock”. But if the customer is trying to order more than the current stock, we should change that notification.

+

Displaying Stock on the Order Page

+

Let’s start by opening the /app/helpers/products_helper.rb file and finding the print_stock method we created earlier. We want to create logic like this:

+ +

But how will the helper know what quantity the current order is requesting? We’ll have to add a second parameter. So change

+
+def print_stock(stock)
+
+

…to…

+
+  def print_stock(stock,requested)
+
+

Then use the requested variable to implement the logic above.

+

Go back to your your show view template for orders (/app/views/orders/show.html.erb). Try to add in a column to the display that prints out the stock status of each of the items in the order. Here are the steps you need to do:

+ +

Refresh your browser and confirm that it’s working. Try setting your quantities to trigger the “Insufficient Stock” message.

+

Displaying Stock on the Products Index

+

Now go back to your Products listing page. Problem? Your products listing is also trying to use that print_stock helper, but it’s just sending in one parameter. Now we’re getting the error message that the method is expecting two parameters. How to fix it? There are two ways.

+

The ugly way would be to open the products index view and change our call to the helper, adding in a 0 for the number requested. That’d work, but we don’t like ugly. If we end up using the helper anywhere else, we’d have to remember to always put in this hack.

+

Instead we’ll improve the helper, so switch back to that file. Ruby has a great way of implementing optional parameters. We can set it up so calling the helper with one parameter will print whether or not the item is in stock, and sending in two parameters will check if there’s sufficient stock. All we need to do is change…

+
+  def print_stock(stock, requested)
+
+

to…

+
+  def print_stock(stock, requested = 1)
+
+

With that change, if we send in a value for requested the method will use it. If we don’t send in a value for requested, and thus have only one parameter, it’ll just set requested to one. This will allow our product listing to work just like it did before and our order page to have the smarter sufficient-quantity check.

+

Test it out and, when it works, we’re done with iteration 5!

+

Iteration 6: Establishing Identity

+

What’s the point of a web application if only one person can use it? Let’s make our system support multiple users. There are three pieces to making this happen:

+ +

Background on Authentication

+

There have been about a dozen popular methods for authenticating Rails applications over the past five years.

+

The most popular right now is Devise because it makes it very easy to get up and running quickly. The downside is that the implementation uses very aggressive Ruby metaprogramming techniques which make it very challenging to customize.

+

In the past I’ve been a fan of AuthLogic because it takes a very straightforward model/view/controller approach, but it means you have to write a lot of code to get it up and running.

+

As we learn more about constructing web applications there is a greater emphasis on decoupling components. It makes a lot of sense to depend on an external service for our authentication, then that service can serve this application along with many others.

+

Why OmniAuth?

+

The best application of this concept is the OmniAuth. It’s popular because it allows you to use multiple third-party services to authenticate, but it is really a pattern for component-based authentication.

+

You could let your users login with their Twitter account, but you could also build your own OmniAuth provider that authenticates all your company’s apps. Maybe you can use the existing LDAP provider to hook into ActiveDirectory or OpenLDAP, or make use of the Google Apps interface?

+

Better yet, OmniAuth can handle multiple concurrent strategies, so you can offer users multiple ways to authenticate. Your app is just built against the OmniAuth interface, those external components can be changed later.

+

Getting Started with OmniAuth

+

The first step is to add the dependency to your Gemfile:

+
+  gem "omniauth"
+
+

Then run bundle from your terminal.

+

OmniAuth runs as a “Rack Middleware” which means it’s not really a part of our app, it’s a thin layer between our app and the client. To instantiate and control the middleware, we need an initializer. Create a file /config/initializers/omniauth.rb and add the following:

+
+  Rails.application.config.middleware.use OmniAuth::Builder do
+    provider :twitter, "i0KU4jLYYjWzxpTraWviw", "djMu7QStnBK89MkfIg78tIZ4sFhmGOYjdCjDD0Wsc"
+  end
+
+

What is all that garbage? Twitter, like many API-providing services, wants to track who’s using it. They accomplish this by distributing API accounts. Specifically, they use the OAuth protocol which requires a “comsumer key” and a “consumer secret.” If you want to build an application using the Twitter API you’ll need to register and get your own credentials. For this tutorial, I’ve registered a sample application and given you my key/secret above.

+

Trying It Out

+

You need to restart your server so the new library and initializer are picked up. In your browser go to http://127.0.0.1:3000/auth/twitter and, after a few seconds, you should see a Twitter login page. Login to Twitter using any account, then you should see a Routing Error from your application. If you’ve got that, then things are on the right track.

+

If you get to this point and encounter a 401 Unauthorized message there is more work to do. You’re probably using your own API key and secret. You need to go into the settings on Twitter for your application, and add http://127.0.0.1 as a registered callback domain. I also add http://0.0.0.0 and http://localhost while I’m in there. Now give it a try and you should get the Routing Error

+

Handling the Callback

+

The way this authentication works is that your app redirects to the third party authenticator, the third party processes the authentication, then it sends the user back to your application at a “callback URL”. Twitter is attempting to send the data back to your application, but your app isn’t listening at the default OmniAuth callback address, /auth/twitter/callback. Let’s add a route to listen for those requests.

+

Open /app/config/routes.rb and add this line:

+
+  match '/auth/:provider/callback', :to => 'sessions#create'
+
+

Re-visit http://localhost:3000/auth/twitter, it will process your already-existing Twitter login, then redirect back to your application and give you Uninitialized Constant SessionsController. Our router is attempting to call the create action of the SessionsController, but that controller doesn’t exist yet.

+

Creating a Sessions Controller

+

Let’s use a generator to create the controller from the command line:

+
+  rails generate controller sessions
+
+

Then open up that controller and add code so it looks like this:

+
+  class SessionsController < ApplicationController
+    def create
+      render :text => request.env["omniauth.auth"]
+    end
+  end
+
+

Revisit /auth/twitter and, once it redirects to your application, you should see a bunch of information provided by Twitter about the authenticated user! Now we just need to figure out what to do with all that.

+

Creating a User Model

+

Even though we’re using an external service for authentication, we’ll still need to keep track of user objects within our system. Let’s create a model that will be responsible for that data.

+

As you saw, Twitter gives us a ton of data about the user. What should we store in our database? The minimum expectations for an OmniAuth provider are three things:

+ +

Let’s start with just those three in our model. From your terminal:

+
+  rails generate model User provider:string uid:string name:string
+
+

Then update the database with rake db:migrate.

+

Creating Actual Users

+

How you create users might vary depending on the application. For the purposes of our shopping cart, we’ll allow anyone to create an account automatically just by logging in with the third party service.

+

Finding or Creating User Objects

+

Hop back to the SessionsController. I believe strongly that the controller should have as little code as possible, so we’ll proxy the User lookup/creation from the controller down to the model like this:

+
+  def create
+    @user = User.find_or_create_by_auth(request.env["omniauth.auth"])
+  end
+
+ +

Now the User model is responsible for figuring out what to do with that big hash of data from Twitter. We encapsulate the complexity at the model layer where it’s easy to test an reuse. Open the User model and add this method:

+
+  def self.find_or_create_by_auth(auth_data)
+    find_or_create_by_provider_and_uid_and_name(auth_data["provider"], auth_data["uid"], auth_data["user_info"]["name"])   
+  end
+
+

Rails supports dynamic finders. The one we’ve used here is composed of:

+ +

This will work, but it’s fragile. The UID and provider aren’t going to change, so those are fine. The name might change, though. On Twitter, for instance, the human-readable name is configurable. If they change their name on the external service this lookup is going to fail, creating a second account for them on our service.

+

Instead, do the lookup with just the fields that won’t change:

+
+  def self.find_or_create_by_auth(auth_data)
+    find_or_create_by_provider_and_uid(auth_data["provider"], auth_data["uid"])   
+  end
+
+

This is fault-tolerant, but it has an issue. When a user visits our site for the first time the user object will be created. But we’re now not utilizing the name at all — so new records won’t have the human name saved.

+

Thankfully find_or_create_by has a solution. We can add a hash of parameters that will only be used when creating records, not as part of the lookup:

+
+  def self.find_or_create_by_auth(auth_data)
+    find_or_create_by_provider_and_uid(auth_data["provider"], auth_data["uid"],
+                                       :name => auth_data["user_info"]["name"])
+  end
+
+

That will work great!

+

Create Action Redirection

+

Now, back to SessionsController, let’s add a redirect action to send them to the companies_path after login:

+
+  def create
+    @user = User.find_or_create_by_auth(request.env["omniauth.auth"])
+    session[:user_id] = @user.id
+    redirect_to products_path, :notice => "Logged in as #{@user.name}"
+  end
+
+

Now visit /auth/twitter and you should eventually be redirected to your Products listing and the flash message at the top will show a message saying that you’re logged in.

+

UI for Login/Logout

+

That’s exciting, but now we need links for login/logout that don’t require manually manipulating URLs. Anything like login/logout that you want visible on every page goes in the layout.

+

Open /app/views/layouts/application.html.erb and you’ll see the framing for all our view templates. Let’s add in the following just below the flash messages:

+
+  <div id="account">
+    <% if current_user %>
+      <span>Welcome, <%= current_user.name %></span>
+      <%= link_to "logout", logout_path, :id => "login" %>
+    <% else %>
+      <%= link_to "login", login_path, :id => "logout" %>
+    <% end %>
+  </div>
+
+

If you refresh your browser that will all crash for several reasons.

+

Accessing the Current User

+

It’s a convention that Rails authentication systems provide a current_user method to access the user. Let’s create that in our ApplicationController with these steps:

+ +
+  private
+    def current_user
+      @current_user ||= User.find(session[:user_id]) if session[:user_id]
+    end  
+
+

Even though the current_user method is private in ApplicationController, it will be available to all our controllers because they inherit from ApplicationController. In addition, the helper_method line makes the method available to all our views. Now we can access current_user from any controller and any view!

+

Refresh your page and you’ll move on to the next error, undefined local variable or method `login_path'.

+

Convenience Routes

+

Just because we’re following the REST convention doesn’t mean we can’t also create our own named routes. The view snippet we wrote is attempting to link to login_path and logout_path, but our application doesn’t yet know about those routes.

+

Open /config/routes.rb and add two custom routes:

+
+  match "/login" => redirect("/auth/twitter"), :as => :login
+  match "/logout" => "sessions#destroy", :as => :logout  
+
+

The first line creates a path named login which just redirects to the static address /auth/twitter which will be intercepted by the OmniAuth middleware. The second line creates a logout path which will call the destroy action of our SessionsController.

+

With those in place, refresh your browser and it should load without error.

+

Implementing Logout

+

Our login works great, but we can’t logout! When you click the logout link it’s attempting to call the destroy action of SessionsController. Let’s implement that.

+ +

Now try logging out and you’ll probably end up looking at the Rails “Welcome Aboard” page. Why isn’t your root_path taking affect?

+

If you have a file in /public that matches the requested URL, that will get served without ever triggering your router or application. Since Rails generated a /public/index.html file, that’s getting served instead of our root_path route. Delete the index.html file from public, and refresh your browser.

+

NOTE: At this point I observed some strange errors from Twitter. Stopping and restarting my server, which clears the cached data, got it going again.

+

Connecting Users to Orders

+

We’ve done the hard work of creating User objects and logging them into the system. Now we need to tie orders to users.

+

Connecting the Order to a User

+

Open the Order model and add a new belongs_to line:

+
+  belongs_to :user, :foreign_key => :customer_id
+
+

Note the extra foreign_key parameter. In the database that’s the name of the column, though our associated model is named User. This extra piece of information tells Rails how to connect them. That means we also need to modify the attr_accessible line like this:

+
+  attr_accessible :user, :status
+
+

Connecting the User to Orders

+

Then in the User model, just add has_many :orders

+

User/Order Workflow

+

Now we need to figure out when in the lifecycle we can connect a User and an Order

+
    +
  1. When an order is created, connect them to the current user if one is logged in
  2. +
  3. When they login, connect them to the current order
  4. +
+

Displaying the User on the Order

+

Let’s output the associated User on the order’s show page. There’s already a TH for “Customer”, just add this into the TD:

+
+  <td><%= @order.user.name if @order.user %></td>
+
+

When an Order is Created

+

Our orders are created in the load_order method in ApplicationController. Let’s modify it to build off the current user if one is logged in:

+
+  def load_order
+    begin
+      @order = Order.find(session[:order_id])
+    rescue ActiveRecord::RecordNotFound
+      if current_user
+        @order = current_user.orders.create(:status => "unsubmitted")
+      else
+        @order = Order.create(:status => "unsubmitted")
+      end
+      session[:order_id] = @order.id
+    end
+  end
+
+

When an Order Exists before They Login

+

Open up the SessionsController. The create action is what logs them in. Once the User is known we can look for an existing order and, if there is one, connect it to the User:

+
+  def create
+    @user = User.find_or_create_by_auth(request.env["omniauth.auth"])
+    session[:user_id] = @user.id
+    load_order
+    @order.update_attributes(:user => @user)
+    redirect_to products_path, :notice => "Logged in as #{@user.name}"
+  end
+
+

Clear the Order on Logout

+

As long as we’re in the SessionsController, let’s clear the order from the session when they logout like this:

+
+  def destroy
+    session[:user_id] = nil
+    session[:order_id] = nil
+    redirect_to root_path
+  end  
+
+

Test It!

+

Now you should be all set. Try creating orders when you’re not logged in, then login and the order is preserved. Login first, then create an order and it’s connected to your account.

+

Bonus Points

+

It would be awesome if, when a user logs in, it would retrieve their last unsubmitted order. Here’s one way to pull it off.

+

In the load_order method, change the lookup to find_or_create_by like this:

+
+  @order = current_user.orders.find_or_create_by_status("unsubmitted")
+
+

This will work if they are logged out then login before creating an order. If they’ve already created an order, then login, though, it won’t find the old one. What would you have the system do in this case? Maybe merge the old saved order and the new one? Give it a shot!

+

Iteration 7 – Checkout

+

We’ve got a decent shopping experience going — except you can’t actually place the order.

+

Let’s build the ability to add a shipping address and “submit” the order. We won’t actually integrate with a payment gateway, but can point out along the way where that would happen.

+

Building Addresses

+

A user might ship different orders to different places, so the address needs to be associated with the order. But, at the same time, our frequent customers don’t want to enter in the same address every time.

+

Designing the Data Relationships

+

The solution? We’ll build an address model that is tied to a user. When the user submits the order, they’ll pick one of their addresses or add a new one. The order will then be connected to that address.

+

From a model/database perspective:

+ +

The belongs_to side of a relationship holds the foreign key. So we see that Address needs a foreign key user_id and the Order needs an address_id.

+

The Address Model

+

We know that it’ll need a user_id, but what else goes into a US address?

+ +

All those fields can be stored as strings. Let’s use the nifty_scaffold generator, even though we won’t use all the parts:

+
+rails generate nifty:scaffold Address line1:string line2:string city:string state:string zip:string user_id:integer
+rake db:migrate
+
+

Validating Addresses

+

Everything except line2 should be required in an address. Let’s assume that the zipcode should be exactly 5 characters that are only digits. The state must be a two-letter uppercase abbreviation.

+

Add validations that protect each of these requirements. Look here for tips: http://api.rubyonrails.org/classes/ActiveRecord/Validations/ClassMethods.html

+

Modifying the Orders Table

+

We also need to add that address_id foreign key to the orders table. That means creating a migration.

+

When you generate a migration to add a column to an existing database you can do everything from the command line if you follow the convention. It goes like this:

+
+  rails generate migration add_[name]_to_[table] [column_name]:[column_type]
+
+

So in this case:

+
+  rails generate migration add_address_id_to_orders address_id:integer
+  rake db:migrate
+
+

Our database is setup, but there is a lot more to do!

+

Setting Relationships

+

Open the Order, User, and Address models. Add the relationships we described above.

+

Checkout UI

+

When a user is on the order page they should be able to select an existing address from a drop-down list of their associated addresses or click a link to add a new address.

+

Building Addresses in the Console

+

Let’s create two sample addresses for your current user in the console. Here’s how you might do that:

+
+  a = User.last.addresses.build(:line1 => "123 First Street", :line2 => "Suite B", :city => "Washington", :state => "DC", :zip => "20011")
+  a.save
+  b = User.last.addresses.build(:line1 => "321 Second Street", :city => "Arlington", :state => "VA", :zip => "22182")
+  b.save
+
+

Wrapping the Order in a Form

+

Open app/views/orders/show.html.erb and add another row to the end of the table like this:

+
+  <tr>
+    <th>Shipping To:</th>
+    <td>
+      (addresses select box)
+      (add an address link)
+    </td>
+  </tr>
+
+

To have a select box and, eventually, a “submit” button we’ll need a form. The best approach is probably to enclose the entire table in the form tags. Up above the table opening tag, add a form_for like this:

+
+  <%= form_for @order do |f| %>
+
+

Then put a matching end at the bottom of the template.

+

Displaying Addresses on the Order

+

Now to figure out that select. Creating select boxes in Rails has always seemed unnecessarily difficult.

+

We’ll use the form helper select. It expects as parameters the name of the attribute being set, here address_id then an array of options. Each of those options should be a two element array where the first element is the name to show on the form and the second is the value to submit. In this case, we want:

+
+<%= f.select :address_id, current_user.addresses.collect{|a| [a.to_s, a.id]} %>
+
+

The options here are built by grabbing all the current user’s addresses and creating an array made up of arrays containing the address converted to a string (to_s) and the id.

+

Load it in your browser and you should see something like #<Address:0x0000028394482 in the select box. This is actually good!

+

Implementing the to_s

+

There are a few ways we could format the address for presentation in the select box, but the easiest is to define to_s in the model. Whenever you see that format like #<ClassName:0x00002838203 you’re looking at a model that doesn’t implement to_s, but to_s is being called on it anyway. This implementation of to_s comes from Ruby’s Object, the parent of all objects.

+

Let’s implement a better to_s in the Address model. Define the method, build an array of the attributes you want to display, then join them together with a command and a space.

+

If you have sample addresses without a “Line 2”, you’ll see an extra comma in the output. You can remove nil or empty string entries with this method call: .reject{|x| x.blank?}.

+

Adding Addresses

+

Now let’s build out that “add an address” link. You can handle this on your own, here are the steps:

+ +

Submitting the Order

+

We can pick an address, but we can’t submit the order. Add another row to the table that includes a submit button like this:

+
+  <%= f.submit "Submit Order" %>
+
+

View it in your browser, click the button, and look at the log file from your server.

+

You’ll see that it got a POST request to "/orders/1". If you look at the routing table, you’ll see that that isn’t a valid combination of verb and address pattern. It happens to trigger the update action because of details about how PUT/POST are recognized in the router.

+

This is a good place to use a custom route and action. Let’s build that now.

+

Adding a Custom Route

+

Open your routes.rb file and change the resources :orders to this:

+
+  resources :orders do
+    member do
+      put :purchase
+    end
+  end
+
+

That tells the router that orders will have a custom “member” action — an action that happens to a single order. The action will use a PUT verb and the name “purchase.” If you run rake routes in your terminal, you’ll see the new listing like this:

+
+  purchase_order PUT   /orders/:id/purchase(.:format)     {:action=>"purchase", :controller=>"orders"}  
+
+

We chose the PUT verb here because the operation we’re performing is similar to an edit/update. Since update uses PUT, our purchase will too.

+

Using the Route

+

In the order’s show.html.erb we need to modify the form_for line. To control the submission address and verb, we add the :url and :method parameters like this:

+
+  <%= form_for @order, :url => purchase_order_path(@order), :method => :put do |f| %>
+
+

Now refresh your order in the browser and click the submit button.

+

Defining the Action

+

You’ll then get an error that the purchase action is not defined. Let’s start with this sketch in OrdersController:

+
+  def purchase
+    # Find the order
+    # Set the address_id
+    # Change the status to "submitted"
+    # Save it
+    # Remove the order_id from the session so they can't edit it
+    # Display a thank you page with an order summary
+  end
+
+

To look at the incoming parameters, try using raise "bang!" and scroll down the error screen. Given those params and the outline above, go ahead and implement it!

+

One stumbling block you might run into is the address_id not being stored. Look in your log for a line like this:

+
+  WARNING: Can't mass-assign protected attributes: address_id
+
+

How do you control which attributes can and can’t be mass-assigned?

+

It Should Work!

+

You should now be able to submit and order. Iteration 7 is complete!

+

Next Steps

+

Here are some extension ideas:

+ +
+
+ + + + + + + + + + + + diff --git a/public/projects/jsmerchant_notes.textile b/public/projects/jsmerchant_notes.textile new file mode 100644 index 000000000..50568657e --- /dev/null +++ b/public/projects/jsmerchant_notes.textile @@ -0,0 +1,2 @@ +To-Do List: + - Write authorization iteration \ No newline at end of file diff --git a/public/sitemap.xml b/public/sitemap.xml index f7d158b67..02b119b14 100644 --- a/public/sitemap.xml +++ b/public/sitemap.xml @@ -8,6 +8,18 @@ http://yoursite.com/projects/jsattend.html 2011-10-02T08:56:06-04:00 + + http://yoursite.com/projects/jsblogger.html + 2011-09-28T15:19:30-04:00 + + + http://yoursite.com/projects/jscontact.html + 2011-09-28T15:19:30-04:00 + + + http://yoursite.com/projects/jsmerchant.html + 2011-09-28T15:19:30-04:00 + http://yoursite.com/projects/jstwitter.html 2012-01-07T09:55:45-05:00 @@ -104,6 +116,10 @@ http://yoursite.com/topics/debugging/outputting_text.html 2011-09-25T04:11:36-04:00 + + http://yoursite.com/topics/decorators.html + 2011-09-28T15:19:30-04:00 + http://yoursite.com/topics/environment/bundler.html 2011-12-12T14:26:17-05:00 diff --git a/public/topics/decorators.html b/public/topics/decorators.html new file mode 100644 index 000000000..8a1e6d362 --- /dev/null +++ b/public/topics/decorators.html @@ -0,0 +1,176 @@ + + + + + + + + Experimenting with Decorators - Jumpstart Lab Curriculum + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Jumpstart Lab Curriculum

+ +
+ +
+ +
+
+

Let’s play around with the concept of decorators and check out some of the features offered by the Draper gem.

+

Setup

+

First we need a sample project. I’ve setup a version of our JSMerchant project, a simple online store, that we can experiment with. To clone the sample project:

+
+  git clone git://github.com/JumpstartLab/jsmerchant_online.git
+
+

Then change into the project directory.

+

Install Draper

+

Next, open the Gemfile and add a dependency on 'draper' like this:

+
+  gem 'draper'
+
+

Run bundle, then start up your server.

+

Generate a Decorator

+

We’ll create a decorator to wrap the Product model. Draper gives you a handy generator:

+
+  rails generate draper:model Product
+
+

It will create the folder app/decorators/ and the file app/decorators/product_decorator.rb. Open the file and you’ll find the frame of a ProductDecorator class.

+

Restart your server so the new folder is added to the load path.

+

First Usage

+

Without changing the decorator, let’s see the simplest usage. Open the products_controller and look at the show action. It currently has:

+
+  def show
+    @product = Product.find(params[:id])
+  end  
+
+

To make use of the decorator, call the .new method and pass in the product from the database:

+
+  def show
+    source = Product.find(params[:id])
+    @product = ProductDecorator.new(source)
+  end
+
+

Then go and view the show page for a single product by clicking on its name on the index.

+

Polymorphic Path Bug — FIXED??

+

Update: I think I’ve got this nailed, but leaving the note here for now, just in case.

+

You will likely hit a bug here: undefined method 'name' for nil. The show template is relying on Rails’ polymorphic path which jumps through some complex hoops to figure out the proper path name. It’s fixable, but the solution isn’t quite right yet.

+

The fix is to just use a normal path. Open the show.html.erb template and change the destroy link like this:

+
+  <%= link_to "Destroy", product_path(@product), :confirm => 'Are you sure?', :method => :delete %>
+
+

Now your decorator is in action — doing nothing of interest.

+

Adding Methods

+

Now let’s add some actual functionality to our decorator.

+

Product Price

+

Currently the show page just displays the raw price attribute. There is a helper named print_price in ApplicationHelper that wraps the price in a currency format.

+

The challenge is that we have to remember to use that helper whenever we output the price. What a pain! Instead, let’s override the price method in our decorator:

+
+  def price
+    helpers.number_to_currency(source.price)
+  end
+
+

This uses two methods inherited from the Draper::Base. First we have a method helpers that is a proxy to ActionView’s built-in helpers. That way we don’t have to include all of the helpers directly in our decorator.

+

The second is source which stores the original wrapped object. We can use the accessor to reach methods not available in the decorator, such as ones we override or explicitly deny.

+

Product Stock

+

Currently the show page uses the print_stock helper:

+
+  <%= print_stock @product.stock %>
+
+

Find that method in ProductsHelper and try rewriting it into your decorator. The content_tag helper is available as a normal method in the decorator class.

+

Using Denies

+

When we define an interface we want to be able to exclude or include specific accessors. Let’s try using the denies method.

+

Denies is modeled after attr_protected in Rails. If you don’t use the method, everything is permitted. If you do use the method, then all calls are permitted except to those listed in the denies call.

+

For instance, say we wanted to block usage of updated_at. First, add it as a display item in the show view:

+
+  <p>
+    Last Updated: <%= @product.updated_at %>
+  </p>
+
+

Refresh your browser and the timestamp should show up. Now let’s try denying access to that method. Inside your ProductDecorator class, add this:

+
+  class ProductDecorator < Draper::Base
+    denies :updated_at
+    #...
+
+

Refresh your page and it should raise an exception, the method updated_at is not defined.

+

Using Allows

+

When writing models, attr_protected is not frequently used. More often we want to define a whitelist using attr_accessible. The same is true with decorator models. It’s more common to specify which methods are made available.

+

Combining Allows and Denies

+

First, try adding this line right under the denies:

+
+  allows :title
+
+

Refresh your page and you should see an exception. You can’t use both allows and denies in the same decorator because it leaves ambiguity about the unlisted methods. Remove the denies line and try again.

+

Allowing More Methods

+

So far you’re only allowing :title, so you’ll get exceptions as the other accessors try to pull out data. Add the appropriate methods to allows, separated by commas. Make sure you include to_param so your links will work properly.

+

Note that you don’t need to allow methods defined in the decorator, they’re allowed by default.

+

Now, Play!

+

Here are some other things you can try:

+
    +
  • Define a to_xml or to_json in the decorator and use respond_with to serve them up
  • +
  • Try calling ProductDecorator.decorate(sources) to create an array of decorated objects from an array of source objects, then use experiment with the index
  • +
  • Try defining a format attribute on your decorator (so it’d hold a value like :xml or :json), set it when creating the decorator instance, then in your methods react to that attribute to output formatted data. (ASIDE: Is this a good idea? It’d probably be better to define a parent decorator like ProductDecorator, then create subclasses ProductDecoratorXML and ProductDecoratorJSON.)
  • +
+

Where We Go from Here

+

The decorator pattern is ready to start replacing your helpers and defining an interface between view template and data. What’s next?

+

The next challenge is to provide access to traditional user-defined helpers from within the decorator. For instance, to do proper data shaping based on authorization, we would want to call current_user from within the decorator. ActiveView provides a way to proxy the built in helpers, but there isn’t one for user defined helpers. There’s a tricky hack to do it that we’ll likely implement soon.

+

Respected friend Xavier Shay posts his solution here: https://gist.github.com/1077274

+

From there, it’s time for some real-world usage. I’d love your help testing this out with experimental code. Please don’t use it in a production system until it hits 1.0!

+
+
+ + + + + + + + + + + + diff --git a/public/topics/handlebars.markdown b/public/topics/handlebars.markdown new file mode 100644 index 000000000..3381de865 --- /dev/null +++ b/public/topics/handlebars.markdown @@ -0,0 +1,4 @@ +1. Demo +2. Setup Handlebars +3. Template Processing with Static Data +4. Fetching Dynamic Data \ No newline at end of file