diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..53411e3 Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore index c917d7d..6597b62 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ /tmp coverage + +# Ignore application configuration +/config/application.yml diff --git a/Gemfile b/Gemfile index 01ad264..e66d9a5 100644 --- a/Gemfile +++ b/Gemfile @@ -12,11 +12,29 @@ gem 'jquery-rails' gem 'jbuilder', '~> 2.0' gem 'sdoc', '~> 0.4.0', group: :doc +gem 'pg' +gem 'pg_search' + +gem "paperclip", "~> 4.2" +gem 'aws-sdk' + + gem 'devise' +gem 'devise-async' gem 'cancancan' +gem 'figaro' +gem 'angular-rails' + +gem 'stripe' + +gem 'sidekiq' +gem 'sinatra' + group :development do + gem 'annotate' gem 'better_errors' + gem 'letter_opener' gem 'binding_of_caller' gem 'pry-rails' gem 'quiet_assets' diff --git a/Gemfile.lock b/Gemfile.lock index 12924d3..f716b9e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,65 +1,86 @@ GEM remote: https://rubygems.org/ specs: - actionmailer (4.1.4) - actionpack (= 4.1.4) - actionview (= 4.1.4) - mail (~> 2.5.4) - actionpack (4.1.4) - actionview (= 4.1.4) - activesupport (= 4.1.4) + actionmailer (4.1.6) + actionpack (= 4.1.6) + actionview (= 4.1.6) + mail (~> 2.5, >= 2.5.4) + actionpack (4.1.6) + actionview (= 4.1.6) + activesupport (= 4.1.6) rack (~> 1.5.2) rack-test (~> 0.6.2) - actionview (4.1.4) - activesupport (= 4.1.4) + actionview (4.1.6) + activesupport (= 4.1.6) builder (~> 3.1) erubis (~> 2.7.0) - activemodel (4.1.4) - activesupport (= 4.1.4) + activemodel (4.1.6) + activesupport (= 4.1.6) builder (~> 3.1) - activerecord (4.1.4) - activemodel (= 4.1.4) - activesupport (= 4.1.4) + activerecord (4.1.6) + activemodel (= 4.1.6) + activesupport (= 4.1.6) arel (~> 5.0.0) - activesupport (4.1.4) + activesupport (4.1.6) i18n (~> 0.6, >= 0.6.9) json (~> 1.7, >= 1.7.7) minitest (~> 5.1) thread_safe (~> 0.1) tzinfo (~> 1.1) addressable (2.3.6) + angular-rails (0.0.12) + coffee-script (~> 2.2.0) + rails + annotate (2.6.5) + activerecord (>= 2.3.0) + rake (>= 0.8.7) arel (5.0.1.20140414130214) + aws-sdk (1.53.0) + aws-sdk-v1 (= 1.53.0) + aws-sdk-v1 (1.53.0) + json (~> 1.4) + nokogiri (>= 1.4.4) bcrypt (3.1.7) - better_errors (1.1.0) + better_errors (2.0.0) coderay (>= 1.0.0) erubis (>= 2.6.6) + rack (>= 0.9.0) binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) builder (3.2.2) cancancan (1.9.2) - capybara (2.4.1) + capybara (2.4.3) mime-types (>= 1.16) nokogiri (>= 1.3.3) rack (>= 1.0.0) rack-test (>= 0.5.4) xpath (~> 2.0) + celluloid (0.15.2) + timers (~> 1.1.0) + climate_control (0.0.3) + activesupport (>= 3.0) + cocaine (0.5.4) + climate_control (>= 0.0.3, < 1.0) coderay (1.1.0) coffee-rails (4.0.1) coffee-script (>= 2.2.0) railties (>= 4.0.0, < 5.0) - coffee-script (2.3.0) + coffee-script (2.2.0) coffee-script-source execjs - coffee-script-source (1.7.1) + coffee-script-source (1.8.0) + connection_pool (2.0.0) debug_inspector (0.0.2) - devise (3.2.4) + devise (3.3.0) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 3.2.6, < 5) thread_safe (~> 0.1) warden (~> 1.2.3) + devise-async (0.9.0) + devise (~> 3.2) diff-lcs (1.2.5) - docile (1.1.3) + docile (1.1.5) erubis (2.7.0) execjs (2.2.1) factory_girl (4.4.0) @@ -67,8 +88,10 @@ GEM factory_girl_rails (4.4.1) factory_girl (~> 4.4.0) railties (>= 3.0.0) - faker (1.3.0) + faker (1.4.3) i18n (~> 0.5) + figaro (1.0.0) + thor (~> 0.14) haml (4.0.5) tilt haml-rails (0.5.3) @@ -81,25 +104,36 @@ GEM jbuilder (2.1.3) activesupport (>= 3.0.0, < 5) multi_json (~> 1.2) - jquery-rails (3.1.1) + jquery-rails (3.1.2) railties (>= 3.0, < 5.0) thor (>= 0.14, < 2.0) json (1.8.1) launchy (2.4.2) addressable (~> 2.3) - mail (2.5.4) - mime-types (~> 1.16) - treetop (~> 1.4.8) + letter_opener (1.2.0) + launchy (~> 2.2) + mail (2.6.1) + mime-types (>= 1.16, < 3) method_source (0.8.2) - mime-types (1.25.1) + mime-types (2.3) mini_portile (0.6.0) - minitest (5.4.0) + minitest (5.4.1) multi_json (1.10.1) + netrc (0.7.7) nokogiri (1.6.3.1) mini_portile (= 0.6.0) orm_adapter (0.5.0) - polyglot (0.3.5) - pry (0.10.0) + paperclip (4.2.0) + activemodel (>= 3.0.0) + activesupport (>= 3.0.0) + cocaine (~> 0.5.3) + mime-types + pg (0.17.1) + pg_search (0.7.8) + activerecord (>= 3.1) + activesupport (>= 3.1) + arel + pry (0.10.1) coderay (~> 1.1.0) method_source (~> 0.8.1) slop (~> 3.4) @@ -108,42 +142,50 @@ GEM quiet_assets (1.0.3) railties (>= 3.1, < 5.0) rack (1.5.2) + rack-protection (1.5.3) + rack rack-test (0.6.2) rack (>= 1.0) - rails (4.1.4) - actionmailer (= 4.1.4) - actionpack (= 4.1.4) - actionview (= 4.1.4) - activemodel (= 4.1.4) - activerecord (= 4.1.4) - activesupport (= 4.1.4) + rails (4.1.6) + actionmailer (= 4.1.6) + actionpack (= 4.1.6) + actionview (= 4.1.6) + activemodel (= 4.1.6) + activerecord (= 4.1.6) + activesupport (= 4.1.6) bundler (>= 1.3.0, < 2.0) - railties (= 4.1.4) + railties (= 4.1.6) sprockets-rails (~> 2.0) - railties (4.1.4) - actionpack (= 4.1.4) - activesupport (= 4.1.4) + railties (4.1.6) + actionpack (= 4.1.6) + activesupport (= 4.1.6) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) rake (10.3.2) - rdoc (4.1.1) + rdoc (4.1.2) json (~> 1.4) - rspec-core (3.0.2) - rspec-support (~> 3.0.0) - rspec-expectations (3.0.2) + redis (3.1.0) + redis-namespace (1.5.1) + redis (~> 3.0, >= 3.0.4) + rest-client (1.7.2) + mime-types (>= 1.16, < 3.0) + netrc (~> 0.7) + rspec-core (3.1.4) + rspec-support (~> 3.1.0) + rspec-expectations (3.1.1) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.0.0) - rspec-mocks (3.0.2) - rspec-support (~> 3.0.0) - rspec-rails (3.0.1) + rspec-support (~> 3.1.0) + rspec-mocks (3.1.1) + rspec-support (~> 3.1.0) + rspec-rails (3.1.0) actionpack (>= 3.0) activesupport (>= 3.0) railties (>= 3.0) - rspec-core (~> 3.0.0) - rspec-expectations (~> 3.0.0) - rspec-mocks (~> 3.0.0) - rspec-support (~> 3.0.0) - rspec-support (3.0.2) + rspec-core (~> 3.1.0) + rspec-expectations (~> 3.1.0) + rspec-mocks (~> 3.1.0) + rspec-support (~> 3.1.0) + rspec-support (3.1.0) sass (3.2.19) sass-rails (4.0.3) railties (>= 4.0.0, < 5.0) @@ -153,11 +195,21 @@ GEM sdoc (0.4.1) json (~> 1.7, >= 1.7.7) rdoc (~> 4.0) - simplecov (0.8.2) + sidekiq (3.2.5) + celluloid (= 0.15.2) + connection_pool (>= 2.0.0) + json + redis (>= 3.0.6) + redis-namespace (>= 1.3.1) + simplecov (0.9.1) docile (~> 1.1.0) - multi_json + multi_json (~> 1.0) simplecov-html (~> 0.8.0) simplecov-html (0.8.0) + sinatra (1.4.5) + rack (~> 1.4) + rack-protection (~> 1.4) + tilt (~> 1.3, >= 1.3.4) slop (3.6.0) spring (1.1.3) spring-commands-rspec (1.0.2) @@ -167,17 +219,19 @@ GEM multi_json (~> 1.0) rack (~> 1.0) tilt (~> 1.1, != 1.3.0) - sprockets-rails (2.1.3) + sprockets-rails (2.1.4) actionpack (>= 3.0) activesupport (>= 3.0) sprockets (~> 2.8) sqlite3 (1.3.9) + stripe (1.15.0) + json (~> 1.8.1) + mime-types (>= 1.25, < 3.0) + rest-client (~> 1.4) thor (0.19.1) thread_safe (0.3.4) tilt (1.4.1) - treetop (1.4.15) - polyglot - polyglot (>= 0.3.1) + timers (1.1.0) tzinfo (1.2.2) thread_safe (~> 0.1) uglifier (2.5.3) @@ -192,26 +246,38 @@ PLATFORMS ruby DEPENDENCIES + angular-rails + annotate + aws-sdk better_errors binding_of_caller cancancan capybara coffee-rails (~> 4.0.0) devise + devise-async factory_girl_rails faker + figaro haml-rails jbuilder (~> 2.0) jquery-rails launchy + letter_opener + paperclip (~> 4.2) + pg + pg_search pry-rails quiet_assets rails rspec-rails sass-rails (~> 4.0.3) sdoc (~> 0.4.0) + sidekiq simplecov + sinatra spring spring-commands-rspec sqlite3 + stripe uglifier (>= 1.3.0) diff --git a/app/.DS_Store b/app/.DS_Store new file mode 100644 index 0000000..6ec7837 Binary files /dev/null and b/app/.DS_Store differ diff --git a/app/assets/.DS_Store b/app/assets/.DS_Store new file mode 100644 index 0000000..3ba1f3b Binary files /dev/null and b/app/assets/.DS_Store differ diff --git a/app/assets/images/.DS_Store b/app/assets/images/.DS_Store new file mode 100644 index 0000000..053b0c6 Binary files /dev/null and b/app/assets/images/.DS_Store differ diff --git a/app/assets/images/nicEditorIcons.gif b/app/assets/images/nicEditorIcons.gif new file mode 100644 index 0000000..5cf1ebe Binary files /dev/null and b/app/assets/images/nicEditorIcons.gif differ diff --git a/app/assets/javascripts/.DS_Store b/app/assets/javascripts/.DS_Store new file mode 100644 index 0000000..635f3e6 Binary files /dev/null and b/app/assets/javascripts/.DS_Store differ diff --git a/app/assets/javascripts/nicEdit.js.erb b/app/assets/javascripts/nicEdit.js.erb new file mode 100644 index 0000000..9fade73 --- /dev/null +++ b/app/assets/javascripts/nicEdit.js.erb @@ -0,0 +1,1357 @@ +/* NicEdit - Micro Inline WYSIWYG + * Copyright 2007-2008 Brian Kirchoff + * + * NicEdit is distributed under the terms of the MIT license + * For more information visit http://nicedit.com/ + * Do not remove this copyright message + */ +var bkExtend = function(){ + var args = arguments; + if (args.length == 1) args = [this, args[0]]; + for (var prop in args[1]) args[0][prop] = args[1][prop]; + return args[0]; +}; +function bkClass() { } +bkClass.prototype.construct = function() {}; +bkClass.extend = function(def) { + var classDef = function() { + if (arguments[0] !== bkClass) { return this.construct.apply(this, arguments); } + }; + var proto = new this(bkClass); + bkExtend(proto,def); + classDef.prototype = proto; + classDef.extend = this.extend; + return classDef; +}; + +var bkElement = bkClass.extend({ + construct : function(elm,d) { + if(typeof(elm) == "string") { + elm = (d || document).createElement(elm); + } + elm = $BK(elm); + return elm; + }, + + appendTo : function(elm) { + elm.appendChild(this); + return this; + }, + + appendBefore : function(elm) { + elm.parentNode.insertBefore(this,elm); + return this; + }, + + addEvent : function(type, fn) { + bkLib.addEvent(this,type,fn); + return this; + }, + + setContent : function(c) { + this.innerHTML = c; + return this; + }, + + pos : function() { + var curleft = curtop = 0; + var o = obj = this; + if (obj.offsetParent) { + do { + curleft += obj.offsetLeft; + curtop += obj.offsetTop; + } while (obj = obj.offsetParent); + } + var b = (!window.opera) ? parseInt(this.getStyle('border-width') || this.style.border) || 0 : 0; + return [curleft+b,curtop+b+this.offsetHeight]; + }, + + noSelect : function() { + bkLib.noSelect(this); + return this; + }, + + parentTag : function(t) { + var elm = this; + do { + if(elm && elm.nodeName && elm.nodeName.toUpperCase() == t) { + return elm; + } + elm = elm.parentNode; + } while(elm); + return false; + }, + + hasClass : function(cls) { + return this.className.match(new RegExp('(\\s|^)nicEdit-'+cls+'(\\s|$)')); + }, + + addClass : function(cls) { + if (!this.hasClass(cls)) { this.className += " nicEdit-"+cls }; + return this; + }, + + removeClass : function(cls) { + if (this.hasClass(cls)) { + this.className = this.className.replace(new RegExp('(\\s|^)nicEdit-'+cls+'(\\s|$)'),' '); + } + return this; + }, + + setStyle : function(st) { + var elmStyle = this.style; + for(var itm in st) { + switch(itm) { + case 'float': + elmStyle['cssFloat'] = elmStyle['styleFloat'] = st[itm]; + break; + case 'opacity': + elmStyle.opacity = st[itm]; + elmStyle.filter = "alpha(opacity=" + Math.round(st[itm]*100) + ")"; + break; + case 'className': + this.className = st[itm]; + break; + default: + //if(document.compatMode || itm != "cursor") { // Nasty Workaround for IE 5.5 + elmStyle[itm] = st[itm]; + //} + } + } + return this; + }, + + getStyle : function( cssRule, d ) { + var doc = (!d) ? document.defaultView : d; + if(this.nodeType == 1) + return (doc && doc.getComputedStyle) ? doc.getComputedStyle( this, null ).getPropertyValue(cssRule) : this.currentStyle[ bkLib.camelize(cssRule) ]; + }, + + remove : function() { + this.parentNode.removeChild(this); + return this; + }, + + setAttributes : function(at) { + for(var itm in at) { + this[itm] = at[itm]; + } + return this; + } +}); + +var bkLib = { + isMSIE : (navigator.appVersion.indexOf("MSIE") != -1), + + addEvent : function(obj, type, fn) { + (obj.addEventListener) ? obj.addEventListener( type, fn, false ) : obj.attachEvent("on"+type, fn); + }, + + toArray : function(iterable) { + var length = iterable.length, results = new Array(length); + while (length--) { results[length] = iterable[length] }; + return results; + }, + + noSelect : function(element) { + if(element.setAttribute && element.nodeName.toLowerCase() != 'input' && element.nodeName.toLowerCase() != 'textarea') { + element.setAttribute('unselectable','on'); + } + for(var i=0;i.nicEdit-main p { margin: 0; }<\/scr"+"ipt>"); + $BK("__ie_onload").onreadystatechange = function() { + if (this.readyState == "complete"){bkLib.domLoaded();} + }; + } + window.onload = bkLib.domLoaded; + } +}; + +function $BK(elm) { + if(typeof(elm) == "string") { + elm = document.getElementById(elm); + } + return (elm && !elm.appendTo) ? bkExtend(elm,bkElement.prototype) : elm; +} + +var bkEvent = { + addEvent : function(evType, evFunc) { + if(evFunc) { + this.eventList = this.eventList || {}; + this.eventList[evType] = this.eventList[evType] || []; + this.eventList[evType].push(evFunc); + } + return this; + }, + fireEvent : function() { + var args = bkLib.toArray(arguments), evType = args.shift(); + if(this.eventList && this.eventList[evType]) { + for(var i=0;i', + buttonList : ['fontFormat','bold','italic','ol','ul','indent','outdent','superscript','subscript','image','upload','link','unlink','hr','removeformat'], + iconList : {"bgcolor":1,"forecolor":2,"bold":3,"center":4,"hr":5,"indent":6,"italic":7,"justify":8,"left":9,"ol":10,"outdent":11,"removeformat":12,"right":13,"save":24,"strikethrough":15,"subscript":16,"superscript":17,"ul":18,"underline":19,"image":20,"link":21,"unlink":22,"close":23,"arrow":25} + +}); +/* END CONFIG */ + + +var nicEditors = { + nicPlugins : [], + editors : [], + + registerPlugin : function(plugin,options) { + this.nicPlugins.push({p : plugin, o : options}); + }, + + allTextAreas : function(nicOptions) { + var textareas = document.getElementsByTagName("textarea"); + for(var i=0;i'); + } + this.instanceDoc = document.defaultView; + this.elm.addEvent('mousedown',this.selected.closureListener(this)).addEvent('keypress',this.keyDown.closureListener(this)).addEvent('focus',this.selected.closure(this)).addEvent('blur',this.blur.closure(this)).addEvent('keyup',this.selected.closure(this)); + this.ne.fireEvent('add',this); + }, + + remove : function() { + this.saveContent(); + if(this.copyElm || this.options.hasPanel) { + this.editorContain.remove(); + this.e.setStyle({'display' : 'block'}); + this.ne.removePanel(); + } + this.disable(); + this.ne.fireEvent('remove',this); + }, + + disable : function() { + this.elm.setAttribute('contentEditable','false'); + }, + + getSel : function() { + return (window.getSelection) ? window.getSelection() : document.selection; + }, + + getRng : function() { + var s = this.getSel(); + if(!s || s.rangeCount === 0) { return; } + return (s.rangeCount > 0) ? s.getRangeAt(0) : s.createRange(); + }, + + selRng : function(rng,s) { + if(window.getSelection) { + s.removeAllRanges(); + s.addRange(rng); + } else { + rng.select(); + } + }, + + selElm : function() { + var r = this.getRng(); + if(!r) { return; } + if(r.startContainer) { + var contain = r.startContainer; + if(r.cloneContents().childNodes.length == 1) { + for(var i=0;i'+((css) ? '' : '')+''+this.initialContent+''); + fd.close(); + this.frameDoc = fd; + + this.frameWin = $BK(this.elmFrame.contentWindow); + this.frameContent = $BK(this.frameWin.document.body).setStyle(this.savedStyles); + this.instanceDoc = this.frameWin.document.defaultView; + + this.heightUpdate(); + this.frameDoc.addEvent('mousedown', this.selected.closureListener(this)).addEvent('keyup',this.heightUpdate.closureListener(this)).addEvent('keydown',this.keyDown.closureListener(this)).addEvent('keyup',this.selected.closure(this)); + this.ne.fireEvent('add',this); + }, + + getElm : function() { + return this.frameContent; + }, + + setContent : function(c) { + this.content = c; + this.ne.fireEvent('set',this); + this.frameContent.innerHTML = this.content; + this.heightUpdate(); + }, + + getSel : function() { + return (this.frameWin) ? this.frameWin.getSelection() : this.frameDoc.selection; + }, + + heightUpdate : function() { + this.elmFrame.style.height = Math.max(this.frameContent.offsetHeight,this.initialHeight)+'px'; + }, + + nicCommand : function(cmd,args) { + this.frameDoc.execCommand(cmd,false,args); + setTimeout(this.heightUpdate.closure(this),100); + } + + +}); +var nicEditorPanel = bkClass.extend({ + construct : function(e,options,nicEditor) { + this.elm = e; + this.options = options; + this.ne = nicEditor; + this.panelButtons = new Array(); + this.buttonList = bkExtend([],this.ne.options.buttonList); + + this.panelContain = new bkElement('DIV').setStyle({overflow : 'hidden', width : '100%', border : '1px solid #cccccc', backgroundColor : '#efefef'}).addClass('panelContain'); + this.panelElm = new bkElement('DIV').setStyle({margin : '2px', marginTop : '0px', zoom : 1, overflow : 'hidden'}).addClass('panel').appendTo(this.panelContain); + this.panelContain.appendTo(e); + + var opt = this.ne.options; + var buttons = opt.buttons; + for(button in buttons) { + this.addButton(button,opt,true); + } + this.reorder(); + e.noSelect(); + }, + + addButton : function(buttonName,options,noOrder) { + var button = options.buttons[buttonName]; + var type = (button['type']) ? eval('(typeof('+button['type']+') == "undefined") ? null : '+button['type']+';') : nicEditorButton; + var hasButton = bkLib.inArray(this.buttonList,buttonName); + if(type && (hasButton || this.ne.options.fullPanel)) { + this.panelButtons.push(new type(this.panelElm,buttonName,options,this.ne)); + if(!hasButton) { + this.buttonList.push(buttonName); + } + } + }, + + findButton : function(itm) { + for(var i=0;i'+this.sel[itm]+''); + } + } +}); + +var nicEditorFontFamilySelect = nicEditorSelect.extend({ + sel : {'arial' : 'Arial','comic sans ms' : 'Comic Sans','courier new' : 'Courier New','georgia' : 'Georgia', 'helvetica' : 'Helvetica', 'impact' : 'Impact', 'times new roman' : 'Times', 'trebuchet ms' : 'Trebuchet', 'verdana' : 'Verdana'}, + + init : function() { + this.setDisplay('Font Family...'); + for(itm in this.sel) { + this.add(itm,''+this.sel[itm]+''); + } + } +}); + +var nicEditorFontFormatSelect = nicEditorSelect.extend({ + sel : {'p' : 'Paragraph', 'pre' : 'Pre', 'h6' : 'Heading 6', 'h5' : 'Heading 5', 'h4' : 'Heading 4', 'h3' : 'Heading 3', 'h2' : 'Heading 2', 'h1' : 'Heading 1'}, + + init : function() { + this.setDisplay('Font Format...'); + for(itm in this.sel) { + var tag = itm.toUpperCase(); + this.add('<'+tag+'>','<'+itm+' style="padding: 0px; margin: 0px;">'+this.sel[itm]+''); + } + } +}); + +nicEditors.registerPlugin(nicPlugin,nicSelectOptions); + + + +/* START CONFIG */ +var nicLinkOptions = { + buttons : { + 'link' : {name : 'Add Link', type : 'nicLinkButton', tags : ['A']}, + 'unlink' : {name : 'Remove Link', command : 'unlink', noActive : true} + } +}; +/* END CONFIG */ + +var nicLinkButton = nicEditorAdvancedButton.extend({ + addPane : function() { + this.ln = this.ne.selectedInstance.selElm().parentTag('A'); + this.addForm({ + '' : {type : 'title', txt : 'Add/Edit Link'}, + 'href' : {type : 'text', txt : 'URL', value : 'http://', style : {width: '150px'}}, + 'title' : {type : 'text', txt : 'Title'}, + 'target' : {type : 'select', txt : 'Open In', options : {'' : 'Current Window', '_blank' : 'New Window'},style : {width : '100px'}} + },this.ln); + }, + + submit : function(e) { + var url = this.inputs['href'].value; + if(url == "http://" || url == "") { + alert("You must enter a URL to Create a Link"); + return false; + } + this.removePane(); + + if(!this.ln) { + var tmp = 'javascript:nicTemp();'; + this.ne.nicCommand("createlink",tmp); + this.ln = this.findElm('A','href',tmp); + } + if(this.ln) { + this.ln.setAttributes({ + href : this.inputs['href'].value, + title : this.inputs['title'].value, + target : this.inputs['target'].options[this.inputs['target'].selectedIndex].value + }); + } + } +}); + +nicEditors.registerPlugin(nicPlugin,nicLinkOptions); + + + +/* START CONFIG */ +var nicColorOptions = { + buttons : { + 'forecolor' : {name : __('Change Text Color'), type : 'nicEditorColorButton', noClose : true}, + 'bgcolor' : {name : __('Change Background Color'), type : 'nicEditorBgColorButton', noClose : true} + } +}; +/* END CONFIG */ + +var nicEditorColorButton = nicEditorAdvancedButton.extend({ + addPane : function() { + var colorList = {0 : '00',1 : '33',2 : '66',3 :'99',4 : 'CC',5 : 'FF'}; + var colorItems = new bkElement('DIV').setStyle({width: '270px'}); + + for(var r in colorList) { + for(var b in colorList) { + for(var g in colorList) { + var colorCode = '#'+colorList[r]+colorList[g]+colorList[b]; + + var colorSquare = new bkElement('DIV').setStyle({'cursor' : 'pointer', 'height' : '15px', 'float' : 'left'}).appendTo(colorItems); + var colorBorder = new bkElement('DIV').setStyle({border: '2px solid '+colorCode}).appendTo(colorSquare); + var colorInner = new bkElement('DIV').setStyle({backgroundColor : colorCode, overflow : 'hidden', width : '11px', height : '11px'}).addEvent('click',this.colorSelect.closure(this,colorCode)).addEvent('mouseover',this.on.closure(this,colorBorder)).addEvent('mouseout',this.off.closure(this,colorBorder,colorCode)).appendTo(colorBorder); + + if(!window.opera) { + colorSquare.onmousedown = colorInner.onmousedown = bkLib.cancelEvent; + } + + } + } + } + this.pane.append(colorItems.noSelect()); + }, + + colorSelect : function(c) { + this.ne.nicCommand('foreColor',c); + this.removePane(); + }, + + on : function(colorBorder) { + colorBorder.setStyle({border : '2px solid #000'}); + }, + + off : function(colorBorder,colorCode) { + colorBorder.setStyle({border : '2px solid '+colorCode}); + } +}); + +var nicEditorBgColorButton = nicEditorColorButton.extend({ + colorSelect : function(c) { + this.ne.nicCommand('hiliteColor',c); + this.removePane(); + } +}); + +nicEditors.registerPlugin(nicPlugin,nicColorOptions); + + + +/* START CONFIG */ +var nicImageOptions = { + buttons : { + 'image' : {name : 'Add Image', type : 'nicImageButton', tags : ['IMG']} + } + +}; +/* END CONFIG */ + +var nicImageButton = nicEditorAdvancedButton.extend({ + addPane : function() { + this.im = this.ne.selectedInstance.selElm().parentTag('IMG'); + this.addForm({ + '' : {type : 'title', txt : 'Add/Edit Image'}, + 'src' : {type : 'text', txt : 'URL', 'value' : 'http://', style : {width: '150px'}}, + 'alt' : {type : 'text', txt : 'Alt Text', style : {width: '100px'}}, + 'align' : {type : 'select', txt : 'Align', options : {none : 'Default','left' : 'Left', 'right' : 'Right'}} + },this.im); + }, + + submit : function(e) { + var src = this.inputs['src'].value; + if(src == "" || src == "http://") { + alert("You must enter a Image URL to insert"); + return false; + } + this.removePane(); + + if(!this.im) { + var tmp = 'javascript:nicImTemp();'; + this.ne.nicCommand("insertImage",tmp); + this.im = this.findElm('IMG','src',tmp); + } + if(this.im) { + this.im.setAttributes({ + src : this.inputs['src'].value, + alt : this.inputs['alt'].value, + align : this.inputs['align'].value + }); + } + } +}); + +nicEditors.registerPlugin(nicPlugin,nicImageOptions); + + + + +/* START CONFIG */ +var nicSaveOptions = { + buttons : { + 'save' : {name : __('Save this content'), type : 'nicEditorSaveButton'} + } +}; +/* END CONFIG */ + +var nicEditorSaveButton = nicEditorButton.extend({ + init : function() { + if(!this.ne.options.onSave) { + this.margin.setStyle({'display' : 'none'}); + } + }, + mouseClick : function() { + var onSave = this.ne.options.onSave; + var selectedInstance = this.ne.selectedInstance; + onSave(selectedInstance.getContent(), selectedInstance.elm.id, selectedInstance); + } +}); + +nicEditors.registerPlugin(nicPlugin,nicSaveOptions); + diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index a443db3..73ed1d7 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -13,3 +13,20 @@ *= require_tree . *= require_self */ + +.biggercart { + font-size: 1.5em; +} + + + +.hr_icon { + width: 18px; + height: 18px; + overflow: hidden; + zoom: 1; cursor: pointer; + background-image: url(/assets/nicEditorIcons.gif); + background-position: -72px 0px; + display: inline-block; + margin-bottom: -6px; +} diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 433ee20..63192ce 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -11,4 +11,6 @@ class ApplicationController < ActionController::Base end end + + end diff --git a/app/controllers/carts_controller.rb b/app/controllers/carts_controller.rb new file mode 100644 index 0000000..0aeb73d --- /dev/null +++ b/app/controllers/carts_controller.rb @@ -0,0 +1,40 @@ +class CartsController < ApplicationController + before_action :check_for_cart + + def show + if cannot? :manage, :cart + redirect_to :back + end + end + + def add + item = Item.find params[:id] + @cart.add params[:id] + redirect_to :back, :flash => { :success => "#{item.title} was successfuly added to you cart!"} + end + + def remove + item = Item.find params[:id] + @cart.remove params[:id] + redirect_to :back, :flash => { :success => "#{item.title} was successfully removed from your cart"} + end + + def checkout + @invoice = @cart.checkout! + if @invoice.save + redirect_to @invoice + else + redirect_to :back, :flash => { :failure => "Your checkout was successful" } + end + end + + + + private + + def check_for_cart + @cart = current_user.carts.first_or_create + end + + +end diff --git a/app/controllers/changerole_controller.rb b/app/controllers/changerole_controller.rb new file mode 100644 index 0000000..a79f4d4 --- /dev/null +++ b/app/controllers/changerole_controller.rb @@ -0,0 +1,10 @@ +class ChangeroleController < ApplicationController + def update + if params[:role] == "seller" + current_user.update_attributes(seller: true, shopper: false) + elsif params[:role] == "shopper" + current_user.update_attributes(shopper: true, seller: false) + end + redirect_to root_path + end +end diff --git a/app/controllers/invoices_controller.rb b/app/controllers/invoices_controller.rb new file mode 100644 index 0000000..1a3de55 --- /dev/null +++ b/app/controllers/invoices_controller.rb @@ -0,0 +1,17 @@ +class InvoicesController < ApplicationController + def show + @invoice = current_user.invoices.find params[:id] + end + + def close + @invoice = current_user.invoices.find params[:id] + begin + CardProcessor.new( @invoice, params[:stripeToken] ).process + flash[:success] = "Your payment was processed successfully" + current_user.carts.first.clean + rescue CardProcessor::ProcessingError => e + flash[:error] = e.message + end + redirect_to @invoice + end +end diff --git a/app/controllers/items_controller.rb b/app/controllers/items_controller.rb new file mode 100644 index 0000000..83f5bb4 --- /dev/null +++ b/app/controllers/items_controller.rb @@ -0,0 +1,63 @@ +class ItemsController < ApplicationController + + def show + @item = Item.find(params[:id]) + end + + + def new + if can? :manage, :items + @item = current_user.items.new + else + redirect_to root_path + end + end + + def create + if can? :manage, :items + @item = current_user.items.new create_params + if @item.save! + redirect_to item_path(@item) + else + redirect_to :back + end + end + + end + + + def edit + if (current_user[:id] = params[:seller_id]) || (current_user.admin?) + @item = Item.find(params[:id]) + else + redirect_to :back, :alert => "You do not have permission to edit this item." + end + end + + def update + if can? :manage, :items + @item = Item.find(params[:id]) + if @item.update! create_params + redirect_to @item + else + redirect_to @item, :alert => "Something went wrong" + end + else + redirect_to root_path, :alert => "You do not have permission to modify edit this item." + end + end + + + + + def destroy + + end + + private + + def create_params + params.require(:item).permit(:title, :price, :description, :avatar) + end + +end diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb new file mode 100644 index 0000000..475b63e --- /dev/null +++ b/app/controllers/registrations_controller.rb @@ -0,0 +1,18 @@ +class RegistrationsController < Devise::RegistrationsController + + def create + super + current_user.carts.create! + end + + def new + super + end + + def update + super + + end + + +end diff --git a/app/controllers/static_pages_controller.rb b/app/controllers/static_pages_controller.rb index cf48a8b..aba6cd1 100644 --- a/app/controllers/static_pages_controller.rb +++ b/app/controllers/static_pages_controller.rb @@ -1,4 +1,9 @@ class StaticPagesController < ApplicationController + + def home + @items = Item.text_search(params[:query]) end + + end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb new file mode 100644 index 0000000..6413b74 --- /dev/null +++ b/app/controllers/users_controller.rb @@ -0,0 +1,5 @@ +class UsersController < ApplicationController + + + +end diff --git a/app/mailers/invoice_mailler.rb b/app/mailers/invoice_mailler.rb new file mode 100644 index 0000000..8b88f1f --- /dev/null +++ b/app/mailers/invoice_mailler.rb @@ -0,0 +1,7 @@ +class InvoiceMailer < ActionMailer::Base + default from: 'receipts@iron-shop.example.com' + def receipt invoice + @invoice = invoice + mail to: invoice.shopper.email, subject: "Receipt for purchase on; #{invoice.created_at}" + end +end diff --git a/app/models/ability.rb b/app/models/ability.rb index 508b776..8d4209f 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -4,6 +4,16 @@ class Ability def initialize(user) user ||= User.new + if user.admin + can :manage, :all + elsif user.seller + can :manage, :items + else + can :read, :all + can :manage, :cart + can :read, :invoice + end + # Define abilities for the passed in user here. For example: # # user ||= User.new # guest user (not logged in) @@ -13,12 +23,12 @@ def initialize(user) # can :read, :all # end # - # The first argument to `can` is the action you are giving the user + # The first argument to `can` is the action you are giving the user # permission to do. # If you pass :manage it will apply to every action. Other common actions # here are :read, :create, :update and :destroy. # - # The second argument is the resource the user can perform the action on. + # The second argument is the resource the user can perform the action on. # If you pass :all it will apply to every resource. Otherwise pass a Ruby # class of the resource. # diff --git a/app/models/cart.rb b/app/models/cart.rb new file mode 100644 index 0000000..2dcc4c0 --- /dev/null +++ b/app/models/cart.rb @@ -0,0 +1,44 @@ +class Cart < ActiveRecord::Base + + has_many :cart_items + has_many :items, through: :cart_items + belongs_to :shopper, class_name: "User" + + def add item_id + new_item = self.cart_items.new + new_item.item_id = item_id + new_item.save + end + + def remove item_id + item = self.cart_items.where(cart_id: self.id, item_id: item_id).first + self.cart_items.delete (item) + end + + def subtotal + subtotal = 0 + self.items.each do |item| + subtotal = subtotal + item.price + end + subtotal + end + + def total + total = self.subtotal * (1 + self.tax_rate) + total.round(2) + end + + def checkout! + invoice = self.shopper.invoices.new + self.items.each do |item| + invoice.items << item + end + invoice.amount = self.total + invoice + end + + def clean + items = self.cart_items + items.delete_all + end +end diff --git a/app/models/cart_item.rb b/app/models/cart_item.rb new file mode 100644 index 0000000..65044d6 --- /dev/null +++ b/app/models/cart_item.rb @@ -0,0 +1,4 @@ +class CartItem < ActiveRecord::Base + belongs_to :cart + belongs_to :item +end diff --git a/app/models/invoice.rb b/app/models/invoice.rb new file mode 100644 index 0000000..9ef1ee4 --- /dev/null +++ b/app/models/invoice.rb @@ -0,0 +1,26 @@ +# == Schema Information +# +# Table name: invoices +# +# id :integer not null, primary key +# amount :float +# shopper_id :integer +# created_at :datetime +# updated_at :datetime +# paid :boolean default(FALSE) +# + +class Invoice < ActiveRecord::Base + validates_presence_of :shopper_id, :amount + validates :amount, numericality: { greater_than_or_equal_to: 0 } + + belongs_to :shopper, class_name: "User" + + has_many :invoice_items + has_many :items, through: :invoice_items + + def amount_in_cents + (self.amount * 100).round() + end + +end diff --git a/app/models/invoice_item.rb b/app/models/invoice_item.rb new file mode 100644 index 0000000..0373260 --- /dev/null +++ b/app/models/invoice_item.rb @@ -0,0 +1,15 @@ +# == Schema Information +# +# Table name: invoice_items +# +# id :integer not null, primary key +# invoice_id :integer +# item_id :integer +# created_at :datetime +# updated_at :datetime +# + +class InvoiceItem < ActiveRecord::Base + belongs_to :invoice + belongs_to :item +end diff --git a/app/models/item.rb b/app/models/item.rb new file mode 100644 index 0000000..022ca5a --- /dev/null +++ b/app/models/item.rb @@ -0,0 +1,37 @@ +# == Schema Information +# +# Table name: items +# +# id :integer not null, primary key +# title :string(255) +# description :text +# price :float +# seller_id :integer +# created_at :datetime +# updated_at :datetime +# + +class Item < ActiveRecord::Base + validates_presence_of :title, :description, :price, :seller_id + validates :price, numericality: { greater_than_or_equal_to: 0 } + + belongs_to :seller, class_name: "User" + has_many :carts, through: :cart_items + has_many :cart_items + + has_attached_file :avatar, :styles => { :medium => "300x300>", :thumb => "100x100>" }, :default_url => "/images/:style/missing.png" + validates_attachment_content_type :avatar, :content_type => /\Aimage\/.*\Z/ + + + include PgSearch + pg_search_scope :search, against: [:title, :description], + using: { tsearch: { dictionary: "english" } } + + def self.text_search(query) + if query.present? + where("title @@ :q or description @@ :q", q: query) + else + all + end + end +end diff --git a/app/models/relationship.rb b/app/models/relationship.rb new file mode 100644 index 0000000..85a1832 --- /dev/null +++ b/app/models/relationship.rb @@ -0,0 +1,5 @@ +class Relationship < ActiveRecord::Base + belongs_to :item + belongs_to :invoice + +end diff --git a/app/models/user.rb b/app/models/user.rb index c822027..af2ce47 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,6 +1,35 @@ +# == Schema Information +# +# Table name: users +# +# id :integer not null, primary key +# email :string(255) default(""), not null +# encrypted_password :string(255) default(""), not null +# reset_password_token :string(255) +# reset_password_sent_at :datetime +# remember_created_at :datetime +# sign_in_count :integer default(0), not null +# current_sign_in_at :datetime +# last_sign_in_at :datetime +# current_sign_in_ip :string(255) +# last_sign_in_ip :string(255) +# created_at :datetime +# updated_at :datetime +# role :string(255) +# admin :boolean default(FALSE) +# + class User < ActiveRecord::Base + # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable and :omniauthable devise :database_authenticatable, :registerable, - :recoverable, :rememberable, :trackable, :validatable + :recoverable, :rememberable, :trackable, :validatable, :async + + has_many :items, foreign_key: 'seller_id' + has_many :invoices, foreign_key: 'shopper_id' + + has_many :carts, foreign_key: 'shopper_id' + # has_and_belongs_to_many :invoices + end diff --git a/app/services/card_processor.rb b/app/services/card_processor.rb new file mode 100644 index 0000000..25bb981 --- /dev/null +++ b/app/services/card_processor.rb @@ -0,0 +1,43 @@ +class CardProcessor + class ProcessingError < StandardError ; end + + def initialize invoice, token + @invoice, @token = invoice, token + end + + def process + begin + @customer = create_Stripe_customer + process_charge! @customer + rescue Stripe::CardError => e + raise ProcessingError, e.message + end + note_payment + end + + def create_Stripe_customer + customer = Stripe::Customer.create( + :email => @invoice.shopper.email, + :card => @token + ) + end + + def process_charge! customer + charge = Stripe::Charge.create( + :customer => customer.id, + :amount => @invoice.amount_in_cents, + :description => 'Iron Shop customer', + :currency => 'usd' + ) + end + + def note_payment + @invoice.paid = true + @invoice.save! + end + + def send_receipt + ## InvoiceMailler.receipt(@invoice).deliver + MailReceiptWorker.perform_async @invoice.id + end +end diff --git a/app/views/carts/show.html.haml b/app/views/carts/show.html.haml new file mode 100644 index 0000000..b486b88 --- /dev/null +++ b/app/views/carts/show.html.haml @@ -0,0 +1,33 @@ +.page-header + %h1 + Shopping Cart + %small + = "for #{current_user.email}" + +- if @cart.items.empty? + %br + %br + .jumbotron + %h1 Your Cart Is Empty! + %p Follow this link here to find some products + %p.btn.btn-primary=link_to "Click Me", root_path, :style => 'color: #FFF;' + +- else + %table.table.table-striped + %thead + %tr + %th Item + %th Price + %th Seller + %th + %tbody + -@cart.items(include: :seller).each do |item| + %tr + %td= item.title + %td= item.price + %td= item.seller.email + %td= link_to "Remove Items", cart_remove_path(id: item.id) + + %br + %hr + %button.btn.btn-default= link_to "Check Out!", cart_checkout_path diff --git a/app/views/devise/registrations/edit.html.haml b/app/views/devise/registrations/edit.html.haml index 04251b2..ca29f73 100644 --- a/app/views/devise/registrations/edit.html.haml +++ b/app/views/devise/registrations/edit.html.haml @@ -1,5 +1,27 @@ +%h2 + Change Role +%br + +- if current_user.shopper? + %input{ type: "hidden", id: "role", value: "shopper" } +- elsif current_user.seller? + %input{ type: "hidden", id: "role", value: "seller" } + +.text-center + = form_tag changerole_path, method: :post, class: "btn-group" do + %label.btn.btn-default + %input{ type: "radio", name: "role", value: "shopper", id: "shopper" } + Shopping + %label.btn.btn-default + %input{ type: "radio", name: "role", value: "seller", id: "seller" } + Selling + %button.btn.btn-primary.pull-right Done +%br +%hr + %h2 Edit #{resource_name.to_s.humanize} +%br = form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }, class: "form") do |f| = devise_error_messages! .form-group @@ -17,9 +39,22 @@ .form-group = f.label :current_password = f.password_field :current_password, autocomplete: "off", class: "form-control" - = f.submit "Update", class: "btn btn-primary" + = f.submit "Update", class: "btn btn-primary pull-right" + %br + %hr %h3 Cancel my account %p Unhappy? #{button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?" }, class: "btn btn-danger", method: :delete} = link_to "Back", :back + + +:javascript + $(function() { + var role = document.getElementById("role").value; + if(role === "shopper"){ + $('#shopper').prop("checked", true); + } else if(role === "seller") { + $('#seller').prop("checked", true); + } + }); diff --git a/app/views/invoice_mailer/receipt.html.haml b/app/views/invoice_mailer/receipt.html.haml new file mode 100644 index 0000000..ab79d48 --- /dev/null +++ b/app/views/invoice_mailer/receipt.html.haml @@ -0,0 +1,13 @@ +%h1 Here is your recent transaction with the Iron Shop + +%ul + - if @invoice + - @invoice.items.each do |item| + %li.row + .col-lg-6 + = item.title + .col-lg-6 + = item.price + +%hr +%h2 Thank you for shopping with us. diff --git a/app/views/invoices/show.html.haml b/app/views/invoices/show.html.haml new file mode 100644 index 0000000..0414373 --- /dev/null +++ b/app/views/invoices/show.html.haml @@ -0,0 +1,24 @@ +%h1 + #{@invoice.paid? ? 'Paid' : 'Unpaid'} Invoice + %small= @invoice.created_at + +%table.table.table-striped + %thead + %tr + %th Item + %th Price + %tbody + - @invoice.items.each do |item| + %tr + %td= item.title + %td= item.price + %tr + %td Total + %td= @invoice.amount +-if !@invoice.paid? + = form_tag close_invoice_path(@invoice) do + %script.stripe-button{ 'src' => 'https://checkout.stripe.com/checkout.js', + 'data-key' => ENV['STRIPE_PUBLISHABLE_KEY'], + 'data-name' => 'Iron Shop', + 'data-description' => "#{@invoice.items.count} items", + 'data-amount' => @invoice.amount_in_cents } diff --git a/app/views/items/edit.html.haml b/app/views/items/edit.html.haml new file mode 100644 index 0000000..6d144ff --- /dev/null +++ b/app/views/items/edit.html.haml @@ -0,0 +1,17 @@ += form_for(@item, :html => { :multipart => true }, class: "form") do |p| + .form-group + =p.label :title + =p.text_field :title, class: "form-control" + .form-group + =p.file_field :avatar + .form-group + =p.label :description + =p.text_area :description, class: "form-control", rows: 6 + .form-group + =p.label :price + =p.text_field :price, placeholder: '0.00', class: "form-control" + + =p.submit "Update Item", {:class => "btn btn-default"} + +:javascript + bkLib.onDomLoaded(nicEditors.allTextAreas); diff --git a/app/views/items/new.html.haml b/app/views/items/new.html.haml new file mode 100644 index 0000000..adbf502 --- /dev/null +++ b/app/views/items/new.html.haml @@ -0,0 +1,17 @@ += form_for(@item, :html => { :multipart => true }, class: "form") do |p| + .form-group + =p.label :title + =p.text_field :title, class: "form-control" + .form-group + =p.file_field :avatar + .form-group + =p.label :description + =p.text_area :description, class: "form-control", rows: 6 + .form-group + =p.label :price + =p.text_field :price, placeholder: '0.00', class: "form-control" + + =p.submit + +:javascript + bkLib.onDomLoaded(nicEditors.allTextAreas); diff --git a/app/views/items/show.html.haml b/app/views/items/show.html.haml new file mode 100644 index 0000000..dd6e9c1 --- /dev/null +++ b/app/views/items/show.html.haml @@ -0,0 +1,19 @@ +.page-header + %h1 + = @item.title + %small= "$#{@item.price}" + .pull-right + -if can? :manage, :cart + %button.btn.btn-default + =link_to "Add to Cart", cart_add_path(id: @item) + -if can? :manage, :items + %button.btn.btn-default + =link_to "Edit", edit_item_path +.row + .col-md-4 + = image_tag @item.avatar.url(:medium), :class => "img-rounded img-responsive" + .col-md-8 + %p= simple_format(@item.description) + + %br + %br diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index debc1fb..4dfc462 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -16,14 +16,23 @@ %nav.navbar.navbar-default .container .navbar-header - = link_to "", root_path, class: "navbar-brand" + = link_to "Iron Shop", root_path, class: "navbar-brand" %ul.nav.navbar-nav - / Main links will go here %ul.nav.navbar-nav.pull-right - if current_user / Signed in + - if can? :manage, :items + %li= link_to "Post Item", new_item_path + + - if can? :manage, :cart + - current_user.carts.each do |cart| + %li.biggercart= link_to "", cart_path, class: "glyphicon glyphicon-shopping-cart" + -if cart.subtotal.to_s != "0" + %li= link_to "$#{cart.subtotal.to_s}", cart_path, :style => 'color: green;' + + %li= link_to current_user.email, edit_user_registration_path %li= link_to "Log out", destroy_user_session_path, method: :delete - else diff --git a/app/views/static_pages/home.html.haml b/app/views/static_pages/home.html.haml index e69de29..08919fb 100644 --- a/app/views/static_pages/home.html.haml +++ b/app/views/static_pages/home.html.haml @@ -0,0 +1,23 @@ +.page-header + %h1 + Iron Shop + %small Ruling ecommerce with an Iron fist. + += form_tag root_path, class: "form", method: :get do + .form-group.col-lg-10 + = text_field_tag :query, params[:query], class: "form-control", placeholder: "Search for items" + = submit_tag "Search", name: nil, class: "btn" + = link_to "Reset", root_path + %hr +%table.table.table-striped + %thead + %tr + %th Item + %th Description + %th Price + %tbody + - @items.each do |item| + %tr + %td= link_to item.title, item_path(item) + %td= strip_tags(item.description.truncate(100)) + %td= "$#{item.price}" diff --git a/app/workers/mail_receipt_worker.rb b/app/workers/mail_receipt_worker.rb new file mode 100644 index 0000000..35e8ee8 --- /dev/null +++ b/app/workers/mail_receipt_worker.rb @@ -0,0 +1,10 @@ +class MailReceiptWorker + include Sidekiq::Worker + + def perform invoice_id + invoice = Invoice.find_by_id invoice_id + return if invoice.nil? + InvoiceMailer.receipt(invoice).deliver + + end +end diff --git a/config/database.yml b/config/database.yml index 1c1a37c..d62a62b 100644 --- a/config/database.yml +++ b/config/database.yml @@ -9,9 +9,17 @@ default: &default pool: 5 timeout: 5000 +# development: +# <<: *default +# database: db/development.sqlite3 + development: - <<: *default - database: db/development.sqlite3 + adapter: postgresql + encoding: utf8 + database: project_development + pool: 5 + username: + password: # Warning: The database defined as "test" will be erased and # re-generated from your development database when you run "rake". diff --git a/config/environments/development.rb b/config/environments/development.rb index 5851ebe..e4d3423 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -1,6 +1,18 @@ Rails.application.configure do - # Settings specified here will take precedence over those in config/application.rb. + + config.paperclip_defaults = { + :storage => :s3, + :s3_credentials => { + :bucket => ENV['S3_BUCKET_NAME'], + :access_key_id => ENV['AWS_ACCESS_KEY_ID'], + :secret_access_key => ENV['AWS_SECRET_ACCESS_KEY'] + } + } + + + # Settings specified here will take precedence over those in config/application.rb. + config.action_mailer.delivery_method = :letter_opener # In the development environment your application's code is reloaded on # every request. This slows down response time but is perfect for development # since you don't have to restart the web server when you make code changes. diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 09a2231..388530e 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -1,6 +1,8 @@ # Use this hook to configure devise mailer, warden hooks and so forth. # Many of these configuration options can be set straight in your model. Devise.setup do |config| + #require 'devise/orm/active_record' + # The secret key used by Devise. Devise uses this key to generate # random tokens. Changing this key will render invalid all existing # confirmation, reset password and unlock tokens in the database. diff --git a/config/initializers/devise_async.rb b/config/initializers/devise_async.rb new file mode 100644 index 0000000..6078eff --- /dev/null +++ b/config/initializers/devise_async.rb @@ -0,0 +1,4 @@ +Devise::Async.setup do |config| + config.enabled = true # | false + config.backend = :sidekiq # default is :resque +end diff --git a/config/initializers/stripe.rb b/config/initializers/stripe.rb new file mode 100644 index 0000000..8231cc8 --- /dev/null +++ b/config/initializers/stripe.rb @@ -0,0 +1,6 @@ +Rails.configuration.stripe = { + :publishable_key => ENV['STRIPE_PUBLISHABLE_KEY'], + :secret_key => ENV['STRIPE_API_KEY'] +} + +Stripe.api_key = Rails.configuration.stripe[:secret_key] diff --git a/config/routes.rb b/config/routes.rb index 4ccf26b..5590e6c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,5 +1,31 @@ +require 'sidekiq/web' + Rails.application.routes.draw do - devise_for :users + + + + devise_for :users, :controllers => {:registrations => "registrations"} + resources :items, except: [:index] + + post '/changerole' => 'changerole#update' + + get '/cart', to: 'carts#show' + get '/cart/add', to: 'carts#add' + get '/cart/remove', to: 'carts#remove' + get '/cart/checkout', to: 'carts#checkout' + + resources :invoices, only: [:show] do + member do + post :close + end + end root to: "static_pages#home" + + # authenticate :user, lambda { |u| u.admin? } do + # mount Sidekiq::Web => '/sidekiq' + # end + authenticate :user do + mount Sidekiq::Web => '/sidekiq' + end end diff --git a/db/.DS_Store b/db/.DS_Store new file mode 100644 index 0000000..3d42fc4 Binary files /dev/null and b/db/.DS_Store differ diff --git a/db/migrate/20140926010316_add_attachment_avatar_to_items.rb b/db/migrate/20140926010316_add_attachment_avatar_to_items.rb new file mode 100644 index 0000000..f72028a --- /dev/null +++ b/db/migrate/20140926010316_add_attachment_avatar_to_items.rb @@ -0,0 +1,11 @@ +class AddAttachmentAvatarToItems < ActiveRecord::Migration + def self.up + change_table :items do |t| + t.attachment :avatar + end + end + + def self.down + remove_attachment :items, :avatar + end +end diff --git a/db/schema.rb b/db/schema.rb index 3f5d177..a92012c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,48 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20140826192403) do +ActiveRecord::Schema.define(version: 20140926010316) do + + # These are extensions that must be enabled in order to support this database + enable_extension "plpgsql" + + create_table "cart_items", force: true do |t| + t.integer "cart_id" + t.integer "item_id" + end + + create_table "carts", force: true do |t| + t.integer "shopper_id" + t.decimal "tax_rate", precision: 7, scale: 2, default: 0.04, null: false + end + + create_table "invoice_items", force: true do |t| + t.integer "invoice_id" + t.integer "item_id" + t.datetime "created_at" + t.datetime "updated_at" + end + + create_table "invoices", force: true do |t| + t.decimal "amount", precision: 7, scale: 2 + t.integer "shopper_id" + t.datetime "created_at" + t.datetime "updated_at" + t.boolean "paid", default: false + end + + create_table "items", force: true do |t| + t.string "title" + t.text "description" + t.decimal "price", precision: 7, scale: 2 + t.integer "seller_id" + t.datetime "created_at" + t.datetime "updated_at" + t.string "avatar_file_name" + t.string "avatar_content_type" + t.integer "avatar_file_size" + t.datetime "avatar_updated_at" + end create_table "users", force: true do |t| t.string "email", default: "", null: false @@ -26,9 +67,12 @@ t.string "last_sign_in_ip" t.datetime "created_at" t.datetime "updated_at" + t.boolean "admin", default: false + t.boolean "shopper", default: true + t.boolean "seller", default: false end - add_index "users", ["email"], name: "index_users_on_email", unique: true - add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true + add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree + add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree end diff --git a/lib/tasks/populate.rake b/lib/tasks/populate.rake new file mode 100644 index 0000000..ddf938a --- /dev/null +++ b/lib/tasks/populate.rake @@ -0,0 +1,20 @@ +namespace :db do + desc "Fill database with sample data" + task populate: :environment do + require 'faker' + make_users_and_items_and_invoices + end +end + +def make_users_and_items_and_invoices + 10.times do |n| + User.create!(email: Faker::Internet.email, + password: "password", + password_confirmation: "password") + + Item.create!(seller_id: 1, + title: Faker::App.name, + description: Faker::Hacker.say_something_smart, + price: Faker::Commerce.price) + end +end diff --git a/spec/factories/invoice.rb b/spec/factories/invoice.rb index f6b5094..454d297 100644 --- a/spec/factories/invoice.rb +++ b/spec/factories/invoice.rb @@ -1,6 +1,7 @@ FactoryGirl.define do factory :invoice do - user { create :user, :shopper } + shopper { create :user, :shopper } amount 1.00 + paid false end end diff --git a/spec/factories/user.rb b/spec/factories/user.rb index b59ff87..6f0c393 100644 --- a/spec/factories/user.rb +++ b/spec/factories/user.rb @@ -7,9 +7,9 @@ trait :shopper do # v- DB column role - role 'shopper' end trait :seller do - role 'seller' + seller true + shopper false end end diff --git a/spec/models/cart_spec.rb b/spec/models/cart_spec.rb index 32812dc..34ea61d 100644 --- a/spec/models/cart_spec.rb +++ b/spec/models/cart_spec.rb @@ -1,9 +1,18 @@ +# == Schema Information +# +# Table name: carts +# +# id :integer not null, primary key +# shopper_id :integer +# tax_rate :integer +# + require 'rails_helper' describe Cart do before :each do @shopper = create :user, :shopper - @cart = Cart.new @shopper + @cart = @shopper.carts.new [1.00, 5432.99, 161.8].each do |price| @cart.add( create :item, price: price ) end @@ -24,7 +33,7 @@ it 'can set a different tax rate' do @cart.tax_rate = 0.07 - expect( @cart.tax_rate ).to eq 0.07 + expect( @cart.tax_rate.to_f ).to eq 0.07 expect( @cart.total ).to eq 5987.50 end diff --git a/spec/models/invoice_spec.rb b/spec/models/invoice_spec.rb index 194486c..425d22f 100644 --- a/spec/models/invoice_spec.rb +++ b/spec/models/invoice_spec.rb @@ -1,7 +1,19 @@ +# == Schema Information +# +# Table name: invoices +# +# id :integer not null, primary key +# amount :float +# shopper_id :integer +# created_at :datetime +# updated_at :datetime +# paid :boolean default(FALSE) +# + require 'rails_helper' describe Invoice do - %i(user amount).each do |field| + %i(shopper amount).each do |field| it "requires a #{field}" do invoice = build :invoice, field => nil expect( invoice.valid? ).to eq false diff --git a/spec/models/item_spec.rb b/spec/models/item_spec.rb index 3543387..7a1f884 100644 --- a/spec/models/item_spec.rb +++ b/spec/models/item_spec.rb @@ -1,3 +1,16 @@ +# == Schema Information +# +# Table name: items +# +# id :integer not null, primary key +# title :string(255) +# description :text +# price :float +# seller_id :integer +# created_at :datetime +# updated_at :datetime +# + require 'rails_helper' describe Item do diff --git a/spec/services/card_processor_spec.rb b/spec/services/card_processor_spec.rb new file mode 100644 index 0000000..1b9b88f --- /dev/null +++ b/spec/services/card_processor_spec.rb @@ -0,0 +1,35 @@ +describe CardProcessor do + before :each do + @invoice = create :invoice + end + + it 'marks the invoice as paid' do + token = Stripe::Token.create(card: { + number: '4242424242424242', + exp_month: 1, + exp_year: 2015, + cvc: '777' + }) + + processor = CardProcessor.new @invoice, token.id + processor.process + + expect( @invoice.paid? ).to be true + end + + it 'handles declined cards' do + token = Stripe::Token.create(card: { + number: '4000000000000002', + exp_month: 1, + exp_year: 2015, + cvc: '112' + }) + + processor = CardProcessor.new @invoice, token.id + expect do + processor.process + end.to raise_error CardProcessor::ProcessingError + + expect( @invoice.paid? ).to be false + end +end diff --git a/spec/support/sidekiq.rb b/spec/support/sidekiq.rb new file mode 100644 index 0000000..cf3faea --- /dev/null +++ b/spec/support/sidekiq.rb @@ -0,0 +1 @@ +require 'sidekiq/testing'