diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 788352b..b951e4c 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -16,3 +16,6 @@ *= require_self *= require purecss */ +body { + padding: 30px; +} diff --git a/app/assets/stylesheets/forms.css.scss b/app/assets/stylesheets/forms.css.scss new file mode 100644 index 0000000..5a6d32d --- /dev/null +++ b/app/assets/stylesheets/forms.css.scss @@ -0,0 +1,22 @@ +.error_panel { + margin-bottom: 1em; + border-radius: 3px; + border: 1px solid #ebccd1; + .error_header { + .error_title { + font-size: 16px; + margin-top: 0; + margin-bottom: 0; + } + padding: 10px 15px; + color: #a94442; + background-color: #f2dede; + } + .error_body { + padding: 15px; + ul { + margin-bottom: 0px; + margin-top: 0px; + } + } +} diff --git a/app/assets/stylesheets/purecss.css b/app/assets/stylesheets/purecss.css index a8615b6..f6647f5 100644 --- a/app/assets/stylesheets/purecss.css +++ b/app/assets/stylesheets/purecss.css @@ -7,3 +7,39 @@ =require purecss/menus =require purecss/tables */ + +/* Alerts */ +.pure-alert { + position: relative; + margin-bottom: 1em; + padding: 1em; + background: #ccc; + border-radius: 3px; +} + +.pure-alert label { + display: inline-block; + *display: inline; + /* IE7 inline-block hack */ + *zoom: 1; + white-space: nowrap; +} + +.pure-alert { + background-color: rgb(209, 235, 238); + color: rgb(102, 131, 145); +} +.pure-alert-error { + background-color: #D13C38; + color: #fff; +} + +.pure-alert-warning { + background-color: rgb(250, 191, 103); + color: rgb(151, 96, 13); +} + +.pure-alert-success { + background-color: rgb(83, 180, 79); + color: #fff; +} diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index dd697e4..c7cc410 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,29 +1,23 @@ class ApplicationController < ActionController::Base # Prevent CSRF attacks by raising an exception. # For APIs, you may want to use :null_session instead. - protect_from_forgery with: :null_session + protect_from_forgery with: :exception rescue_from CanCan::AccessDenied do |exception| redirect_to root_url, alert: exception.message end + def authenticate_user_or_client! + current_user || current_client || head(:unauthorized) + end + def current_client - @current_client ||= identify_client + @current_client ||= Client.find_by key: request.headers["X-API-KEY"] end def current_ability - if current_user - @current_ability ||= Ability.new(current_user) - elsif current_client - @current_ability ||= ClientAbility.new(current_account) - end + @current_ability ||= + current_client.try { |c| ClientAbility.new(c) } || + Ability.new(current_user) end - - private - - def identify_client - key = request.headers["X-API-KEY"] - Client.find_by key: key if key - end - end diff --git a/app/controllers/transactions_controller.rb b/app/controllers/transactions_controller.rb index 9e3eb55..7b5626d 100644 --- a/app/controllers/transactions_controller.rb +++ b/app/controllers/transactions_controller.rb @@ -1,4 +1,10 @@ class TransactionsController < ApplicationController + load_and_authorize_resource + skip_before_filter :verify_authenticity_token, only: :create + + before_action :authenticate_user!, except: :create + before_action :authenticate_user_or_client!, only: :create + def index @transactions = Transaction.all end @@ -8,19 +14,35 @@ class TransactionsController < ApplicationController end def create - @transaction = current_user.outgoing_transactions.build( - transaction_params.merge(origin: I18n.t('origin.created_by_user'))) + @transaction = Transaction.new(transaction_params) + respond_to do |format| + format.html do + if @transaction.save + flash[:success] = "Transaction created" + redirect_to new_transaction_path + else + render 'new' + end + end - if @transaction.save - redirect_to current_user - else - render 'new' + format.json do + head(@transaction.save ? :created : :unprocessable_entity) + end end end private def transaction_params - params.require(:transaction).permit(:creditor_id, :amount, :message) + t = params.require(:transaction) + .permit(:debtor, :creditor, :message, :euros, :cents) + + { + debtor: User.find_by(name: t[:debtor]) || User.zeus, + creditor: User.find_by(name: t[:creditor]) || User.zeus, + issuer: current_client || current_user, + amount: (t[:euros].to_f*100 + t[:cents].to_f).to_i, + message: t[:message] + } end end diff --git a/app/models/ability.rb b/app/models/ability.rb index f04be4f..c3a4c81 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -2,12 +2,10 @@ class Ability include CanCan::Ability def initialize(user) - user ||= User.new # guest user (not logged in) + return unless user - if user.penning? - can :manage, :all - else - can :read, user, id: user.id - end + can :manage, :all if user.penning? + can :read, user, id: user.id + can :create, Transaction, debtor: user end end diff --git a/app/models/client.rb b/app/models/client.rb index 842921b..d8bd7da 100644 --- a/app/models/client.rb +++ b/app/models/client.rb @@ -14,7 +14,6 @@ class Client < ActiveRecord::Base before_create :generate_key validates :name, presence: true, uniqueness: true - validates :key, presence: true, uniqueness: true private def generate_key diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 41f8d44..716c9b4 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -7,5 +7,7 @@ = javascript_include_tag 'application', 'data-turbolinks-track' => true = csrf_meta_tags %body - = content_tag :div, flash[:alert] if flash[:alert] + - flash.each do |key, value| + .pure-alert{ class: "pure-alert-#{key}" } + = value = yield diff --git a/app/views/partials/_form_errors.html.haml b/app/views/partials/_form_errors.html.haml new file mode 100644 index 0000000..46a401d --- /dev/null +++ b/app/views/partials/_form_errors.html.haml @@ -0,0 +1,10 @@ +- if object.errors.any? + .error_panel + .error_header + %h3.error_title + This transaction could not be saved. + .error_body + %ul + - object.errors.full_messages.each do |tr| + %li + = tr diff --git a/app/views/transactions/new.html.haml b/app/views/transactions/new.html.haml index 68d35f0..91f158f 100644 --- a/app/views/transactions/new.html.haml +++ b/app/views/transactions/new.html.haml @@ -1,6 +1,11 @@ -= @transaction.errors.full_messages.join(", ") += render 'partials/form_errors', object: @transaction = simple_form_for @transaction do |f| - = f.collection_select :creditor_id, User.all, :id, :name, {}, { class: 'select2-selector' } + - if current_user.penning + = f.collection_select :debtor, User.all, :name, :name, {}, { class: 'select2-selector' } + - else + = f.hidden_field :debtor, value: current_user.name + = f.collection_select :creditor, User.all, :name, :name, {}, { class: 'select2-selector' } = f.input :amount = f.input :message, required: true - = f.submit "Send it!" + .pure-controls + = f.button :submit, "Send it!" diff --git a/config/initializers/simple_form.rb b/config/initializers/simple_form.rb new file mode 100644 index 0000000..bdf2171 --- /dev/null +++ b/config/initializers/simple_form.rb @@ -0,0 +1,165 @@ +# Use this setup block to configure all options available in SimpleForm. +SimpleForm.setup do |config| + # Wrappers are used by the form builder to generate a + # complete input. You can remove any component from the + # wrapper, change the order or even add your own to the + # stack. The options given below are used to wrap the + # whole input. + config.wrappers :default, class: "input pure-control-group", + hint_class: :field_with_hint, error_class: :field_with_errors do |b| + ## Extensions enabled by default + # Any of these extensions can be disabled for a + # given input by passing: `f.input EXTENSION_NAME => false`. + # You can make any of these extensions optional by + # renaming `b.use` to `b.optional`. + + # Determines whether to use HTML5 (:email, :url, ...) + # and required attributes + b.use :html5 + + # Calculates placeholders automatically from I18n + # You can also pass a string as f.input placeholder: "Placeholder" + b.use :placeholder + + ## Optional extensions + # They are disabled unless you pass `f.input EXTENSION_NAME => true` + # to the input. If so, they will retrieve the values from the model + # if any exists. If you want to enable any of those + # extensions by default, you can change `b.optional` to `b.use`. + + # Calculates maxlength from length validations for string inputs + b.optional :maxlength + + # Calculates pattern from format validations for string inputs + b.optional :pattern + + # Calculates min and max from length validations for numeric inputs + b.optional :min_max + + # Calculates readonly automatically from readonly attributes + b.optional :readonly + + ## Inputs + b.use :label_input + b.use :hint, wrap_with: { tag: :span, class: :hint } + b.use :error, wrap_with: { tag: :span, class: :error } + + ## full_messages_for + # If you want to display the full error message for the attribute, you can + # use the component :full_error, like: + # + # b.use :full_error, wrap_with: { tag: :span, class: :error } + end + + # The default wrapper to be used by the FormBuilder. + config.default_wrapper = :default + + # Define the way to render check boxes / radio buttons with labels. + # Defaults to :nested for bootstrap config. + # inline: input + label + # nested: label > input + config.boolean_style = :nested + + # Default class for buttons + config.button_class = 'pure-button pure-button-primary' + + # Method used to tidy up errors. Specify any Rails Array method. + # :first lists the first message for each field. + # Use :to_sentence to list all errors for each field. + # config.error_method = :first + + # Default tag used for error notification helper. + config.error_notification_tag = :div + + # CSS class to add for error notification helper. + config.error_notification_class = 'error_notification' + + # ID to add for error notification helper. + # config.error_notification_id = nil + + # Series of attempts to detect a default label method for collection. + # config.collection_label_methods = [ :to_label, :name, :title, :to_s ] + + # Series of attempts to detect a default value method for collection. + # config.collection_value_methods = [ :id, :to_s ] + + # You can wrap a collection of radio/check boxes in a pre-defined tag, defaulting to none. + # config.collection_wrapper_tag = nil + + # You can define the class to use on all collection wrappers. Defaulting to none. + # config.collection_wrapper_class = nil + + # You can wrap each item in a collection of radio/check boxes with a tag, + # defaulting to :span. + # config.item_wrapper_tag = :span + + # You can define a class to use in all item wrappers. Defaulting to none. + # config.item_wrapper_class = nil + + # How the label text should be generated altogether with the required text. + # config.label_text = lambda { |label, required, explicit_label| "#{required} #{label}" } + + # You can define the class to use on all labels. Default is nil. + # config.label_class = nil + + # You can define the default class to be used on forms. Can be overriden + # with `html: { :class }`. Defaulting to none. + config.default_form_class = "pure-form" + + # You can define which elements should obtain additional classes + # config.generate_additional_classes_for = [:wrapper, :label, :input] + + # Whether attributes are required by default (or not). Default is true. + # config.required_by_default = true + + # Tell browsers whether to use the native HTML5 validations (novalidate form option). + # These validations are enabled in SimpleForm's internal config but disabled by default + # in this configuration, which is recommended due to some quirks from different browsers. + # To stop SimpleForm from generating the novalidate option, enabling the HTML5 validations, + # change this configuration to true. + config.browser_validations = false + + # Collection of methods to detect if a file type was given. + # config.file_methods = [ :mounted_as, :file?, :public_filename ] + + # Custom mappings for input types. This should be a hash containing a regexp + # to match as key, and the input type that will be used when the field name + # matches the regexp as value. + # config.input_mappings = { /count/ => :integer } + + # Custom wrappers for input types. This should be a hash containing an input + # type as key and the wrapper that will be used for all inputs with specified type. + # config.wrapper_mappings = { string: :prepend } + + # Namespaces where SimpleForm should look for custom input classes that + # override default inputs. + # config.custom_inputs_namespaces << "CustomInputs" + + # Default priority for time_zone inputs. + # config.time_zone_priority = nil + + # Default priority for country inputs. + # config.country_priority = nil + + # When false, do not use translations for labels. + # config.translate_labels = true + + # Automatically discover new inputs in Rails' autoload path. + # config.inputs_discovery = true + + # Cache SimpleForm inputs discovery + # config.cache_discovery = !Rails.env.development? + + # Default class for inputs + # config.input_class = nil + + # Define the default class of the input wrapper of the boolean input. + config.boolean_label_class = 'checkbox' + + # Defines if the default input wrapper class should be included in radio + # collection wrappers. + # config.include_default_input_wrapper_class = true + + # Defines which i18n scope will be used in Simple Form. + # config.i18n_scope = 'simple_form' +end diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml new file mode 100644 index 0000000..2374383 --- /dev/null +++ b/config/locales/simple_form.en.yml @@ -0,0 +1,31 @@ +en: + simple_form: + "yes": 'Yes' + "no": 'No' + required: + text: 'required' + mark: '*' + # You can uncomment the line below if you need to overwrite the whole required html. + # When using html, text and mark won't be used. + # html: '*' + error_notification: + default_message: "Please review the problems below:" + # Examples + # labels: + # defaults: + # password: 'Password' + # user: + # new: + # email: 'E-mail to sign in.' + # edit: + # email: 'E-mail.' + # hints: + # defaults: + # username: 'User name to sign in.' + # password: 'No special characters, please.' + # include_blanks: + # defaults: + # age: 'Rather not say' + # prompts: + # defaults: + # age: 'Select your age' diff --git a/lib/templates/haml/scaffold/_form.html.haml b/lib/templates/haml/scaffold/_form.html.haml new file mode 100644 index 0000000..705b47a --- /dev/null +++ b/lib/templates/haml/scaffold/_form.html.haml @@ -0,0 +1,10 @@ += simple_form_for(@<%= singular_table_name %>) do |f| + = f.error_notification + + .form-inputs + <%- attributes.each do |attribute| -%> + = f.<%= attribute.reference? ? :association : :input %> :<%= attribute.name %> + <%- end -%> + + .pure-controlls + = f.button :submit diff --git a/spec/controllers/transactions_controller_spec.rb b/spec/controllers/transactions_controller_spec.rb index c95f401..e96cc51 100644 --- a/spec/controllers/transactions_controller_spec.rb +++ b/spec/controllers/transactions_controller_spec.rb @@ -1,5 +1,78 @@ require 'rails_helper' +require 'spec_helper' RSpec.describe TransactionsController, type: :controller do + describe "creating transaction" do + before :each do + @debtor = create(:user) + @creditor = create(:user) + sign_in @debtor + end + context "with valid attributes" do + before :each do + @attributes = { transaction: { + debtor: @debtor.name, + creditor: @creditor.name, + cents: 70, + message: 'hoi' + }} + post :create, @attributes + @transaction = Transaction.last + end + + it "should create a new transaction" do + expect {post :create, @attributes}.to change {Transaction.count}.by(1) + end + + it "should set debtor" do + expect(@transaction.debtor).to eq(@debtor) + end + + it "should set amount" do + expect(@transaction.amount).to eq(70) + end + + it "should set creditor" do + expect(@transaction.creditor).to eq(@creditor) + end + + it "should set issuer" do + expect(@transaction.issuer).to eq(@debtor) + end + end + + context "with float euros" do + it "should set correct amount" do + post :create, transaction: { + debtor: @debtor.name, + creditor: @creditor.name, + euros: 10.5, + message: "Omdat je een leuke jongen bent!" + } + expect(Transaction.last.amount).to eq(1050) + end + end + + context "with negative amount" do + it "should be refused" do + expect do + post :create, transaction: attributes_for(:transaction, cents: -20) + end.not_to change {Transaction.count} + end + end + + context "for other user" do + it "should be refused" do + expect do + post :create, transaction: { + debtor: @creditor.name, + creditor: @debtor.name, + euros: 10000000, + message: 'DIT IS OVERVAL' + } + end.not_to change {Transaction.count} + end + end + end end diff --git a/spec/helpers/transactions_helper_spec.rb b/spec/helpers/transactions_helper_spec.rb index f29b78f..48c6c73 100644 --- a/spec/helpers/transactions_helper_spec.rb +++ b/spec/helpers/transactions_helper_spec.rb @@ -11,5 +11,4 @@ require 'rails_helper' # end # end RSpec.describe TransactionsHelper, type: :helper do - pending "add some examples to (or delete) #{__FILE__}" end diff --git a/spec/helpers/users_helper_spec.rb b/spec/helpers/users_helper_spec.rb index b2e3444..890768c 100644 --- a/spec/helpers/users_helper_spec.rb +++ b/spec/helpers/users_helper_spec.rb @@ -11,5 +11,4 @@ require 'rails_helper' # end # end RSpec.describe UsersHelper, type: :helper do - pending "add some examples to (or delete) #{__FILE__}" end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index cc47a08..831f776 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -21,8 +21,10 @@ Coveralls.wear!('rails') # # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration require 'factory_girl' +require 'devise' RSpec.configure do |config| config.include FactoryGirl::Syntax::Methods + config.include Devise::TestHelpers, type: :controller # rspec-expectations config goes here. You can use an alternate # assertion/expectation library such as wrong or the stdlib/minitest # assertions if you prefer.