diff --git a/app/assets/stylesheets/carbuncle.css b/app/assets/stylesheets/carbuncle.css index 0fc4080..ffc980f 100644 --- a/app/assets/stylesheets/carbuncle.css +++ b/app/assets/stylesheets/carbuncle.css @@ -105,4 +105,8 @@ div div.elem:last-of-type .submit_field{ padding-top: 15px; +} + +.clickable { + cursor: pointer; } \ No newline at end of file diff --git a/app/controllers/administration/manage_users_controller.rb b/app/controllers/administration/manage_users_controller.rb index 1b6f685..a4af5fe 100644 --- a/app/controllers/administration/manage_users_controller.rb +++ b/app/controllers/administration/manage_users_controller.rb @@ -43,7 +43,7 @@ def destroy @user.destroy end - redirect_to administration_users_path + redirect_to administration_manage_users_path end private @@ -63,7 +63,7 @@ def save_form @user.attributes = user_params if @user.save flash[:notice] = "Successfully saved #{@user.email}" - redirect_to administration_users_url + redirect_to administration_manage_users_url else setup_form end diff --git a/app/controllers/notes_controller.rb b/app/controllers/notes_controller.rb new file mode 100644 index 0000000..8799316 --- /dev/null +++ b/app/controllers/notes_controller.rb @@ -0,0 +1,91 @@ +class NotesController < ApplicationController + before_action :setup_sorting_variables, only: [:index] + before_action :find_note, except: [:index] + + def index + sort_key = [:value, :email, :created, :updated][@sort] + direction = (@asc == 1) ? :asc : :desc + + @notes = Note.similar_notes(params[:search]). + ordered_by(sort_key, direction).page(@page).per_page(10) + end + + def search_users + @users = User.similar_emails(params[:partial_email]). + ordered_by(:email, :asc).limit(10) + + respond_to do |format| + format.html { render partial: "/layouts/user_list" } + format.json { render :search_users } + end + end + + def show + end + + def new + setup_form + end + + def edit + setup_form + end + + def create + save_form + end + + def update + save_form + end + + def destroy + if @note.nil? + flash[:error] = "Can't find note" + elsif @note.new_record? + flash[:error] = "Can't destroy new note" + else + flash[:notice] = "You destroyed #{@note.value}" + @note.destroy + end + + redirect_to notes_path + end + + private + def find_note + @note = if params[:id].blank? + Note.new + else + Note.where(id: params[:id]).first + end + end + + def setup_form + render :form + end + + def save_form + @note.attributes = note_params + if @note.save + flash[:notice] = "Successfully saved #{@note.value}" + redirect_to notes_url + else + setup_form + end + end + + def note_params + params.require(:note).permit(:value, :user_id) + end + + def view_content + render params[:action] + end + + def setup_sorting_variables + @sort = params[:sort].blank? ? 0 : params[:sort].to_i + @asc = params[:asc].blank? ? 0 : params[:asc].to_i + @page = params[:page].blank? ? 1 : params[:page].to_i + end +end diff --git a/app/controllers/products_controller.rb b/app/controllers/products_controller.rb index 393047d..5b5000f 100644 --- a/app/controllers/products_controller.rb +++ b/app/controllers/products_controller.rb @@ -3,7 +3,7 @@ class ProductsController < ApplicationController before_action :find_product, except: [:index] def index - sort_key = [:name, :descritpion, :cost, :created, :updated][@sort] + sort_key = [:name, :description, :cost, :created, :updated][@sort] direction = (@asc == 1) ? :asc : :desc @products = Product.similar_names(params[:search]). diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index c369bd5..30fa72a 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,7 +1,7 @@ module ApplicationHelper def sortable(column, title = nil) title ||= column.titleize - css_class = (column == @sort) ? "current ↵ #{(@asc == 1) ? 'asc' : 'desc'}" : nil + css_class = (column == @sort) ? "current #{(@asc == 1) ? 'asc' : 'desc'}" : nil direction = (column == @sort && @asc == 1) ? 0 : 1 #link_to title, {:sort => column, :direction => direction}, {:class => css_class} link_to title, params.merge(sort: column, asc: direction, page: nil), {:class => css_class} diff --git a/app/models/note.rb b/app/models/note.rb new file mode 100644 index 0000000..4be831e --- /dev/null +++ b/app/models/note.rb @@ -0,0 +1,22 @@ +class Note < ActiveRecord::Base + belongs_to :user + + validates :value, presence: true + + scope :similar_notes, -> (partial) { partial.blank? ? all : where{ value =~ "%#{partial}%" } } + + def self.ordered_by(sort, asc) + case sort + when :value + order{ value.send(asc) } + when :email + joins{ user.outer }.order{ user.email.send(asc) } + when :created + order{ created_at.send(asc) } + when :updated + order{ updated_at.send(asc) } + else + all + end + end +end diff --git a/app/models/painting.rb b/app/models/painting.rb index 985702c..6644872 100644 --- a/app/models/painting.rb +++ b/app/models/painting.rb @@ -4,10 +4,10 @@ class Painting < ActiveRecord::Base mount_uploader :image, ImageUploader - before_create :default_title + before_validation :default_title def default_title - self.title ||= File.basename(image.filename, '.*').titleize if image + self.title ||= File.basename(image.filename.to_s, '.*').titleize if image end def self.ordered_by(sort, asc) diff --git a/app/models/user.rb b/app/models/user.rb index 3e108eb..9516456 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,4 +1,5 @@ class User < ActiveRecord::Base + has_many :notes, dependent: :destroy # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable and :omniauthable devise :database_authenticatable, :registerable, :omniauthable, diff --git a/app/presenters/note_presenter.rb b/app/presenters/note_presenter.rb new file mode 100644 index 0000000..b09a451 --- /dev/null +++ b/app/presenters/note_presenter.rb @@ -0,0 +1,12 @@ +class NotePresenter < BasePresenter + presents :note + + delegate :value, :user, :created_at, :updated_at, to: :note + + def creator + unless note.user.nil? + note.user.email + end + end + # Other presenter methods to help show user information +end diff --git a/app/views/administration/manage_users/index.html.haml b/app/views/administration/manage_users/index.html.haml index 28feb30..8949746 100644 --- a/app/views/administration/manage_users/index.html.haml +++ b/app/views/administration/manage_users/index.html.haml @@ -17,27 +17,8 @@ - else No users match your search criteria - %br/ %br/ .white_link.small_margin = link_to "New User", new_administration_manage_user_path, class: 'btn btn-default' - - -:javascript - // Add click event to each of the headers of the products table, and pagination - $(function () { - $('#users_table th a, #users_table .pagination a').on('click', ↵ - function () { - $.getScript(this.href); - return false; - } - ); - - // Search form. - $('#user_search').submit(function () { - $.get(this.action, $(this).serialize(), null, 'script'); - return false; - }); - }); \ No newline at end of file diff --git a/app/views/layouts/_user_list.html.haml b/app/views/layouts/_user_list.html.haml new file mode 100644 index 0000000..9b81656 --- /dev/null +++ b/app/views/layouts/_user_list.html.haml @@ -0,0 +1,6 @@ +- unless @users.empty? + .elem + Select from users with similar emails + - @users.each do |u| + .selection{ id: u.id } + = u.email \ No newline at end of file diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 38ed3a1..d070a27 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -12,13 +12,15 @@
- <%= link_to "Dashboard", root_path, {class: 'btn btn-primary'} %> - <%= link_to "Users", administration_manage_users_path, {class: 'btn btn-primary'} %> - <%= link_to "Images", paintings_path, {class: 'btn btn-primary'} %> - <%= link_to "Image Fader", fader_path, {class: 'btn btn-primary'} %> - <%= link_to "Products", products_path, {class: 'btn btn-primary'} %> - <%= link_to "Knockout test", knockout_test_path, {class: 'btn btn-primary'} %> - +
<% if user_signed_in? %> @@ -34,6 +36,12 @@
+ <% unless flash[:alert].blank? %> +
+ <%= flash[:alert] %> +
+ <% end %> + <% unless flash[:notice].blank? %>
<%= flash[:notice] %> diff --git a/app/views/notes/_list.html.haml b/app/views/notes/_list.html.haml new file mode 100644 index 0000000..d222089 --- /dev/null +++ b/app/views/notes/_list.html.haml @@ -0,0 +1,14 @@ += will_paginate @notes + += hidden_field_tag :sort, params[:sort] += hidden_field_tag :page, params[:page] + +%table.table.table-striped + %tr + - ["Value", "User", "Created", "Modified"].each_with_index do |title, index| + %th= sortable index, title + %th + %th + %th + + = render partial: 'notes_table' diff --git a/app/views/notes/_notes_table.html.haml b/app/views/notes/_notes_table.html.haml new file mode 100644 index 0000000..559eb2a --- /dev/null +++ b/app/views/notes/_notes_table.html.haml @@ -0,0 +1,10 @@ +- @notes.each do |note| + - present note do |n| + %tr + %td= n.value + %td= n.creator || "No user" + %td= n.created_at.strftime('%D') + %td= n.updated_at.strftime('%D') + %td= link_to "Show", n.note + %td= link_to 'Edit', edit_note_path(n.note) + %td= link_to 'Destroy', n.note, method: :delete, data: { confirm: "Are you sure?" } \ No newline at end of file diff --git a/app/views/notes/form.html.haml b/app/views/notes/form.html.haml new file mode 100644 index 0000000..155eecf --- /dev/null +++ b/app/views/notes/form.html.haml @@ -0,0 +1,89 @@ +%h1= (@note.new_record?) ? 'Create note' : 'Edit note' + +- if @note.errors.any? + #error_explanation + %h2= pluralize(@note.errors.count, t(:error)).html_safe + t(:_prohibited_this_note_from_being_saved) + %ul + - @note.errors.full_messages.each do |msg| + %li= msg + +.elem.generic_form + = form_for @note, as: :note, url: "/notes/#{@note.id}" do |f| + .elem + .labels + = f.label :value + = f.text_field :value, class: "form-control" + + = f.hidden_field :user_id, "data-bind" => "value: userId" + + .elem + .labels + = label_tag :user_email + = text_field_tag :user_email, f.object.user.nil? ? "" : f.object.user.email, "data-bind" => "value: email, valueUpdate: 'afterkeydown'", class: "form-control" + + .results.well{ "data-bind" => "visible: showUserResults" } + %h5 + Please select from the list below (If the user is not found, then no user will be set) + %span{ "data-bind" => "template: { name: 'users', foreach: emailList }" } + + %script{ "id" => "users", "type" => "text/html" } + .clickable{ "data-bind" => "text: email, click: $parent.selectUser" } + + .btn-group.submit_field + - if @note.new_record? + = f.submit 'Create', class: 'btn btn-success' + - else + = f.submit 'Update', class: 'btn btn-success' + = link_to 'Show', @note, { class: 'btn btn-default' } + + = link_to 'Back', notes_path, { class: 'btn btn-default' } + +:javascript + + var initial_user = $('#user_email').val(), + initial_user_id = $('#note_user_id').val(), + viewModel = { email: ko.observable(initial_user || ''), + userId: ko.observable(initial_user_id || ''), + showUserResults: ko.observable(false), + emailList: ko.observableArray() }; + + viewModel.email.subscribe(function (partial) { + if (partial === '') { + viewModel.userId(''); + } else { + var posting = {partial_email: ko.unwrap(partial)}; + $.ajax({ + url: '#{search_users_path}', + dataType: 'json', + method: 'post', + data: posting, + success: function (results) { + // If the only result is the partial, then hide results + if (results.length === 1 && results[0].email === partial) { + viewModel.showUserResults(false); + } else { + viewModel.showUserResults(true); + } + viewModel.emailList(results); + }, + error: function (jqXHR, textStatus, errorThrown) { + alert(jqXHR.responseText); + } + }); + } + }); + + viewModel.selectUser = function (data) { + viewModel.email(data.email); + }; + + //Before form submits check if the user id can be updated + $('#edit_note').submit(function () { + if ($('.results .clickable:hidden').length === 1) { + if (viewModel.emailList()[0].email === viewModel.email()) { + viewModel.userId(viewModel.emailList()[0].id); + } + } + }); + + ko.applyBindings(viewModel); \ No newline at end of file diff --git a/app/views/notes/index.html.haml b/app/views/notes/index.html.haml new file mode 100644 index 0000000..6686bec --- /dev/null +++ b/app/views/notes/index.html.haml @@ -0,0 +1,25 @@ +%h1 Listing Notes + +%br/ + +.elem + = form_tag notes_path, method: :get, id: "note_search" do + = text_field_tag :search, params[:search], placeholder: "Search notes" + = submit_tag "Search", name: nil + +- unless @notes.empty? + .elem{ id: 'notes_table' } + = render partial: 'list' +- else + .elem + - if params[:search].blank? + No notes exist + - else + No notes match your search criteria + + +%br/ +%br/ + +.white_link.small_margin + = link_to "New note", new_note_path, class: 'btn btn-default' diff --git a/app/views/notes/index.json.jbuilder b/app/views/notes/index.json.jbuilder new file mode 100644 index 0000000..a7f871c --- /dev/null +++ b/app/views/notes/index.json.jbuilder @@ -0,0 +1,4 @@ +json.array!(@notes) do |note| + json.extract! note, :value, :user_id + json.url note_url(note, format: :json) +end diff --git a/app/views/notes/search_users.json.jbuilder b/app/views/notes/search_users.json.jbuilder new file mode 100644 index 0000000..d1d43a3 --- /dev/null +++ b/app/views/notes/search_users.json.jbuilder @@ -0,0 +1,3 @@ +json.array!(@users) do |user| + json.extract! user, :id, :email +end diff --git a/app/views/notes/show.html.haml b/app/views/notes/show.html.haml new file mode 100644 index 0000000..4365de7 --- /dev/null +++ b/app/views/notes/show.html.haml @@ -0,0 +1,13 @@ +.elem.small_margin.btn-group + = link_to 'Edit', edit_note_path(@note), { class: 'btn btn-default' } + = link_to 'Back', notes_path, { class: 'btn btn-default' } + +.elem + .label + Note: + = @note.value +- unless @note.user.nil? + .elem + .label + User: + = @note.user.email \ No newline at end of file diff --git a/app/views/notes/show.json.jbuilder b/app/views/notes/show.json.jbuilder new file mode 100644 index 0000000..d98bdeb --- /dev/null +++ b/app/views/notes/show.json.jbuilder @@ -0,0 +1 @@ +json.extract! @note, :value, :user_id, :created_at, :updated_at diff --git a/app/views/products/index.html.haml b/app/views/products/index.html.haml index adc8147..a0722c9 100644 --- a/app/views/products/index.html.haml +++ b/app/views/products/index.html.haml @@ -23,21 +23,3 @@ .white_link.small_margin = link_to "New Product", new_product_path, class: 'btn btn-default' - - -:javascript - // Add click event to each of the headers of the products table, and pagination - $(function () { - $('#products_table th a, #products_table .pagination a').on('click', ↵ - function () { - $.getScript(this.href); - return false; - } - ); - - // Search form. - $('#product_search').submit(function () { - $.get(this.action, $(this).serialize(), null, 'script'); - return false; - }); - }); \ No newline at end of file diff --git a/config/environments/development.rb b/config/environments/development.rb index fbd2468..90e0c6d 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -13,6 +13,8 @@ config.consider_all_requests_local = true config.action_controller.perform_caching = false + config.action_mailer.default_url_options = { :host => "http://localhost:3000" } + # Don't care if the mailer can't send. config.action_mailer.raise_delivery_errors = false diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 848d9a6..3667272 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -10,7 +10,7 @@ # Configure the e-mail address which will be shown in Devise::Mailer, # note that it will be overwritten if you use your own mailer class # with default "from" parameter. - config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com' + config.mailer_sender = 'adamdboudreau@hotmail.co.uk' # Configure the class responsible to send e-mails. # config.mailer = 'Devise::Mailer' diff --git a/config/routes.rb b/config/routes.rb index ab0efd8..de54fe4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,4 +1,7 @@ Carbuncle::Application.routes.draw do + resources :notes + post 'search_users' => 'notes#search_users' + resources :products get 'fader' => 'paintings#fader' delete 'image_batch_destroy' => 'paintings#batch_destroy' diff --git a/db/development.sqlite3 b/db/development.sqlite3 index e180fc6..01e83d3 100644 Binary files a/db/development.sqlite3 and b/db/development.sqlite3 differ diff --git a/db/migrate/20140911002456_create_notes.rb b/db/migrate/20140911002456_create_notes.rb new file mode 100644 index 0000000..1fb8d0f --- /dev/null +++ b/db/migrate/20140911002456_create_notes.rb @@ -0,0 +1,10 @@ +class CreateNotes < ActiveRecord::Migration + def change + create_table :notes do |t| + t.string :value + t.integer :user_id + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index fba4d5e..a3d565f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,14 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20131210005441) do +ActiveRecord::Schema.define(version: 20140911002456) do + + create_table "notes", force: true do |t| + t.string "value" + t.integer "user_id" + t.datetime "created_at" + t.datetime "updated_at" + end create_table "paintings", force: true do |t| t.string "image" diff --git a/db/test.sqlite3 b/db/test.sqlite3 index 969ce1d..b56b46d 100644 Binary files a/db/test.sqlite3 and b/db/test.sqlite3 differ diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb new file mode 100644 index 0000000..068aa65 --- /dev/null +++ b/spec/factories/notes.rb @@ -0,0 +1,5 @@ +FactoryGirl.define do + factory :note do + value "Test note" + end +end \ No newline at end of file diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb new file mode 100644 index 0000000..64cecc9 --- /dev/null +++ b/spec/models/note_spec.rb @@ -0,0 +1,72 @@ +require 'spec_helper' + +describe Note do + it { should validate_presence_of :value } + + describe ".similar_notes" do + let(:note) { FactoryGirl.create(:note) } + it "should return all notes when given nil" do + Note.similar_notes(nil).should == [note] + end + + it "should return notes with the partial note given" do + Note.similar_notes("est").should == [note] + end + + it "should not return notes that don't match the partial given" do + Note.similar_notes("Some note that is different from the note factory").should be_empty + end + end + + describe ".ordered_by" do + let(:notes) do + n1 = n2 = nil + + Timecop.freeze(2008, 7, 1, 12, 0, 0) do + n1 = FactoryGirl.create(:note) + end + + Timecop.freeze(2008, 7, 1, 12, 30, 0) do + n2 = FactoryGirl.create(:note) + end + n3 = FactoryGirl.create(:note) + + [n1, n2, n3] + end + + let(:alpha_notes) do + [FactoryGirl.create(:note, value: 'All your base are belong to us!'), FactoryGirl.create(:note, value: 'I FEEL you mon!'), FactoryGirl.create(:note, value: 'Vi sitter i ventrilo och spelar DotA')] + end + + context "value" do + it "should sort by note values" do + Note.ordered_by(:value, :asc).should == alpha_notes + end + end + + context "created" do + it "should sort by created time" do + Note.ordered_by(:created, :asc).should == notes + end + end + + context "updated" do + it "should sort by updated time" do + Note.ordered_by(:updated, :asc).should == notes + end + end + + context "email" do + let(:u1) { FactoryGirl.create(:user, email: 'acdc@test.com') } + let(:u2) { FactoryGirl.create(:user, email: 'zztop@test.com') } + before do + u1.notes << notes[1] + u2.notes << notes[2] + end + + it "should sort by users associated with the notes" do + Note.ordered_by(:email, :asc).should == notes + end + end + end +end \ No newline at end of file