├── spec
├── spec.opts
├── matchers.rb
├── extensions
│ ├── string_extensions_spec.rb
│ ├── float_extensions_spec.rb
│ └── date_extensions_spec.rb
├── factory_patch.rb
├── mailer_helper.rb
├── models
│ ├── duration_spec.rb
│ ├── role_spec.rb
│ ├── invoice_spec.rb
│ ├── activity_custom_property_spec.rb
│ ├── setting_spec.rb
│ ├── free_day_spec.rb
│ ├── client_spec.rb
│ ├── purse_spec.rb
│ ├── activity_custom_property_value_spec.rb
│ └── role_activities_in_project_summary_spec.rb
├── controllers
│ ├── home_spec.rb
│ ├── settings_spec.rb
│ ├── roles_spec.rb
│ └── activity_custom_properties_spec.rb
├── model_helper.rb
├── rubytime_specs_helper.rb
├── controller_helper.rb
└── spec_helper.rb
├── autotest
└── discover.rb
├── app
├── views
│ ├── activities
│ │ ├── new.html.erb
│ │ ├── edit.html.erb
│ │ ├── update.html.erb
│ │ ├── _filter.html.erb
│ │ ├── _activity.html.erb
│ │ ├── _grouped_activities.html.erb
│ │ ├── _grouped_clients.html.erb
│ │ ├── _grouped_roles.html.erb
│ │ ├── _activity_details.html.erb
│ │ ├── _criteria_group.html.erb
│ │ ├── day.html.erb
│ │ ├── calendar.html.erb
│ │ ├── _roles_summary_by_project.html.erb
│ │ ├── index.html.erb
│ │ ├── _grouped_projects.html.erb
│ │ ├── _invoice_forms.html.erb
│ │ ├── _filter_form.html.erb
│ │ └── _activity_form.html.erb
│ ├── invoices
│ │ ├── edit.html.erb
│ │ ├── _new_invoice.html.erb
│ │ ├── _edit.html.erb
│ │ ├── show.html.erb
│ │ └── index.html.erb
│ ├── currencies
│ │ ├── edit.html.erb
│ │ ├── _new_currency.html.erb
│ │ ├── _currency_form.html.erb
│ │ └── index.html.erb
│ ├── users
│ │ ├── index.html.erb
│ │ ├── request_password.html.erb
│ │ ├── _users.html.erb
│ │ ├── show.html.erb
│ │ ├── edit.html.erb
│ │ ├── _user_form.html.erb
│ │ └── settings.html.erb
│ ├── layout
│ │ ├── _session_info.html.erb
│ │ ├── _navigation.html.erb
│ │ └── application.html.erb
│ ├── clients
│ │ ├── index.html.erb
│ │ ├── _client.html.erb
│ │ ├── edit.html.erb
│ │ ├── _new_client.html.erb
│ │ └── show.html.erb
│ ├── activity_custom_properties
│ │ ├── _fields.html.erb
│ │ ├── edit.html.erb
│ │ ├── _new_activity_custom_property.html.erb
│ │ └── index.html.erb
│ ├── roles
│ │ ├── edit.html.erb
│ │ ├── _new_role.html.erb
│ │ └── index.html.erb
│ ├── activity_types
│ │ ├── _new_activity_type.html.erb
│ │ ├── edit.html.erb
│ │ ├── show.html.erb
│ │ └── index.html.erb
│ ├── projects
│ │ ├── _new_project.html.erb
│ │ ├── index.html.erb
│ │ ├── edit.html.erb
│ │ ├── show.html.erb
│ │ └── _hourly_rates.html.erb
│ ├── settings
│ │ └── edit.html.erb
│ └── exceptions
│ │ ├── not_found.html.erb
│ │ └── not_acceptable.html.erb
├── helpers
│ ├── roles_helper.rb
│ ├── clients_helper.rb
│ ├── currencies_helper.rb
│ ├── free_days_helper.rb
│ ├── settings_helper.rb
│ ├── hourly_rates_helper.rb
│ ├── global_helpers.rb
│ ├── invoices_helper.rb
│ ├── activity_types_helper.rb
│ ├── projects_helper.rb
│ └── users_helper.rb
├── models
│ ├── client_user.rb
│ ├── project_activity_type.rb
│ ├── duration.rb
│ ├── observers
│ │ ├── hourly_rate_observer.rb
│ │ ├── activity_observer.rb
│ │ └── user_observer.rb
│ ├── role.rb
│ ├── purse.rb
│ ├── role_activities_in_project_summary.rb
│ ├── activity_custom_property.rb
│ ├── activity_custom_property_value.rb
│ ├── setting.rb
│ ├── money.rb
│ ├── client.rb
│ ├── user_version.rb
│ ├── activity_type.rb
│ ├── currency.rb
│ ├── free_day.rb
│ ├── invoice.rb
│ ├── hourly_rate_log.rb
│ ├── project.rb
│ └── hourly_rate.rb
├── mailers
│ ├── views
│ │ └── user_mailer
│ │ │ ├── welcome.text.erb
│ │ │ ├── timesheet_nagger.text.erb
│ │ │ ├── notice.text.erb
│ │ │ ├── password_reset_link.text.erb
│ │ │ ├── timesheet_changes_notifier.text.erb
│ │ │ ├── timesheet_reporter.text.erb
│ │ │ └── timesheet_summary.text.erb
│ └── user_mailer.rb
└── controllers
│ ├── home.rb
│ ├── settings.rb
│ ├── exceptions.rb
│ ├── roles.rb
│ ├── free_days.rb
│ ├── currencies.rb
│ ├── clients.rb
│ ├── activity_custom_properties.rb
│ ├── hourly_rates.rb
│ ├── application.rb
│ └── invoices.rb
├── public
├── favicon.ico
├── favicon.png
├── javascripts
│ ├── sessions.js
│ ├── roles.js
│ ├── lib
│ │ ├── rubytime
│ │ │ └── input-filler.js
│ │ └── jquery-scrollto.js
│ ├── invoices.js
│ ├── activity_types.js
│ ├── indicator.js
│ ├── clients.js
│ └── users.js
├── images
│ ├── bottom.png
│ ├── button.png
│ ├── header.png
│ ├── logo.png
│ ├── merb.jpg
│ ├── ajax-loader.gif
│ ├── icons
│ │ ├── add.png
│ │ ├── clock.png
│ │ ├── cross.png
│ │ ├── minus.png
│ │ ├── plus.png
│ │ ├── role.png
│ │ ├── client.png
│ │ ├── pencil.png
│ │ ├── project.png
│ │ ├── calendar.png
│ │ ├── magnifier.png
│ │ ├── plus_small.png
│ │ ├── cross_small.png
│ │ ├── minus_circle.png
│ │ ├── pencil_small.png
│ │ ├── tick_circle.png
│ │ ├── working_day.png
│ │ ├── calendar_month.png
│ │ ├── notebook_minus.png
│ │ ├── notebook_plus.png
│ │ ├── wrench_pencil.png
│ │ └── calendar-day-off.png
│ ├── macFFBgHack.png
│ ├── tmp
│ │ ├── header.png
│ │ └── menubar.png
│ ├── input-text-bg.png
│ ├── menu-selected.png
│ ├── loadingAnimation.gif
│ ├── powered_by_llp.png
│ ├── powered_by_merb.png
│ ├── lunar-logic-polska.png
│ ├── menu-floor-2-selected.png
│ ├── activity-calendar-mask.png
│ ├── add-activity-button-bg.png
│ ├── ajax-loader-transparent.gif
│ ├── menu-selected-indicator.gif
│ ├── menu-selected-indicator.png
│ └── jquery-ui-cupertino
│ │ ├── ui-icons_2694e8_256x240.png
│ │ ├── ui-icons_2e83ff_256x240.png
│ │ ├── ui-icons_3d80b3_256x240.png
│ │ ├── ui-icons_72a7cf_256x240.png
│ │ ├── ui-icons_ffffff_256x240.png
│ │ ├── ui-bg_flat_15_cd0a0a_40x100.png
│ │ ├── ui-bg_glass_100_e4f1fb_1x400.png
│ │ ├── ui-bg_glass_50_3baae3_1x400.png
│ │ ├── ui-bg_glass_80_d7ebf9_1x400.png
│ │ ├── ui-bg_highlight-hard_70_000000_1x100.png
│ │ ├── ui-bg_highlight-soft_25_ffef8f_1x100.png
│ │ ├── ui-bg_diagonals-thick_90_eeeeee_40x40.png
│ │ ├── ui-bg_highlight-hard_100_f2f5f7_1x100.png
│ │ └── ui-bg_highlight-soft_100_deedf7_1x100.png
└── stylesheets
│ ├── reset.css
│ ├── master.css
│ └── hourly_rates.css
├── config
├── ldap.yml.example
├── rcov_stats.yml.example
├── local_config.rb.example
├── environments
│ ├── production.rb
│ ├── staging.rb
│ ├── rake.rb
│ ├── test.rb
│ └── development.rb
├── database.yml.example
├── schedule.rb.example
├── rack.rb
├── init.rb
├── deploy.rb.example
└── router.rb
├── lib
├── extensions
│ ├── float_extensions.rb
│ ├── string_extensions.rb
│ └── date_extensions.rb
├── tasks
│ ├── import_data.rake
│ ├── metric.rake
│ ├── cron.rake
│ └── install.rake
├── auth
│ └── ldap.rb
└── rubytime
│ └── misc.rb
├── merb
├── session
│ └── session.rb
└── merb-auth
│ ├── strategies.rb
│ └── setup.rb
├── slices
└── merb-auth-slice-password
│ └── app
│ ├── controllers
│ └── sessions.rb
│ └── views
│ ├── layout
│ └── merb-auth-slice-password.html.erb
│ └── exceptions
│ └── unauthenticated.html.erb
├── .gitignore
├── TODO.txt
├── config.ru
├── CHANGELOG-API.txt
├── Rakefile
└── Gemfile
/spec/spec.opts:
--------------------------------------------------------------------------------
1 | -c --format specdoc
2 |
--------------------------------------------------------------------------------
/autotest/discover.rb:
--------------------------------------------------------------------------------
1 | Autotest.add_discovery { "merb" }
--------------------------------------------------------------------------------
/app/views/activities/new.html.erb:
--------------------------------------------------------------------------------
1 | <%= partial(:activity_form) %>
--------------------------------------------------------------------------------
/app/views/activities/edit.html.erb:
--------------------------------------------------------------------------------
1 | <%= partial(:activity_form) %>
--------------------------------------------------------------------------------
/app/helpers/roles_helper.rb:
--------------------------------------------------------------------------------
1 | module Merb
2 | module RolesHelper
3 |
4 | end
5 | end # Merb
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/favicon.png
--------------------------------------------------------------------------------
/public/javascripts/sessions.js:
--------------------------------------------------------------------------------
1 | $(function() {
2 | $("form:first").focusFirstBlank();
3 | });
--------------------------------------------------------------------------------
/app/helpers/clients_helper.rb:
--------------------------------------------------------------------------------
1 | module Merb
2 | module ClientsHelper
3 |
4 | end
5 | end # Merb
--------------------------------------------------------------------------------
/app/views/activities/update.html.erb:
--------------------------------------------------------------------------------
1 | <%= partial :activity, :with => @activity, :as => :activity %>
--------------------------------------------------------------------------------
/app/helpers/currencies_helper.rb:
--------------------------------------------------------------------------------
1 | module Merb
2 | module CurrenciesHelper
3 |
4 | end
5 | end # Merb
--------------------------------------------------------------------------------
/app/helpers/free_days_helper.rb:
--------------------------------------------------------------------------------
1 | module Merb
2 | module FreeDaysHelper
3 |
4 | end
5 | end # Merb
--------------------------------------------------------------------------------
/app/helpers/settings_helper.rb:
--------------------------------------------------------------------------------
1 | module Merb
2 | module SettingsHelper
3 |
4 | end
5 | end # Merb
--------------------------------------------------------------------------------
/public/images/bottom.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/bottom.png
--------------------------------------------------------------------------------
/public/images/button.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/button.png
--------------------------------------------------------------------------------
/public/images/header.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/header.png
--------------------------------------------------------------------------------
/public/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/logo.png
--------------------------------------------------------------------------------
/public/images/merb.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/merb.jpg
--------------------------------------------------------------------------------
/app/helpers/hourly_rates_helper.rb:
--------------------------------------------------------------------------------
1 | module Merb
2 | module HourlyRatesHelper
3 |
4 | end
5 | end # Merb
--------------------------------------------------------------------------------
/public/images/ajax-loader.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/ajax-loader.gif
--------------------------------------------------------------------------------
/public/images/icons/add.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/icons/add.png
--------------------------------------------------------------------------------
/public/images/icons/clock.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/icons/clock.png
--------------------------------------------------------------------------------
/public/images/icons/cross.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/icons/cross.png
--------------------------------------------------------------------------------
/public/images/icons/minus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/icons/minus.png
--------------------------------------------------------------------------------
/public/images/icons/plus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/icons/plus.png
--------------------------------------------------------------------------------
/public/images/icons/role.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/icons/role.png
--------------------------------------------------------------------------------
/public/images/macFFBgHack.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/macFFBgHack.png
--------------------------------------------------------------------------------
/public/images/tmp/header.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/tmp/header.png
--------------------------------------------------------------------------------
/public/images/tmp/menubar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/tmp/menubar.png
--------------------------------------------------------------------------------
/app/views/invoices/edit.html.erb:
--------------------------------------------------------------------------------
1 | <% throw_content :secondary, partial(:edit) if current_user.is_admin? %>
2 |
3 |
--------------------------------------------------------------------------------
/public/images/icons/client.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/icons/client.png
--------------------------------------------------------------------------------
/public/images/icons/pencil.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/icons/pencil.png
--------------------------------------------------------------------------------
/public/images/icons/project.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/icons/project.png
--------------------------------------------------------------------------------
/public/images/input-text-bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/input-text-bg.png
--------------------------------------------------------------------------------
/public/images/menu-selected.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/menu-selected.png
--------------------------------------------------------------------------------
/app/views/currencies/edit.html.erb:
--------------------------------------------------------------------------------
1 |
Edit currency
2 | <%= partial :currency_form, :submit_name => 'Update' %>
3 |
--------------------------------------------------------------------------------
/public/images/icons/calendar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/icons/calendar.png
--------------------------------------------------------------------------------
/public/images/icons/magnifier.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/icons/magnifier.png
--------------------------------------------------------------------------------
/public/images/icons/plus_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/icons/plus_small.png
--------------------------------------------------------------------------------
/public/images/loadingAnimation.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/loadingAnimation.gif
--------------------------------------------------------------------------------
/public/images/powered_by_llp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/powered_by_llp.png
--------------------------------------------------------------------------------
/public/images/powered_by_merb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/powered_by_merb.png
--------------------------------------------------------------------------------
/public/images/icons/cross_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/icons/cross_small.png
--------------------------------------------------------------------------------
/public/images/icons/minus_circle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/icons/minus_circle.png
--------------------------------------------------------------------------------
/public/images/icons/pencil_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/icons/pencil_small.png
--------------------------------------------------------------------------------
/public/images/icons/tick_circle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/icons/tick_circle.png
--------------------------------------------------------------------------------
/public/images/icons/working_day.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/icons/working_day.png
--------------------------------------------------------------------------------
/public/images/lunar-logic-polska.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/lunar-logic-polska.png
--------------------------------------------------------------------------------
/app/views/activities/_filter.html.erb:
--------------------------------------------------------------------------------
1 | Filter
2 |
3 | <%= partial(:filter_form) %>
4 |
--------------------------------------------------------------------------------
/public/images/icons/calendar_month.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/icons/calendar_month.png
--------------------------------------------------------------------------------
/public/images/icons/notebook_minus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/icons/notebook_minus.png
--------------------------------------------------------------------------------
/public/images/icons/notebook_plus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/icons/notebook_plus.png
--------------------------------------------------------------------------------
/public/images/icons/wrench_pencil.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/icons/wrench_pencil.png
--------------------------------------------------------------------------------
/public/images/menu-floor-2-selected.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/menu-floor-2-selected.png
--------------------------------------------------------------------------------
/app/views/currencies/_new_currency.html.erb:
--------------------------------------------------------------------------------
1 | Create new currency
2 | <%= partial :currency_form, :submit_name => 'Create' %>
3 |
--------------------------------------------------------------------------------
/public/images/activity-calendar-mask.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/activity-calendar-mask.png
--------------------------------------------------------------------------------
/public/images/add-activity-button-bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/add-activity-button-bg.png
--------------------------------------------------------------------------------
/public/images/ajax-loader-transparent.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/ajax-loader-transparent.gif
--------------------------------------------------------------------------------
/public/images/icons/calendar-day-off.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/icons/calendar-day-off.png
--------------------------------------------------------------------------------
/public/images/menu-selected-indicator.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/menu-selected-indicator.gif
--------------------------------------------------------------------------------
/public/images/menu-selected-indicator.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/menu-selected-indicator.png
--------------------------------------------------------------------------------
/config/ldap.yml.example:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | :host: ldap.yourcompany.com
4 | :port: 10389
5 | :base: ou=users,dc=yourcompany,dc=com
6 | :attr: uid
7 |
--------------------------------------------------------------------------------
/lib/extensions/float_extensions.rb:
--------------------------------------------------------------------------------
1 | class Float
2 |
3 | def round_to_2_digits
4 | sprintf("%.2f", self).to_f
5 | end
6 |
7 | end
8 |
--------------------------------------------------------------------------------
/app/helpers/global_helpers.rb:
--------------------------------------------------------------------------------
1 | module Merb
2 | module GlobalHelpers
3 | # helpers defined here available to all views.
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/lib/extensions/string_extensions.rb:
--------------------------------------------------------------------------------
1 | class String
2 | def generate_random(length)
3 | Array.new(length) { self[rand(size),1] }.join
4 | end
5 | end
--------------------------------------------------------------------------------
/app/models/client_user.rb:
--------------------------------------------------------------------------------
1 | class ClientUser < User
2 | validates_present :client
3 |
4 | before :valid? do
5 | self.role_id = nil
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/app/views/activities/_activity.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= activity.hours %>
3 | <%= activity.project.name %>
4 |
--------------------------------------------------------------------------------
/public/javascripts/roles.js:
--------------------------------------------------------------------------------
1 | $(function() {
2 | $('#role_form').validate({
3 | rules: {
4 | "role[name]": {
5 | required: true
6 | }
7 | }
8 | });
9 | });
--------------------------------------------------------------------------------
/app/helpers/invoices_helper.rb:
--------------------------------------------------------------------------------
1 | module Merb
2 | module InvoicesHelper
3 | def filter_hash
4 | {:filter => params[:filter]}
5 | end
6 | end
7 | end # Merb
8 |
--------------------------------------------------------------------------------
/public/images/jquery-ui-cupertino/ui-icons_2694e8_256x240.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/jquery-ui-cupertino/ui-icons_2694e8_256x240.png
--------------------------------------------------------------------------------
/public/images/jquery-ui-cupertino/ui-icons_2e83ff_256x240.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/jquery-ui-cupertino/ui-icons_2e83ff_256x240.png
--------------------------------------------------------------------------------
/public/images/jquery-ui-cupertino/ui-icons_3d80b3_256x240.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/jquery-ui-cupertino/ui-icons_3d80b3_256x240.png
--------------------------------------------------------------------------------
/public/images/jquery-ui-cupertino/ui-icons_72a7cf_256x240.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/jquery-ui-cupertino/ui-icons_72a7cf_256x240.png
--------------------------------------------------------------------------------
/public/images/jquery-ui-cupertino/ui-icons_ffffff_256x240.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/jquery-ui-cupertino/ui-icons_ffffff_256x240.png
--------------------------------------------------------------------------------
/spec/matchers.rb:
--------------------------------------------------------------------------------
1 | Spec::Matchers.define :have_errors_on do |attr|
2 | match do |resource|
3 | resource.valid?
4 | not resource.errors.on(attr).nil?
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/public/images/jquery-ui-cupertino/ui-bg_flat_15_cd0a0a_40x100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/jquery-ui-cupertino/ui-bg_flat_15_cd0a0a_40x100.png
--------------------------------------------------------------------------------
/public/images/jquery-ui-cupertino/ui-bg_glass_100_e4f1fb_1x400.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/jquery-ui-cupertino/ui-bg_glass_100_e4f1fb_1x400.png
--------------------------------------------------------------------------------
/public/images/jquery-ui-cupertino/ui-bg_glass_50_3baae3_1x400.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/jquery-ui-cupertino/ui-bg_glass_50_3baae3_1x400.png
--------------------------------------------------------------------------------
/public/images/jquery-ui-cupertino/ui-bg_glass_80_d7ebf9_1x400.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/jquery-ui-cupertino/ui-bg_glass_80_d7ebf9_1x400.png
--------------------------------------------------------------------------------
/merb/session/session.rb:
--------------------------------------------------------------------------------
1 | module Merb
2 | module Session
3 | def abandon!
4 | self.user.forget_me! unless self.user.nil?
5 | authentication.abandon!
6 | end
7 | end
8 | end
--------------------------------------------------------------------------------
/public/images/jquery-ui-cupertino/ui-bg_highlight-hard_70_000000_1x100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/jquery-ui-cupertino/ui-bg_highlight-hard_70_000000_1x100.png
--------------------------------------------------------------------------------
/public/images/jquery-ui-cupertino/ui-bg_highlight-soft_25_ffef8f_1x100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/jquery-ui-cupertino/ui-bg_highlight-soft_25_ffef8f_1x100.png
--------------------------------------------------------------------------------
/public/images/jquery-ui-cupertino/ui-bg_diagonals-thick_90_eeeeee_40x40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/jquery-ui-cupertino/ui-bg_diagonals-thick_90_eeeeee_40x40.png
--------------------------------------------------------------------------------
/public/images/jquery-ui-cupertino/ui-bg_highlight-hard_100_f2f5f7_1x100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/jquery-ui-cupertino/ui-bg_highlight-hard_100_f2f5f7_1x100.png
--------------------------------------------------------------------------------
/public/images/jquery-ui-cupertino/ui-bg_highlight-soft_100_deedf7_1x100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LunarLogic/rubytime/HEAD/public/images/jquery-ui-cupertino/ui-bg_highlight-soft_100_deedf7_1x100.png
--------------------------------------------------------------------------------
/lib/tasks/import_data.rake:
--------------------------------------------------------------------------------
1 | desc "import data from rubytime 2 db"
2 | namespace :rubytime do
3 | task :import_data => :merb_env do
4 | require Merb.root / "lib" / "rubytime" / "legacy"
5 | Rubytime::Legacy::import_data
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/app/helpers/activity_types_helper.rb:
--------------------------------------------------------------------------------
1 | module Merb
2 | module ActivityTypesHelper
3 |
4 | def activity_type_children_summary(activity_type)
5 | activity_type.children.map{|at| at.name}.join(', ').truncate(60)
6 | end
7 | end
8 | end # Merb
--------------------------------------------------------------------------------
/app/mailers/views/user_mailer/welcome.text.erb:
--------------------------------------------------------------------------------
1 | <%= @user.name %>, welcome to Rubytime!
2 |
3 | Your account has been created with following credentials:
4 |
5 | login: <%= @user.login %>
6 | password: <%= @user.password %>
7 |
8 | Rubytime
9 | <%= @url %>
--------------------------------------------------------------------------------
/config/rcov_stats.yml.example:
--------------------------------------------------------------------------------
1 | units_files_to_cover:
2 | - app/models
3 | functionals_files_to_cover:
4 | - app/controllers
5 | - app/helpers
6 | units_files_to_test:
7 | - models
8 | functionals_files_to_test:
9 | - controllers
10 | - helpers
11 |
--------------------------------------------------------------------------------
/app/controllers/home.rb:
--------------------------------------------------------------------------------
1 | class Home < Application
2 |
3 | skip_before :ensure_authenticated
4 |
5 | def index
6 | if current_user
7 | redirect resource(:activities)
8 | else
9 | redirect url(:login)
10 | end
11 | end
12 |
13 | end
14 |
--------------------------------------------------------------------------------
/app/views/users/index.html.erb:
--------------------------------------------------------------------------------
1 | <% throw_content :secondary, partial(:user_form) %>
2 | <%= partial(:users, :users => @users.select { |u| u.is_employee? }, :type => "employees") %>
3 | <%= partial(:users, :users => @users.select { |u| u.is_client_user? }, :type => "clients' users") %>
4 |
--------------------------------------------------------------------------------
/config/local_config.rb.example:
--------------------------------------------------------------------------------
1 | Rubytime::CONFIG[:mail_from] = "rubytime@localhost"
2 | Rubytime::CONFIG[:site_url] = "http://localhost:4000"
3 | Rubytime::CONFIG[:timesheet_report_addressee_email] = "email@localhost"
4 | Rubytime::CONFIG[:exception_report_email] = "email@localhost"
5 |
--------------------------------------------------------------------------------
/app/mailers/views/user_mailer/timesheet_nagger.text.erb:
--------------------------------------------------------------------------------
1 | Hello <%= @user.name %>,
2 |
3 | On <%= @day_without_activities %> you did not fill in any time in the RubyTime system. Please fill in your time as soon as possible.
4 |
5 | This message has been sent automatically.
6 |
7 | Rubytime
8 | <%= @url %>
9 |
--------------------------------------------------------------------------------
/app/mailers/views/user_mailer/notice.text.erb:
--------------------------------------------------------------------------------
1 | Hello <%= @user.name %>,
2 |
3 | You have missed fill activities in your callendar on:
4 |
5 | <% @missed_days.each do |missed_day| %>
6 | <%= "#{missed_day}" %>
7 | <% end %>
8 |
9 | This message has been sent automatically.
10 |
11 | Rubytime
12 | <%= @url %>
--------------------------------------------------------------------------------
/app/mailers/views/user_mailer/password_reset_link.text.erb:
--------------------------------------------------------------------------------
1 | Hello, <%= @user.name %>!
2 |
3 | As you need to reset password for your account please click on following link to setup new password:
4 |
5 | <%= @url + resource(:users, :reset_password, :token => @user.password_reset_token) %>
6 |
7 | Rubytime
8 | <%= @url %>
--------------------------------------------------------------------------------
/app/views/layout/_session_info.html.erb:
--------------------------------------------------------------------------------
1 | <% if session.user %>
2 | Welcome, <%= h session.user.name %>
3 |
4 |
5 | <%= link_to 'Preferences', url(:settings_user, session.user) %>
6 | <%= link_to 'Logout', url(:logout) %>
7 |
8 |
9 | <% end %>
10 |
--------------------------------------------------------------------------------
/app/models/project_activity_type.rb:
--------------------------------------------------------------------------------
1 | class ProjectActivityType
2 | include DataMapper::Resource
3 |
4 | property :project_id, Integer, :required => true, :key => true
5 | property :activity_type_id, Integer, :required => true, :key => true
6 |
7 | belongs_to :project
8 | belongs_to :activity_type
9 | end
10 |
--------------------------------------------------------------------------------
/app/views/clients/index.html.erb:
--------------------------------------------------------------------------------
1 | <% throw_content :secondary, partial(:new_client) %>
2 | List of clients
3 |
4 |
5 | Name
6 | Email
7 | Active
8 |
9 |
10 | <%= partial :client, :with => @clients %>
11 |
--------------------------------------------------------------------------------
/slices/merb-auth-slice-password/app/controllers/sessions.rb:
--------------------------------------------------------------------------------
1 | class MerbAuthSlicePassword::Sessions < MerbAuthSlicePassword::Application
2 |
3 | log_params_filtered :password
4 |
5 | private
6 | def redirect_after_logout
7 | message[:notice] = "Logged Out"
8 | redirect url(:login), :message => message
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/app/models/duration.rb:
--------------------------------------------------------------------------------
1 | class Duration < DelegateClass(Fixnum)
2 |
3 | def to_s(s = nil)
4 | super unless s.nil?
5 | format("%d:%.2d", self / 1.hour, (self % 1.hour) / 1.minute)
6 | end
7 |
8 | def +(fixnum)
9 | Duration.new(super)
10 | end
11 |
12 | def coerce(other)
13 | return self, other
14 | end
15 |
16 | end
17 |
--------------------------------------------------------------------------------
/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | Merb.logger.info("Loaded PRODUCTION Environment...")
2 | Merb::Config.use { |c|
3 | c[:exception_details] = false
4 | c[:reload_classes] = false
5 | c[:log_level] = :info
6 |
7 | c[:log_file] = Merb.root / "log" / "production.log"
8 | # or redirect logger using IO handle
9 | # c[:log_stream] = STDOUT
10 | }
11 |
--------------------------------------------------------------------------------
/config/environments/staging.rb:
--------------------------------------------------------------------------------
1 | Merb.logger.info("Loaded STAGING Environment...")
2 | Merb::Config.use { |c|
3 | c[:exception_details] = false
4 | c[:reload_classes] = false
5 | c[:log_level] = :error
6 |
7 | c[:log_file] = Merb.root / "log" / "staging.log"
8 | # or redirect logger using IO handle
9 | # c[:log_stream] = STDOUT
10 | }
11 |
--------------------------------------------------------------------------------
/app/views/activities/_grouped_activities.html.erb:
--------------------------------------------------------------------------------
1 | <% if current_user.can_see_clients? %>
2 | <%= partial("activities/grouped_clients", :clients => unique_clients_from(@activities)) %>
3 | <% else %>
4 | <%= partial("activities/grouped_projects", :projects => unique_projects_from(@activities, current_user.client), :client => current_user.client) %>
5 | <% end %>
6 |
--------------------------------------------------------------------------------
/config/environments/rake.rb:
--------------------------------------------------------------------------------
1 | Merb.logger.info("Loaded RAKE Environment...")
2 | Merb::Config.use { |c|
3 | c[:exception_details] = true
4 | c[:reload_classes] = false
5 | c[:log_auto_flush ] = true
6 |
7 | c[:log_stream] = STDOUT
8 | c[:log_file] = nil
9 | # Or redirect logging into a file:
10 | # c[:log_file] = Merb.root / "log" / "development.log"
11 | }
12 |
--------------------------------------------------------------------------------
/app/models/observers/hourly_rate_observer.rb:
--------------------------------------------------------------------------------
1 | class HourlyRateObserver
2 | include DataMapper::Observer
3 |
4 | observe HourlyRate
5 |
6 | ['create','update','destroy'].each do |operation_type|
7 | after operation_type do
8 | HourlyRateLog.create :operation_type => operation_type, :operation_author => self.operation_author, :hourly_rate => self
9 | end
10 | end
11 |
12 | end
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # config
2 | config/database.yml
3 | config/deploy.rb
4 | config/local_config.rb
5 | config/rcov_stats.yml
6 | config/schedule.rb
7 | config/ldap.yml
8 |
9 | # regenerable
10 | coverage/*
11 | spec/controllers/log/*
12 | public/javascripts/base.js
13 | public/stylesheets/base.css
14 |
15 | # logs
16 | log/*
17 |
18 | # other
19 | nbproject
20 | .idea/*
21 | tmp/pids/*
22 |
23 | # gems
24 | .bundle
25 |
--------------------------------------------------------------------------------
/spec/extensions/string_extensions_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe String, '#generate_random' do
4 |
5 | it 'should return random string of given size made of original string content' do
6 | "abcd" .generate_random( 1).should =~ /^[abcd]$/
7 | "abcd" .generate_random( 3).should =~ /^[abcd]{3}$/
8 | "123ABC".generate_random(10).should =~ /^[123ABC]{10}$/
9 | end
10 |
11 | end
12 |
--------------------------------------------------------------------------------
/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | Merb.logger.info("Loaded TEST Environment...")
2 | Merb::Config.use { |c|
3 | c[:testing] = true
4 | c[:exception_details] = true
5 | c[:log_auto_flush ] = true
6 | # log less in testing environment
7 | c[:log_level] = :info
8 |
9 | c[:log_file] = Merb.root / "log" / "test.log"
10 | # or redirect logger using IO handle
11 | # c[:log_stream] = STDOUT
12 | }
13 |
--------------------------------------------------------------------------------
/app/views/activity_custom_properties/_fields.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= text_field :name, :label => "Name" %>
3 |
4 |
5 | <%= text_field :unit, :label => "Unit" %>
6 |
7 |
8 | <%= label "Required", :for => :required %>
9 | <%= check_box :required %>
10 |
11 |
12 | <%= label "Show as column in tables", :for => :show_as_column_in_tables %>
13 | <%= check_box :show_as_column_in_tables %>
14 |
15 |
--------------------------------------------------------------------------------
/app/views/clients/_client.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= link_to client.name, url(:client, client) %>
3 | <%= client.email %>
4 | <%= client.active ? "Yes" : "No" %>
5 |
6 | <%= link_to image_tag("icons/pencil.png", :alt => "E"), url(:edit_client, client) %>
7 | <%= link_to image_tag("icons/cross.png", :title => "Remove", :alt => "Remove"), resource(client), :class => "delete_row" %>
8 |
9 |
--------------------------------------------------------------------------------
/app/mailers/views/user_mailer/timesheet_changes_notifier.text.erb:
--------------------------------------------------------------------------------
1 | Hello <%= @project_manager.name %>,
2 |
3 | this is to notify you that <%= @activity.user.name %> has <%= @kind_of_change %> the following entry in RubyTime:
4 |
5 | Project: <%= @activity.project.name %>
6 | Date: <%= @activity.date %>
7 | Hours: <%= @activity.hours %>
8 | Comments: <%= @activity.comments %>
9 |
10 | This message has been sent automatically.
11 |
12 | Rubytime
13 | <%= @url %>
--------------------------------------------------------------------------------
/app/helpers/projects_helper.rb:
--------------------------------------------------------------------------------
1 | module Merb
2 | module ProjectsHelper
3 |
4 | def activity_type_check_box(activity_type, checked = false, disabled = false)
5 | attrs = { :type => "checkbox", :name => "project[activity_type_ids][]", :value => activity_type.id }
6 | attrs[:checked] = 'checked' if checked
7 | attrs[:disabled] = 'disabled' if disabled
8 | tag(:input, nil, attrs) + activity_type.name
9 | end
10 |
11 | end
12 | end # Merb
--------------------------------------------------------------------------------
/app/views/activities/_grouped_clients.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <% clients.each do |client| %>
3 |
4 | <%= partial("activities/grouped_projects", :projects => unique_projects_from(@activities, client), :client => client) %>
5 | Total for client: <%= total_from(activities_from(@activities, client)) %>
6 | <%= total_custom_properties(@activities, client) %>
7 |
8 | <% end %>
9 |
10 |
--------------------------------------------------------------------------------
/app/views/activity_custom_properties/edit.html.erb:
--------------------------------------------------------------------------------
1 | Edit custom property
2 |
3 | <%= error_messages_for(@activity_custom_property) %>
4 | <%= form_for @activity_custom_property, :action => resource(@activity_custom_property), :id => "activity_custom_property_form" do %>
5 |
6 | <%= partial :fields %>
7 |
8 |
9 | <%= submit 'Update', :class => "button" %>
10 |
11 |
12 | <% end =%>
13 |
--------------------------------------------------------------------------------
/spec/extensions/float_extensions_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Float do
4 |
5 | it 'should round itself to 2 decimal places' do
6 | 1.55.round_to_2_digits.should == 1.55
7 | 1.0.round_to_2_digits.should == 1.0
8 | 29.23456.round_to_2_digits.should == 29.23
9 | 29.23556.round_to_2_digits.should == 29.24
10 | (2.0/3).round_to_2_digits.should == 0.67
11 | -567.876.round_to_2_digits.should == -567.88
12 | end
13 |
14 | end
15 |
--------------------------------------------------------------------------------
/lib/extensions/date_extensions.rb:
--------------------------------------------------------------------------------
1 | class Date
2 |
3 | SUNDAY = 0
4 | SATURDAY = 6
5 |
6 | def weekend?
7 | wday == SATURDAY or wday == SUNDAY
8 | end
9 |
10 | def weekday?
11 | not weekend?
12 | end
13 |
14 | def previous_weekday
15 | day = self - 1
16 | until day.weekday?
17 | day -= 1
18 | end
19 | day
20 | end
21 |
22 | def first_day_of_month
23 | Date.parse("#{year}-#{month}-1")
24 | end
25 |
26 | end
27 |
--------------------------------------------------------------------------------
/app/views/users/request_password.html.erb:
--------------------------------------------------------------------------------
1 | Password reset
2 |
3 | If you have forgotten your Rubytime credentials please enter your e-mail address and we will send you password reset link.
4 |
5 |
6 | <%= form :action => "" do %>
7 |
8 |
9 | <%= text_field :name => "email", :label => "E-mail" %>
10 |
11 |
12 | <%= submit "Submit", :class => "button" %>
13 |
14 |
15 | <% end =%>
--------------------------------------------------------------------------------
/app/views/activity_custom_properties/_new_activity_custom_property.html.erb:
--------------------------------------------------------------------------------
1 | Create new custom property
2 |
3 | <%= error_messages_for(activity_custom_property) %>
4 | <%= form_for activity_custom_property, :action => resource(:activity_custom_properties), :id => "activity_custom_property_form" do %>
5 |
6 | <%= partial :fields %>
7 |
8 |
9 | <%= submit 'Create', :class => "button" %>
10 |
11 |
12 | <% end =%>
13 |
--------------------------------------------------------------------------------
/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | Merb.logger.info("Loaded DEVELOPMENT Environment...")
2 | Merb::Config.use { |c|
3 | c[:exception_details] = true
4 | c[:reload_templates] = true
5 | c[:reload_classes] = true
6 | c[:reload_time] = 0.5
7 | c[:ignore_tampered_cookies] = true
8 | c[:log_auto_flush ] = true
9 | c[:log_level] = :debug
10 |
11 | c[:log_stream] = STDOUT
12 | c[:log_file] = nil
13 | # Or redirect logging into a file:
14 | # c[:log_file] = Merb.root / "log" / "development.log"
15 | }
16 |
--------------------------------------------------------------------------------
/app/views/roles/edit.html.erb:
--------------------------------------------------------------------------------
1 | Edit role
2 | <%= form_for @role, :action => resource(@role), :id => "role_form" do %>
3 |
4 |
5 | <%= text_field :name, :label => "Name", :disabled => true %>
6 |
7 |
8 | Can manage financial data
9 | <%= check_box :can_manage_financial_data %>
10 |
11 |
12 | <%= submit 'Update', :class => "button" %>
13 |
14 |
15 | <% end =%>
16 |
--------------------------------------------------------------------------------
/app/views/activity_types/_new_activity_type.html.erb:
--------------------------------------------------------------------------------
1 | Create new <%= activity_type.parent ? 'sub-activity' : 'activity' %> type
2 |
3 | <%= error_messages_for(activity_type) %>
4 | <%= form_for activity_type, :action => resource(:activity_types), :id => "activity_type_form" do %>
5 |
6 | <%= hidden_field :parent_id %>
7 |
8 | <%= text_field :name, :label => "Name" %>
9 |
10 |
11 | <%= submit 'Create', :class => "button" %>
12 |
13 |
14 | <% end =%>
15 |
--------------------------------------------------------------------------------
/public/javascripts/lib/rubytime/input-filler.js:
--------------------------------------------------------------------------------
1 | $.fn.extend({
2 | fill: function(source, slugify) {
3 | var source = $(source);
4 | if (source.blank())
5 | throw "@source doesn't match any element";
6 | else {
7 | var target = this;
8 | function sourceChanged(e) {
9 | target.attr('value', slugify ? source.attr('value').replace(/\W+/g, "-") : source.attr('value'));
10 | };
11 | source.keyup(sourceChanged);
12 | target.change(function() {
13 | source.unbind('keyup', sourceChanged);
14 | });
15 | }
16 | }
17 | });
--------------------------------------------------------------------------------
/app/views/activities/_grouped_roles.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <% roles.each do |role| %>
3 |
4 | <%= image_tag("icons/role.png", :alt => 'role') %>
5 | Role: <%= role.name %>
6 | <% activities = activities_from(@activities, client, project, role) %>
7 | <% if params[:controller] == "invoices" %>
8 | <%= invoice_activities_table(activities) %>
9 | <% else %>
10 | <%= full_activities_table(activities) %>
11 | <% end %>
12 |
13 | <% end %>
14 |
15 |
--------------------------------------------------------------------------------
/TODO.txt:
--------------------------------------------------------------------------------
1 | Things to do (in any order):
2 |
3 | * convert CSS to LESS
4 |
5 | * refactor existing code
6 | - specs are in rather good shape now, but helpers, javascripts and some controllers definitely need some work too
7 | - also, look for things marked #TODO :)
8 | - look for actions which make too many queries and optimize them
9 |
10 | * migrate to Rails 3
11 |
12 | * for deployment, switch from Vlad to Capistrano
13 |
14 | * fix minor bugs, implement new features
15 |
16 | * use 'whenever' for configuring scheduled tasks (timesheet reports etc.)
17 |
--------------------------------------------------------------------------------
/app/mailers/views/user_mailer/timesheet_reporter.text.erb:
--------------------------------------------------------------------------------
1 | Hello,
2 |
3 | <% if @employees_without_activities.empty? %>
4 | All employees filled time in the RubyTime system on <%= @day_without_activities %> :-)
5 |
6 | <% else %>
7 | On <%= @day_without_activities %> the following employees did not fill in any time in the RubyTime system:
8 |
9 | <% @employees_without_activities.each do |employee| %>
10 | <%= employee.name %>
11 | <% end %>
12 | <% end %>
13 |
14 | This message has been sent automatically.
15 |
16 | Rubytime
17 | <%= @url %>
18 |
--------------------------------------------------------------------------------
/config/database.yml.example:
--------------------------------------------------------------------------------
1 | ---
2 | development: &defaults
3 | adapter: mysql
4 | database: rubytime3_dev
5 | username: root
6 | password:
7 | host: localhost
8 | encoding: UTF-8
9 |
10 | test:
11 | <<: *defaults
12 | database: rubytime3_test
13 |
14 | production:
15 | <<: *defaults
16 | database: rubytime3_prod
17 | repositories:
18 | legacy:
19 | adapter: mysql
20 | database: rubytime2_prod
21 | username: root
22 | password:
23 | host: localhost
24 |
25 | rake:
26 | <<: *defaults
27 |
--------------------------------------------------------------------------------
/config.ru:
--------------------------------------------------------------------------------
1 | begin
2 | require File.expand_path('../.bundle/environment', __FILE__)
3 | rescue LoadError
4 | require "rubygems"
5 | require "bundler"
6 | Bundler.setup
7 | end
8 |
9 | require 'merb-core'
10 |
11 | Merb::Config.setup(
12 | :merb_root => ::File.expand_path(::File.dirname(__FILE__)),
13 | :fork_for_class_load => false,
14 | :environment => ENV['RACK_ENV'] || 'production'
15 | )
16 | Merb.environment = Merb::Config[:environment]
17 | Merb.root = Merb::Config[:merb_root]
18 | Merb::BootLoader.run
19 |
20 | run Merb::Rack::Application.new
21 |
--------------------------------------------------------------------------------
/config/schedule.rb.example:
--------------------------------------------------------------------------------
1 | set :output, "log/cron.log"
2 |
3 | job_type :rake, "cd :path && MERB_ENV=:environment bundle exec rake :task :output"
4 |
5 | every 1.day, :at => '10:00 am' do
6 | rake "rubytime:send_timesheet_nagger_emails_for_previous_weekday"
7 | end
8 |
9 | every 1.day, :at => '12:00 am' do
10 | rake "rubytime:send_timesheet_report_email_for_previous_weekday"
11 | end
12 |
13 | every :friday do
14 | rake "rubytime:send_timesheet_summary_emails_for_last_five_days"
15 | end
16 |
17 | every 1.day do
18 | rake "rubytime:send_emails"
19 | end
20 |
--------------------------------------------------------------------------------
/app/controllers/settings.rb:
--------------------------------------------------------------------------------
1 | class Settings < Application
2 |
3 | before :ensure_admin
4 |
5 | def edit
6 | only_provides :html
7 | @setting = Setting.get
8 | raise NotFound unless @setting
9 | display @setting
10 | end
11 |
12 | def update
13 | @setting = Setting.get
14 | raise NotFound unless @setting
15 | if @setting.update(params[:setting])
16 | redirect url(:edit_settings)
17 | else
18 | display @setting, :edit
19 | end
20 | end
21 |
22 | def number_of_columns
23 | 1
24 | end
25 |
26 | end # Settings
27 |
--------------------------------------------------------------------------------
/app/views/roles/_new_role.html.erb:
--------------------------------------------------------------------------------
1 | Create new role
2 | <%= error_messages_for(@role) %>
3 | <%= form_for @role, :action => resource(@role.id ? @role : :roles), :id => "role_form" do %>
4 |
5 |
6 | <%= text_field :name, :label => "Name" %>
7 |
8 |
9 | Can manage financial data
10 | <%= check_box :can_manage_financial_data %>
11 |
12 |
13 | <%= submit 'Create', :class => "button" %>
14 |
15 |
16 | <% end =%>
17 |
--------------------------------------------------------------------------------
/config/rack.rb:
--------------------------------------------------------------------------------
1 | # use PathPrefix Middleware if :path_prefix is set in Merb::Config
2 | if prefix = ::Merb::Config[:path_prefix]
3 | use Merb::Rack::PathPrefix, prefix
4 | end
5 |
6 | # comment this out if you are running merb behind a load balancer
7 | # that serves static files
8 | use Merb::Rack::Static, Merb.dir_for(:public)
9 |
10 | if Merb.env != 'production' || Rubytime::CONFIG[:site_url] =~ /llpdemo.com/
11 | use Rack::RevisionInfo, :path => Merb.root, :append => ".revision_info"
12 | end
13 |
14 | # this is our main merb application
15 | run Merb::Rack::Application.new
16 |
--------------------------------------------------------------------------------
/app/views/activities/_activity_details.html.erb:
--------------------------------------------------------------------------------
1 | <%# TODO: is this used anywhere at all ?... %>
2 |
3 | Project: <%= activity.project.name %>
4 |
5 | Hours:
6 | <%= activity.hours %>
7 | <%= edit_activity activity %>
8 | <%= delete_activity activity %>
9 |
10 |
14 |
--------------------------------------------------------------------------------
/spec/factory_patch.rb:
--------------------------------------------------------------------------------
1 | # Monkey patch for factory_girl to have a more generic sequence
2 | # http://devblog.timmedina.com
3 |
4 | class Factory
5 |
6 | def custom_sequence(name, initial_value = 1, &block)
7 | s = Sequence.new(initial_value, &block)
8 | add_attribute(name) { s.next }
9 | end
10 |
11 | class Sequence
12 |
13 | def initialize(initial_value = 1, &proc)
14 | @proc = proc
15 | @value = initial_value
16 | end
17 |
18 | def next
19 | prev, @value = @value, @value.next
20 | @proc.call(prev)
21 | end
22 |
23 | end
24 |
25 | end
--------------------------------------------------------------------------------
/app/views/activities/_criteria_group.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= select :name => "search_criteria[#{group}_id][]", :id => "search_criteria_#{group}_id", :label => group.to_s.camel_case, :class => "#{group}_combo",
3 | :collection => @search_criteria.send("all_#{group.to_s.pluralize}").map { |o| [o.id, o.name] },
4 | :prompt => "All" %>
5 | <%= link_to(image_tag("icons/minus.png", :alt => "-"), "#", :class => "remove_criterium") %>
6 | <%= link_to(image_tag("icons/plus.png", :alt => "+"), "#", :class => "add_criterium") %>
7 |
8 |
--------------------------------------------------------------------------------
/public/javascripts/invoices.js:
--------------------------------------------------------------------------------
1 | var Invoices = {
2 |
3 | init: function() {
4 | // table listing
5 | Invoices._initActivitiesList();
6 | },
7 |
8 | _initActivitiesList: function() {
9 | // style the table
10 | $(".activities").zebra();
11 |
12 | // init details icon/link
13 | Application.initCommentsIcons();
14 |
15 | // init remove from invoice icon/link
16 | $(".activities .remove_from_invoice_link").click(function() {
17 | Application.notice("Not implemented yet, sorry.");
18 | return false;
19 | });
20 | }
21 |
22 | };
23 |
24 | $(Invoices.init);
25 |
--------------------------------------------------------------------------------
/public/javascripts/activity_types.js:
--------------------------------------------------------------------------------
1 | $(function() {
2 |
3 | $('table#activity_types').sortable({
4 | items: 'tr.activity-type',
5 | axis: 'y',
6 | cursor: 'move',
7 | change: function(event, ui) { $('table#activity_types').zebra() },
8 | stop: function(event, ui) { $('table#activity_types').zebra() },
9 | update: function(event, ui) {
10 | $.ajax({
11 | url: '/activity_types/' + $(ui.item).recordId(),
12 | type: "PUT",
13 | data: { 'activity_type[position]': ui.item.positionInParent('.activity-type') }
14 | });
15 | }
16 | });
17 |
18 |
19 | });
20 |
--------------------------------------------------------------------------------
/app/views/activities/day.html.erb:
--------------------------------------------------------------------------------
1 |
6 | <% if @activities.empty? %>
7 |
8 | No activities for <%= @day %>.
9 |
10 | <% else %>
11 | <%= calendar_activities_table(@activities, :table_id => "calendar_activities", :show_header_icons => true) %>
12 |
13 | Total: <%= total_from(@activities) %>
14 |
15 | <% end %>
16 |
--------------------------------------------------------------------------------
/app/models/observers/activity_observer.rb:
--------------------------------------------------------------------------------
1 | class ActivityObserver
2 | include DataMapper::Observer
3 |
4 | observe Activity
5 |
6 | after :create do
7 | user.update(:activities_count => user.activities_count + 1)
8 | notify_project_managers_if_enabled(:created) if date < Date.today - 1
9 | end
10 |
11 | after :destroy do
12 | user.update(:activities_count => (new_count = user.activities_count - 1) > 0 ? new_count : 0)
13 | notify_project_managers_if_enabled(:destroy) if date < Date.today - 1
14 | end
15 |
16 | after :update do
17 | notify_project_managers_if_enabled(:updated) if date < Date.today - 1
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/app/views/activity_types/edit.html.erb:
--------------------------------------------------------------------------------
1 | <% if @activity_type.parent %>
2 | Edit <%= @activity_type.parent.name %> -> <%= @old_name || @activity_type.name %> sub-activity type
3 | <% else %>
4 | Edit <%= @old_name || @activity_type.name %> activity type
5 | <% end %>
6 |
7 | <%= error_messages_for(@activity_type) %>
8 | <%= form_for @activity_type, :action => resource(@activity_type), :id => "activity_type_form" do %>
9 |
10 |
11 | <%= text_field :name, :label => "Name" %>
12 |
13 |
14 | <%= submit 'Update', :class => "button" %>
15 |
16 |
17 | <% end =%>
18 |
--------------------------------------------------------------------------------
/app/models/role.rb:
--------------------------------------------------------------------------------
1 | class Role
2 | include DataMapper::Resource
3 |
4 | property :id, Serial
5 | property :name, String, :required => true, :unique => true
6 | property :can_manage_financial_data, Boolean, :required => true, :default => false
7 |
8 | has n, :employees
9 |
10 | before :destroy do
11 | throw :halt if employees.count > 0
12 | end
13 |
14 | # class methods
15 |
16 | def self.for_select
17 | all.map { |r| [r.id, r.name] }
18 | end
19 |
20 | # instance methods
21 |
22 | def name=(name)
23 | raise 'The :name attribute is readonly' unless new? or name == self.name
24 | attribute_set(:name, name)
25 | end
26 |
27 | end
28 |
--------------------------------------------------------------------------------
/app/mailers/views/user_mailer/timesheet_summary.text.erb:
--------------------------------------------------------------------------------
1 | Hello <%= @user.name %>,
2 |
3 | this is your RubyTime activities summary for <%= @dates_range %>.
4 |
5 | <% @activities_by_dates_and_projects.each do |date, projects| %>
6 | On <%= date %>:
7 | <% unless projects.empty? %>
8 | <% projects.each do |project, activities| %>
9 | <%= project.name %>:
10 | <% activities.each do |activity| %>
11 | <%= activity.hours %> <%= activity.comments %>
12 | <% end %>
13 | <% end %>
14 | <% else %>
15 | No activities.
16 | <% end %>
17 |
18 | <% end %>
19 |
20 | This message has been sent automatically.
21 |
22 | Rubytime
23 | <%= @url %>
24 |
--------------------------------------------------------------------------------
/app/helpers/users_helper.rb:
--------------------------------------------------------------------------------
1 | module Merb
2 | module UsersHelper
3 | def link_to_calendar(user)
4 | link_to image_tag("icons/calendar.png", :title => "Calendar", :alt => 'C'), url(:user_calendar, user) if user.is_employee?
5 | end
6 |
7 | def recent_days_on_list_desc(v)
8 | "#{v} days"
9 | end
10 |
11 | def date_format_desc(v)
12 | "#{v.capitalize} (#{Rubytime::DATE_FORMATS[v.to_sym][:description]})"
13 | end
14 |
15 | def radio_options(user, property, symbols)
16 | radio_group(property, symbols.map { |s|
17 | { :value => s.to_s, :label => yield(s.to_s), :checked => user.attribute_get(property) == s }})
18 | end
19 | end
20 | end # Merb
--------------------------------------------------------------------------------
/app/views/currencies/_currency_form.html.erb:
--------------------------------------------------------------------------------
1 | <%= error_messages_for(@currency) %>
2 | <%= form_for @currency, :action => resource(@currency.id ? @currency : :currencies), :id => "currency_form" do %>
3 |
4 |
5 | <%= text_field :singular_name, :label => "Singular name" %>
6 |
7 |
8 | <%= text_field :plural_name, :label => "Plural name" %>
9 |
10 |
11 | <%= text_field :prefix, :label => "Prefix" %>
12 |
13 |
14 | <%= text_field :suffix, :label => "Suffix" %>
15 |
16 |
17 | <%= submit(submit_name, :class => "button") %>
18 |
19 |
20 | <% end =%>
21 |
--------------------------------------------------------------------------------
/app/views/invoices/_new_invoice.html.erb:
--------------------------------------------------------------------------------
1 | Create new invoice
2 |
3 | <%= error_messages_for(@invoice) %>
4 | <%= form_for @invoice, :action => resource(:invoices), :id => "invoice_form" do %>
5 |
6 |
7 | <%= select :client_id, :label => "Client", :collection => @clients.map { |c| [c.id, c.name] }, :prompt => "Select a client..." %>
8 |
9 |
10 | <%= text_field :name, :label => "Name" %>
11 |
12 |
13 | <%= text_area :notes, :label => "Notes", :class => "text", :cols => "21", :rows => "5" %>
14 |
15 |
16 | <%= submit 'Create', :class => "button" %>
17 |
18 |
19 | <% end =%>
20 |
--------------------------------------------------------------------------------
/app/models/purse.rb:
--------------------------------------------------------------------------------
1 | class Purse
2 | def initialize
3 | @money_by_currency = {}
4 | end
5 |
6 | def <<(money)
7 | @money_by_currency[money.currency] ||= Money.new(0, money.currency)
8 | @money_by_currency[money.currency] += money
9 | end
10 |
11 | def currencies
12 | @money_by_currency.keys.sort_by { |currency| currency.singular_name }
13 | end
14 |
15 | def [](currency)
16 | @money_by_currency[currency] || Money.new(0, currency)
17 | end
18 |
19 | def to_s
20 | currencies.map { |currency| self[currency].to_s }.join(' and ')
21 | end
22 |
23 | def merge(other)
24 | other.currencies.each { |currency| self << other[currency] }
25 | self
26 | end
27 |
28 | end
29 |
--------------------------------------------------------------------------------
/app/views/projects/_new_project.html.erb:
--------------------------------------------------------------------------------
1 | Create new project
2 |
3 | <%= error_messages_for(@project) %>
4 | <%= form_for @project, :action => resource(:projects), :id => "project_form" do %>
5 |
6 |
7 | <%= text_field :name, :label => "Name" %>
8 |
9 |
10 | <%= text_area :description, :label => "Description", :class => "text", :cols => "21", :rows => "5" %>
11 |
12 |
13 | <%= select :client_id, :label => "Client", :collection => @clients.map { |c| [c.id, c.name] }, :prompt => "Select a client..." %>
14 |
15 |
16 | <%= submit 'Create', :class => "button" %>
17 |
18 |
19 | <% end =%>
20 |
--------------------------------------------------------------------------------
/app/views/clients/edit.html.erb:
--------------------------------------------------------------------------------
1 | <%= error_messages_for @client %>
2 | <%= error_messages_for @client_user %>
3 | <%= form_for @client, :action => resource(@client), :id => "client_form" do %>
4 |
5 | Client editing
6 |
7 | <%= text_field :name, :label => "Name" %>
8 |
9 |
10 | <%= text_area :description, :label => "Description" %>
11 |
12 |
13 | <%= text_field :email, :label => "Email" %>
14 |
15 |
16 | Active?
17 | <%= check_box :active %>
18 |
19 |
20 | <%= submit "Save", :class => "button" %>
21 |
22 |
23 | <% end =%>
24 |
--------------------------------------------------------------------------------
/public/javascripts/indicator.js:
--------------------------------------------------------------------------------
1 | var Indicator = function(button) {
2 | this.button = button;
3 | };
4 |
5 | Indicator.IMAGE_SRC = '/images/ajax-loader.gif';
6 | Indicator.TRANSPARENT_IMAGE_SRC = '/images/ajax-loader-transparent.gif';
7 |
8 | Indicator.prototype = {
9 | start: function(imageSrc) {
10 | this.button.attr('disabled', true);
11 | this.button.after(this.imageTag(imageSrc));
12 | this.spinner = this.button.next();
13 | },
14 |
15 | stop: function() {
16 | this.button.attr('disabled', false);
17 | this.spinner.remove();
18 | this.spinner = null;
19 | },
20 |
21 | imageTag: function(imageSrc) {
22 | return ' ';
23 | }
24 | };
25 |
--------------------------------------------------------------------------------
/app/views/invoices/_edit.html.erb:
--------------------------------------------------------------------------------
1 | Edit invoice
2 |
3 | <%= error_messages_for(@invoice) %>
4 | <%= form_for @invoice, :action => resource(@invoice, filter_hash), :id => "invoice_form" do %>
5 |
6 |
7 | <%= select :client_id, :label => "Client", :collection => @clients.map { |c| [c.id, c.name] }, :prompt => "Select a client..." %>
8 |
9 |
10 | <%= text_field :name, :label => "Name" %>
11 |
12 |
13 | <%= text_area :notes, :label => "Notes", :class => "text", :cols => "21", :rows => "5" %>
14 |
15 |
16 | <%= submit 'Save', :class => "button" %> or <%= link_to "Cancel", resource(@invoice, filter_hash) %>
17 |
18 |
19 | <% end =%>
20 |
--------------------------------------------------------------------------------
/app/views/layout/_navigation.html.erb:
--------------------------------------------------------------------------------
1 | Navigation
2 |
23 |
--------------------------------------------------------------------------------
/app/models/role_activities_in_project_summary.rb:
--------------------------------------------------------------------------------
1 | class RoleActivitiesInProjectSummary
2 | def initialize(role, activities)
3 | self.role = role
4 | self.non_billable_time = 0
5 | self.billable_time = 0
6 | self.price = Purse.new
7 |
8 | activities.each { |activity| self << activity }
9 | end
10 |
11 | attr_reader :role, :non_billable_time, :billable_time, :price
12 |
13 | def <<(activity)
14 | raise ArgumentError unless activity.role_id == role.id
15 |
16 | if activity.price
17 | self.billable_time += activity.duration
18 | self.price << activity.price
19 | else
20 | self.non_billable_time += activity.duration
21 | end
22 | end
23 |
24 | private
25 | attr_writer :role, :non_billable_time, :billable_time, :price
26 | end
27 |
--------------------------------------------------------------------------------
/spec/mailer_helper.rb:
--------------------------------------------------------------------------------
1 | module Rubytime
2 | module Test
3 | module MailerHelper
4 | # Helper to clear mail deliveries.
5 | def clear_mail_deliveries
6 | Merb::Mailer.deliveries.clear
7 | end
8 |
9 | # Helper to access last delivered mail.
10 | # In test mode merb-mailer puts email to
11 | # collection accessible as Merb::Mailer.deliveries.
12 | def last_delivered_mail
13 | Merb::Mailer.deliveries.last
14 | end
15 |
16 | # Helper to deliver
17 | def deliver(action, mail_params = {}, send_params = {})
18 | mailer_params = { :from => "no-reply@webapp.com", :to => "recepient@person.com" }.merge(mail_params)
19 | UserMailer.dispatch_and_deliver(action, mailer_params, send_params)
20 | @delivery = last_delivered_mail
21 | end
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/app/views/roles/index.html.erb:
--------------------------------------------------------------------------------
1 | <% throw_content :secondary, partial(:new_role) %>
2 | List of employees' roles
3 |
4 | <% if @roles.empty? %>
5 | No roles found.
6 | <% else %>
7 |
8 |
9 | Name
10 | Can manage financial data?
11 |
12 |
13 | <% @roles.each do |role| %>
14 |
15 | <%= role.name %>
16 | <%= role.can_manage_financial_data ? 'yes' : 'no' %>
17 |
18 | <%= link_to image_tag("icons/pencil.png", :title => "Edit", :alt => "Edit"), resource(role, :edit) %>
19 | <%= link_to image_tag("icons/cross.png", :title => "Remove", :alt => "Remove"), resource(role), :class => "delete_row" %>
20 |
21 |
22 | <% end %>
23 |
24 | <% end %>
25 |
26 |
--------------------------------------------------------------------------------
/app/models/activity_custom_property.rb:
--------------------------------------------------------------------------------
1 | class ActivityCustomProperty
2 | include DataMapper::Resource
3 |
4 | property :id, Serial
5 | property :name, String, :required => true, :index => true
6 | property :unit, String, :required => false
7 | property :required, Boolean, :required => true, :default => false
8 | property :show_as_column_in_tables, Boolean, :required => true, :default => false
9 | property :updated_at, DateTime
10 | property :created_at, DateTime
11 |
12 | has n, :activity_custom_property_values
13 |
14 | validates_is_unique :name
15 |
16 | def name_with_unit
17 | name + (unit.blank? ? "" : " (#{unit})")
18 | end
19 |
20 | def destroy_allowed?
21 | activity_custom_property_values.count == 0
22 | end
23 |
24 | def destroy
25 | return false unless destroy_allowed?
26 | super()
27 | end
28 |
29 | end
30 |
--------------------------------------------------------------------------------
/app/views/activities/calendar.html.erb:
--------------------------------------------------------------------------------
1 | <% if @projects %>
2 |
3 | Calendar for project <%= select :name => "project_id", :id => "project_id", :collection => @projects.map { |p| [p.id, p.name] }, :selected => @owner.id.to_s %>
4 |
5 |
6 | <%= activities_calendar :activities => @activities_by_date, :year => @year, :month => @month, :owner => @owner %>
7 |
8 | <% else %>
9 | <% if @users %>
10 |
11 | Calendar for user <%= select :name => "user_id", :id => "user_id", :collection => @users.map { |u| [u.id, u.name] }, :selected => @owner.id.to_s %>
12 |
13 | <% end %>
14 |
15 | <%= activities_calendar :activities => @activities_by_date, :year => @year, :month => @month, :owner => @owner %>
16 |
17 | <% end %>
18 |
--------------------------------------------------------------------------------
/lib/auth/ldap.rb:
--------------------------------------------------------------------------------
1 | require 'net/ldap'
2 | require 'yaml'
3 |
4 | class LdapConnectionError < StandardError; end
5 |
6 | module Auth
7 | module LDAP
8 | LDAP_CONFIG_FILE = File.join(Merb.root, 'config', 'ldap.yml')
9 |
10 | def self.isLDAP?
11 | File.exist?(LDAP_CONFIG_FILE)
12 | end
13 |
14 | def self.settings
15 | @settings ||= YAML.load_file(LDAP_CONFIG_FILE)
16 | end
17 |
18 | def self.authenticate(login, password)
19 | ldap = Net::LDAP.new({
20 | :host => settings[:host],
21 | :base => settings[:base],
22 | :port => settings[:port],
23 | :auth => {
24 | :method => :simple,
25 | :username => "#{settings[:attr]}=#{login},#{settings[:base]}",
26 | :password => password
27 | }})
28 | ldap.bind
29 | rescue Net::LDAP::LdapError, Timeout::Error, SystemCallError => e
30 | raise LdapConnectionError, e
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/app/views/activities/_roles_summary_by_project.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | Role
4 | Hours
5 | Price
6 |
7 |
8 | <%# TODO: I know this is evil but this isn't the best moment for refactoring... %>
9 | <% data = roles.map { |role| RoleActivitiesInProjectSummary.new(role,
10 | activities_from(@activities, client, project, role)) } %>
11 |
12 | <% data.each do |summary| %>
13 |
14 | <%= summary.role.name %>
15 | <%= summary.billable_time %>
16 | <%= summary.price %>
17 |
18 | <% end %>
19 |
20 | <% if data.length > 1 %>
21 |
22 | Total:
23 | <%= data.inject(0.0) { |sum, r| sum + r.billable_time } %>
24 | <%= data.inject(Purse.new) { |sum, r| sum.merge(r.price) } %>
25 |
26 | <% end %>
27 |
28 |
29 |
--------------------------------------------------------------------------------
/app/views/users/_users.html.erb:
--------------------------------------------------------------------------------
1 | List of <%= type %>
2 |
3 |
4 |
5 | Name
6 | <%= type == 'employees' ? 'Role' : 'Client' %>
7 | Active
8 |
9 |
10 | <% users.each do |user| %>
11 |
12 | <%= link_to(user.name, url(:user, user)) %>
13 | <%= type == 'employees' ? link_to(user.role.name, resource(user.role)) : link_to(user.client.name, resource(user.client)) %>
14 | <%= user.active? ? 'Yes' : 'No' %>
15 |
16 | <%= link_to_calendar user %>
17 | <%= link_to image_tag("icons/pencil.png", :title => "Edit", :alt => "E"), url(:edit_user, user) %>
18 | <%= link_to image_tag("icons/cross.png", :title => "Remove", :alt => "R"), url(:user, user), :class => "delete_row" unless user == current_user %>
19 |
20 |
21 | <% end %>
22 |
23 |
--------------------------------------------------------------------------------
/app/views/activities/index.html.erb:
--------------------------------------------------------------------------------
1 | <% throw_content :secondary, partial(:filter) %>
2 | <% show_invoice_form = current_user.is_admin? && !@uninvoiced_activities.empty? %>
3 |
4 | List of activities
5 |
6 | <% if @activities.empty? %>
7 | No activities found for selected critieria.
8 | <% else %>
9 |
10 | Found <%= @activities.size %> matching activities
11 | <%= @search_criteria.date_from ? " from #{@search_criteria.date_from.formatted(current_user.date_format)}" : "" %>
12 | <%= @search_criteria.date_to ? " till #{@search_criteria.date_to.formatted(current_user.date_format)}" : "" %>.
13 |
14 |
15 |
16 |
17 | <%= partial(:grouped_activities) %>
18 |
19 | <%= link_to "Export this list to CSV", url(:activities) + ".csv?" + params.reject { |k,v| k =~ /^(id|action|controller|format)$/}.to_params %>
20 |
21 | <% if show_invoice_form %>
22 | <%= partial(:invoice_forms) %>
23 | <% end %>
24 | <% end %>
--------------------------------------------------------------------------------
/app/views/invoices/show.html.erb:
--------------------------------------------------------------------------------
1 | Invoice: <%= @invoice.name %>
2 | <% unless @invoice.notes.blank? %>
3 | Notes: <%= @invoice.notes %>
4 | <% end %>
5 |
6 | <% if @invoice.activities.empty? %>
7 | This invoice has no assigned activities.
8 | <% else %>
9 |
10 | This invoice contains following activities:
11 |
12 |
13 |
14 |
15 | <%= partial("activities/grouped_activities") %>
16 |
17 | <% if @invoice.issued? %>
18 | This invoice has been issued at <%= @invoice.issued_at.to_time.to_date %>
19 | <% elsif current_user.is_admin? %>
20 | <%= form_for @invoice, :action => resource(@invoice, :issue), :id => "issue_invoice_form" do %>
21 | <%= submit "Issue this invoice", :class => "button" %>
22 | <% end =%>
23 | <% end %>
24 | <% end %>
25 |
26 |
27 | <%= link_to 'Back to invoices', url(:invoices, filter_hash) %>
28 | <%= link_to('Edit', resource(@invoice, :edit, filter_hash))if current_user.is_admin? %>
29 |
30 |
--------------------------------------------------------------------------------
/public/javascripts/clients.js:
--------------------------------------------------------------------------------
1 | $(function() {
2 | $('#client_user_login').fill('#client_name', true);
3 | $('#client_user_email').fill('#client_email');
4 | $('#client_user_password, #client_user_password_confirmation').one('change', function() {
5 | $('#generated_password').empty();
6 | });
7 |
8 | $('#client_form').validate({
9 | rules: {
10 | "client[name]": {
11 | required: true,
12 | minlength: 3
13 | },
14 | "client[email]": {
15 | email: true
16 | },
17 | "client_user[name]": {
18 | required: true,
19 | minlength: 3
20 | },
21 | "client_user[login]": {
22 | required: true,
23 | minlength: 3,
24 | maxlength: 20
25 | },
26 | "client_user[email]": {
27 | required: true,
28 | email: true
29 | },
30 |
31 | "client_user[password]": {
32 | required: true
33 | },
34 | "client_user[password_confirmation]": {
35 | equalTo: "#client_user_password"
36 | }
37 | }
38 | });
39 | });
--------------------------------------------------------------------------------
/slices/merb-auth-slice-password/app/views/layout/merb-auth-slice-password.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Fresh MerbAuthSlicePassword Slice
6 |
7 |
8 |
9 |
10 |
11 |
12 |
MerbAuthSlicePassword Slice
13 |
<%= catch_content :for_layout %>
14 |
15 |
16 |
--------------------------------------------------------------------------------
/spec/models/duration_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Duration do
4 |
5 | it "should be summable" do
6 | ( Duration.new(10.minutes) + Duration.new(5.minutes) ).should == Duration.new(15.minutes)
7 | ( Duration.new(10.minutes) + 3.minutes ).should == Duration.new(13.minutes)
8 | ( 15.minutes + Duration.new(10.minutes) ).should == Duration.new(25.minutes)
9 | end
10 |
11 | it "should be comparable with Fixnums" do
12 | ( Duration.new(10.minutes) == 10.minutes ).should be_true
13 | ( Duration.new(10.minutes) > 15.minutes ).should be_false
14 | end
15 |
16 | describe "#to_s" do
17 | context "if called without arguments" do
18 |
19 | it "should return string with formatted hours and minutes" do
20 | Duration.new( 10.minutes ).to_s.should == "0:10"
21 | Duration.new( 3.hours + 25.minutes ).to_s.should == "3:25"
22 | Duration.new( 36.hours ).to_s.should == "36:00"
23 | end
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/app/controllers/exceptions.rb:
--------------------------------------------------------------------------------
1 | class Exceptions < Merb::Controller
2 |
3 | log_params_filtered :password
4 |
5 | # handle NotFound exceptions (404)
6 | def not_found
7 | "not found"
8 | end
9 |
10 | # handle NotAcceptable exceptions (406)
11 | def not_acceptable
12 | render :format => :html
13 | end
14 |
15 | # handle Forbidden exceptins (403)
16 | def forbidden
17 | "Permission denied"
18 | end
19 |
20 | def object_not_found_error
21 | end
22 |
23 | def bad_request
24 | render "Bad Request", :layout => false
25 | end
26 |
27 | def unauthenticated
28 | if request.ajax?
29 | "Permission denied"
30 | else
31 | render :template => "../../slices/merb-auth-slice-password/app/views/exceptions/unauthenticated"
32 | end
33 | end
34 |
35 | def ldap_connection_error
36 | redirect url(:login), :message => { :error => "There are connection problems, try again later." }
37 | end
38 |
39 | def number_of_columns
40 | 1
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/app/models/activity_custom_property_value.rb:
--------------------------------------------------------------------------------
1 | class ActivityCustomPropertyValue
2 | include DataMapper::Resource
3 |
4 | property :id, Serial
5 | property :activity_custom_property_id, Integer, :required => true
6 | property :activity_id, Integer, :required => true, :index => true
7 | property :numeric_value, BigDecimal, :scale => 2, :precision => 10
8 | property :updated_at, DateTime
9 | property :created_at, DateTime
10 |
11 | belongs_to :activity_custom_property
12 | belongs_to :activity
13 |
14 | validates_is_unique :activity_custom_property_id, :scope => :activity_id
15 | validates_present :value
16 |
17 | def value=(value)
18 | self.numeric_value = value.blank? ? nil : value
19 | end
20 |
21 | def value
22 | return nil if numeric_value.nil?
23 | numeric_value.to_i == numeric_value ? numeric_value.to_i : numeric_value.to_f
24 | end
25 |
26 | def incorrect_value?
27 | not valid? and (errors.on(:value) or errors.on(:numeric_value))
28 | end
29 |
30 | end
31 |
--------------------------------------------------------------------------------
/app/models/setting.rb:
--------------------------------------------------------------------------------
1 | class Setting
2 | include DataMapper::Resource
3 |
4 | ACCESS_KEY_CHARS = "1234567890qwrtpsdfghjklzxcvbnmQWRTPSDFGHJKLZXCVBNM"
5 |
6 | property :id, Serial
7 | property :enable_notifications, Boolean, :default => false, :required => true
8 | property :free_days_access_key, String, :default => Proc.new { Setting.generate_free_days_access_key }, :required => true
9 |
10 | before :valid? do
11 | generate_free_days_access_key if free_days_access_key.blank?
12 | end
13 |
14 | def self.get
15 | first || create
16 | end
17 |
18 | def self.enable_notifications
19 | get.enable_notifications
20 | end
21 |
22 | def generate_free_days_access_key
23 | self.free_days_access_key = Setting.generate_free_days_access_key
24 | end
25 |
26 | def self.free_days_access_key
27 | get.free_days_access_key
28 | end
29 |
30 | private
31 |
32 | def self.generate_free_days_access_key
33 | ACCESS_KEY_CHARS.generate_random(50)
34 | end
35 |
36 | end
37 |
--------------------------------------------------------------------------------
/app/views/users/show.html.erb:
--------------------------------------------------------------------------------
1 | Users details
2 |
3 | Name: <%= @user.name %>
4 |
5 | <% if @user.is_a?(Employee) %>
6 |
7 | Role: <%= link_to @user.role.name, resource(@user.role) %>
8 |
9 | <% end %>
10 | <% if @user.is_a?(ClientUser) %>
11 |
12 | Client: <%= link_to @user.client.name, resource(@user.client) %>
13 |
14 | <% end %>
15 |
16 | Login: <%= @user.login %>
17 |
18 |
19 | LDAP Login: <%= @user.ldap_login %>
20 |
21 |
22 | Email: <%= @user.email %>
23 |
24 |
25 | Active: <%= @user.active ? "Yes" : "No" %>
26 |
27 |
28 | Admin: <%= @user.admin ? "Yes" : "No" %>
29 |
30 |
31 | Created at: <%= @user.created_at.to_date.formatted(current_user.date_format) %>
32 |
33 | <% if @user.is_employee? %>
34 |
35 | <%= link_to "Show calendar", url(:user_calendar, @user) %>
36 |
37 | <% end %>
38 |
39 | <%= link_to 'Edit', url(:edit_user, @user) %> |
40 | <%= link_to 'Back', url(:users) %>
41 |
--------------------------------------------------------------------------------
/spec/controllers/home_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Home do
4 | context "when user is logged in" do
5 | it "should redirect to activities list" do
6 | response = as(:employee).dispatch_to(Home, :index)
7 | response.should redirect_to(resource(:activities))
8 | end
9 | end
10 |
11 | context "when admin is logged in" do
12 | it "should redirect to activities list" do
13 | response = as(:admin).dispatch_to(Home, :index)
14 | response.should redirect_to(resource(:activities))
15 | end
16 | end
17 |
18 | context "when client is logged in" do
19 | it "should redirect to activities list" do
20 | response = as(:client).dispatch_to(Home, :index)
21 | response.should redirect_to(resource(:activities))
22 | end
23 | end
24 |
25 | context "when no one is logged in" do
26 | it "should redirect to login page" do
27 | response = as(:guest).dispatch_to(Home, :index)
28 | response.should redirect_to(url(:login))
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/spec/extensions/date_extensions_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Date, '#weekend?' do
4 |
5 | it 'should return true for for weekend dates' do
6 | Date.parse('Saturday 2009-08-01').weekend?.should be_true
7 | end
8 |
9 | it 'should return false for for weekday dates' do
10 | Date.parse('Monday 2009-08-03').weekend?.should be_false
11 | end
12 |
13 | end
14 |
15 | describe Date, '#weekday?' do
16 |
17 | it 'should return true for for weekday dates' do
18 | Date.parse('Monday 2009-08-03').weekday?.should be_true
19 | end
20 |
21 | it 'should return false for weekend dates' do
22 | Date.parse('Saturday 2009-08-01').weekday?.should be_false
23 | end
24 |
25 | end
26 |
27 | describe Date, '#previous_weekday' do
28 |
29 | it "should return previous weekday" do
30 | Date.parse('Friday 2009-08-07').previous_weekday.should == Date.parse('Thursday 2009-08-06')
31 | Date.parse('Monday 2009-08-03').previous_weekday.should == Date.parse('Friday 2009-07-31')
32 | end
33 |
34 | end
35 |
--------------------------------------------------------------------------------
/spec/model_helper.rb:
--------------------------------------------------------------------------------
1 | module Rubytime
2 | module Test
3 | module ModelHelper
4 |
5 | def parse_factory_arguments(args)
6 | factory_name = (args.first.is_a?(Symbol) ? args.shift : self.name.snake_case).to_sym
7 | properties = args.first || {}
8 | [factory_name, properties]
9 | end
10 |
11 | def first_or_generate(attributes = {})
12 | first(attributes) || generate(attributes)
13 | end
14 |
15 | def generate(*args)
16 | Factory.create(*parse_factory_arguments(args))
17 | end
18 |
19 | def prepare(*args)
20 | Factory.build(*parse_factory_arguments(args))
21 | end
22 |
23 | def pick
24 | records = count
25 | (records > 0) ? first(:limit => 1, :offset => rand(records)) : nil
26 | end
27 |
28 | def pick_or_generate
29 | pick || generate
30 | end
31 |
32 | def prepare_hash(*args)
33 | Factory.attributes_for(*parse_factory_arguments(args))
34 | end
35 |
36 | end
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/spec/models/role_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Role do
4 |
5 | describe "can_manage_financial_data attribute" do
6 | it "should have default value false" do
7 | Role.new.can_manage_financial_data.should be_false
8 | end
9 | end
10 |
11 | describe ":name attribute writer" do
12 | context "for new records" do
13 | before { @role = Role.generate(:name => 'Original Name') }
14 |
15 | it "should normally assign values" do
16 | @role.name.should == 'Original Name'
17 | end
18 | end
19 |
20 | context "for existing records" do
21 | before { @role = Role.generate(:name => 'Original Name') }
22 |
23 | it "should raise Exception" do
24 | block_should(raise_error(Exception)) { @role.name = 'New Name' }
25 | end
26 |
27 | context "when assigning unchanged value" do
28 | it "should not raise any Exception" do
29 | block_should_not(raise_error(Exception)) { @role.name = 'Original Name' }
30 | end
31 | end
32 | end
33 | end
34 |
35 | end
36 |
--------------------------------------------------------------------------------
/app/models/money.rb:
--------------------------------------------------------------------------------
1 | class Money
2 |
3 | include Comparable
4 |
5 | attr_reader :value, :currency
6 |
7 | def initialize(value,currency)
8 | self.value = value
9 | self.currency = currency
10 | end
11 |
12 | def value=(value)
13 | raise ArgumentError if value.nil?
14 | @value = value
15 | end
16 |
17 | def currency=(currency)
18 | raise ArgumentError if currency.nil?
19 | @currency = currency
20 | end
21 |
22 | def to_s
23 | currency.render(value)
24 | end
25 |
26 | def as_json
27 | { :value => value, :currency => currency }
28 | end
29 |
30 | def +(other)
31 | raise ArgumentError unless currency == other.currency
32 | Money.new(value + other.value, currency)
33 | end
34 |
35 | def *(numeric)
36 | raise ArgumentError unless numeric.is_a?(Numeric)
37 | Money.new(value * numeric, currency)
38 | end
39 |
40 | def <=>(other)
41 | raise ArgumentError.new('Cannot compare different currencies') unless currency == other.currency
42 | value <=> other.value
43 | end
44 |
45 | end
46 |
--------------------------------------------------------------------------------
/app/views/users/edit.html.erb:
--------------------------------------------------------------------------------
1 | <%= partial(:user_form) %>
2 |
32 |
33 | <%= link_to 'Show', url(:user, @user) %>
34 |
--------------------------------------------------------------------------------
/lib/tasks/metric.rake:
--------------------------------------------------------------------------------
1 | begin
2 | require 'metric_fu'
3 |
4 | MetricFu::Configuration.run do |config|
5 | config.metrics = [:rcov, :churn, :saikuro, :flay, :reek, :roodi]
6 | config.graphs = [:rcov, :flay, :reek, :roodi]
7 | config.flay = { :dirs_to_flay => ['app', 'lib'] }
8 | config.flog = { :dirs_to_flog => ['app', 'lib'] }
9 | config.reek = { :dirs_to_reek => ['app', 'lib'] }
10 | config.roodi = { :dirs_to_roodi => ['app', 'lib'] }
11 | config.saikuro = { :output_directory => 'tmp/metric_fu/saikuro',
12 | :input_directory => ['app', 'lib'],
13 | :cyclo => "",
14 | :filter_cyclo => "0",
15 | :warn_cyclo => "5",
16 | :error_cyclo => "7",
17 | :formater => "text"} #this needs to be set to "text"
18 | config.churn = { :start_date => "1 year ago", :minimum_churn_count => 10}
19 | config.rcov = {
20 | :test_files => ['spec/**/*_spec.rb'],
21 | :rcov_opts => [ "--sort coverage", "--no-html", "--text-coverage", "--no-color", "--profile"]}
22 | end
23 | rescue LoadError; end
24 |
--------------------------------------------------------------------------------
/app/views/currencies/index.html.erb:
--------------------------------------------------------------------------------
1 | <% throw_content :secondary, partial(:new_currency) %>
2 | List of currencies
3 |
4 | <% if @currencies.empty? %>
5 | No currencies found.
6 | <% else %>
7 |
8 |
9 | Singular name
10 | Plural name
11 | Prefix
12 | Suffix
13 | Example
14 |
15 |
16 | <% @currencies.each do |currency| %>
17 |
18 | <%= currency.singular_name %>
19 | <%= currency.plural_name %>
20 | <%= currency.prefix %>
21 | <%= currency.suffix %>
22 | <%= currency.render(12345.67) %>
23 |
24 | <%= link_to image_tag("icons/pencil.png", :title => "Edit", :alt => "Edit"), resource(currency, :edit) %>
25 | <%= link_to image_tag("icons/cross.png", :title => "Remove", :alt => "Remove"), resource(currency), :class => "delete_row" %>
26 |
27 |
28 | <% end %>
29 |
30 | <% end %>
31 |
--------------------------------------------------------------------------------
/spec/controllers/settings_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Settings do
4 |
5 | it "shouldn't show any action for guest, employee and client's user" do
6 | [:edit, :update].each do |action|
7 | block_should(raise_unauthenticated) { as(:guest).dispatch_to(Settings, action) }
8 | block_should(raise_forbidden) { as(:employee).dispatch_to(Settings, action) }
9 | block_should(raise_forbidden) { as(:client).dispatch_to(Settings, action) }
10 | end
11 | end
12 |
13 | describe "GET" do
14 | it "responds successfully" do
15 | as(:admin).dispatch_to(Settings, :edit).should be_successful
16 | end
17 | end
18 |
19 | describe "PUT" do
20 | it "should update the setting record" do
21 | Setting.get.update :enable_notifications => true
22 | Setting.enable_notifications.should be_true
23 | response = as(:admin).dispatch_to(Settings, :update, :setting => { :enable_notifications => false })
24 | response.should redirect_to(url(:edit_settings))
25 | Setting.enable_notifications.should be_false
26 | end
27 | end
28 |
29 | end
30 |
--------------------------------------------------------------------------------
/spec/models/invoice_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Invoice do
4 |
5 | it "should be created" do
6 | block_should(change(Invoice, :count).by(1)) { Invoice.prepare.save.should be_true }
7 | end
8 |
9 | it "should be issued if issued_at date is set" do
10 | invoice1 = Invoice.generate
11 | invoice1.should_not be_issued
12 |
13 | invoice2 = Invoice.generate :issued_at => DateTime.now
14 | invoice2.should be_issued
15 | end
16 |
17 | describe "#issue!" do
18 | it "should mark invoice as issued" do
19 | invoice = Invoice.generate
20 | invoice.should_not be_issued
21 | invoice.issue!
22 | invoice.should be_issued
23 | end
24 | end
25 |
26 | context "if some new activities are invalid" do
27 | it "should not be valid" do
28 | invoice = Invoice.prepare
29 | invoice.new_activities = [Activity.new]
30 | invoice.should_not be_valid
31 |
32 | invoice.new_activities = [Activity.prepare, Activity.new]
33 | expect { invoice.valid? }.to_not(raise_error)
34 | invoice.should_not be_valid
35 | end
36 | end
37 |
38 | end
39 |
--------------------------------------------------------------------------------
/app/views/settings/edit.html.erb:
--------------------------------------------------------------------------------
1 | Settings
2 |
3 | <%= error_messages_for @setting %>
4 | <%= form_for(@setting, :action => resource(:settings), :id => "settings_form") do %>
5 |
6 | <%= fields_for :setting do %>
7 |
8 | <%= select :enable_notifications, :label => "Notifications:", :collection => [[true, "enabled"], [false, "disabled"]] %>
9 |
10 |
11 | <%= submit "Update", :class => "button" %>
12 |
13 | <% end =%>
14 |
15 | <% end =%>
16 |
17 | Security
18 |
19 | Free days iCal feed:
20 | <%= site_url + url(:free_days_index, :access_key => @setting.free_days_access_key, :format => :ics) %>
21 |
22 | <%= form_for(@setting, :action => resource(:settings), :id => "settings_form") do %>
23 |
24 | <%= fields_for :setting do %>
25 |
26 | <%= hidden_field :free_days_access_key, :value => '' %>
27 |
28 |
29 | <%= submit "Re-generate", :class => "button" %>
30 |
31 | <% end =%>
32 |
33 | <% end =%>
--------------------------------------------------------------------------------
/public/stylesheets/reset.css:
--------------------------------------------------------------------------------
1 | html, body, div, span, applet, object, iframe,
2 | h1, h2, h3, h4, h5, h6, p, blockquote, pre,
3 | a, abbr, acronym, address, big, cite, code,
4 | del, dfn, em, font, img, ins, kbd, q, s, samp,
5 | small, strike, strong, sub, sup, tt, var,
6 | dl, dt, dd, ol, ul, li,
7 | fieldset, form, label, legend,
8 | table, caption, tbody, tfoot, thead, tr, th, td {
9 | margin: 0;
10 | padding: 0;
11 | border: 0;
12 | outline: 0;
13 | font-weight: inherit;
14 | font-style: inherit;
15 | font-size: 100%;
16 | font-family: inherit;
17 | vertical-align: baseline;
18 | }
19 | /* remember to define focus styles! */
20 | :focus {
21 | outline: 0;
22 | }
23 | body {
24 | line-height: 1;
25 | color: black;
26 | background: white;
27 | }
28 | ol, ul {
29 | list-style: none;
30 | }
31 | /* tables still need 'cellspacing="0"' in the markup */
32 | table {
33 | border-collapse: separate;
34 | border-spacing: 0;
35 | }
36 | caption, th, td {
37 | text-align: left;
38 | font-weight: normal;
39 | }
40 | blockquote:before, blockquote:after,
41 | q:before, q:after {
42 | content: "";
43 | }
44 | blockquote, q {
45 | quotes: "" "";
46 | }
--------------------------------------------------------------------------------
/app/views/activity_types/show.html.erb:
--------------------------------------------------------------------------------
1 | <% throw_content :secondary, partial(:new_activity_type, :with => @new_activity_type, :as => :activity_type) if current_user.is_admin? %>
2 |
3 | Sub-activity types of <%= @activity_type.name %>
4 |
5 | <% if @activity_type.children.empty? %>
6 | The list is empty.
7 | <% else %>
8 |
9 |
10 | Name
11 |
12 |
13 | <% @activity_type.children.each do |activity_type| %>
14 |
15 | <%= activity_type.name %>
16 |
17 | <% if current_user.is_admin? %>
18 | <%= link_to image_tag("icons/pencil.png", :title => "Edit", :alt => "E"), resource(activity_type, :edit) %>
19 | <% if activity_type.destroy_allowed? %>
20 | <%= link_to image_tag("icons/cross.png", :title => "Remove", :alt => "R"), resource(activity_type), :class => "delete_row" %>
21 | <% end %>
22 | <% end %>
23 |
24 |
25 | <% end %>
26 |
27 | <% end %>
28 |
--------------------------------------------------------------------------------
/app/views/activities/_grouped_projects.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <% projects.each do |project| %>
3 |
4 | <%= image_tag("icons/client.png", :alt => 'client') %>
5 | Client: <%= client.name %>
6 | <%= image_tag("icons/project.png", :alt => 'project') %>
7 | Project: <%= project.name %>
8 | <% if current_user.is_employee? && !current_user.is_admin? %>
9 | <% activities = activities_from(@activities, client, project) %>
10 | <%= full_activities_table(activities) %>
11 | <% else %>
12 | <%= partial("activities/roles_summary_by_project", :roles => unique_roles_from(@activities, client, project), :client => client, :project => project) %>
13 | <%= partial("activities/grouped_roles", :roles => unique_roles_from(@activities, client, project), :client => client, :project => project) %>
14 | Total for project: <%= total_from(activities_from(@activities, client, project)) %>
15 | <%= total_custom_properties(@activities, client, project) %>
16 | <% end %>
17 |
18 | <% end %>
19 |
20 |
--------------------------------------------------------------------------------
/app/models/client.rb:
--------------------------------------------------------------------------------
1 | class Client
2 | include DataMapper::Resource
3 |
4 | property :id, Serial
5 | property :name, String, :required => true, :index => true
6 | property :description, Text
7 | property :email, String
8 | property :active, Boolean, :required => true, :default => true
9 |
10 | has n, :projects
11 | has n, :activities, :through => :projects
12 | has n, :invoices
13 | has n, :client_users
14 |
15 | before :destroy do
16 | throw :halt if invoices.count > 0
17 | throw :halt if activities.count > 0
18 | end
19 |
20 | before :destroy, :destroy_client_users_and_projects
21 |
22 | default_scope(:default).update(:order => [:name])
23 |
24 | def self.active
25 | all(:active => true)
26 | end
27 |
28 | protected
29 |
30 | def destroy_client_users_and_projects
31 | # TODO: 1. This code removes records without validation
32 | # TODO: 2. This code can leave garbage in the database (related records are not removed in a cascade)
33 | client_users.destroy!
34 | projects.each{|project| project.hourly_rates.destroy! }
35 | projects.destroy!
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/app/controllers/roles.rb:
--------------------------------------------------------------------------------
1 | class Roles < Application
2 | before :ensure_admin
3 | before :load_roles, :only => [:index, :create]
4 |
5 | def index
6 | provides :json
7 | @role = Role.new
8 | display @roles
9 | end
10 |
11 | def create
12 | @role = Role.new(params[:role])
13 | if @role.save
14 | redirect url(:roles)
15 | else
16 | render :index
17 | end
18 | end
19 |
20 | def edit
21 | @role = Role.get(params[:id]) or raise NotFound
22 | render
23 | end
24 |
25 | def update
26 | @role = Role.get(params[:id]) or raise NotFound
27 | if @role.update(params[:role]) || !@role.dirty?
28 | redirect resource(:roles)
29 | else
30 | render :edit
31 | end
32 | end
33 |
34 | def destroy
35 | raise NotFound unless @role = Role.get(params[:id])
36 | if @role.destroy
37 | render_success
38 | else
39 | render_failure "Users with this role exist. Couldn't delete."
40 | end
41 | end
42 |
43 | protected
44 | def load_roles
45 | @roles = Role.all(:order => [:name])
46 | end
47 |
48 | def number_of_columns
49 | params[:action] == "edit" ? 1 : super
50 | end
51 | end # Roles
52 |
--------------------------------------------------------------------------------
/app/models/user_version.rb:
--------------------------------------------------------------------------------
1 | # Poor man's implementation of dm-is-versioned, which apparently does not work with STI
2 | # at the time of this writing
3 |
4 | class UserVersion
5 | include DataMapper::Resource
6 |
7 | # some of user's properties were excluded from versioning
8 | # id = user's id, not version's id
9 | property :id, Integer
10 | property :name, String
11 | property :type, String
12 | property :login, String
13 | property :email, String
14 | property :admin, Boolean
15 | property :role_id, Integer
16 | property :client_id, Integer
17 | property :created_at, DateTime
18 | property :modified_at, DateTime # not updated_at on purpose
19 | property :date_format, Integer
20 | property :recent_days_on_list, Integer
21 | property :remind_by_email, Boolean
22 | property :version_id, Serial
23 |
24 | default_scope(:default).update(:order => [:version_id])
25 |
26 | belongs_to :role
27 | end
28 |
--------------------------------------------------------------------------------
/spec/rubytime_specs_helper.rb:
--------------------------------------------------------------------------------
1 | module Rubytime
2 | module Test
3 | module SpecsHelper
4 | private
5 |
6 | class BlockMatcher
7 | def initialize()
8 | @matchers = []
9 | end
10 |
11 | def and(matcher, &blk)
12 | @matchers << [:should, matcher]
13 | run(&blk) if block_given?
14 | self
15 | end
16 |
17 | def and_not(matcher, &blk)
18 | @matchers << [:should_not, matcher]
19 | run(&blk) if block_given?
20 | self
21 | end
22 |
23 | private
24 |
25 | def run(&blk)
26 | @matchers.inject(blk) { |memo, matcher|
27 | proc { memo.send matcher[0], matcher[1] }
28 | }.call
29 | end
30 | end
31 |
32 | def block_should(matcher, &blk)
33 | BlockMatcher.new.and(matcher, &blk)
34 | end
35 |
36 | def block_should_not(matcher, &blk)
37 | BlockMatcher.new.and_not(matcher, &blk)
38 | end
39 |
40 | def date(s)
41 | Date.parse(s)
42 | end
43 |
44 | def raise_argument_error
45 | raise_error ArgumentError
46 | end
47 | end
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/app/controllers/free_days.rb:
--------------------------------------------------------------------------------
1 | class FreeDays < Application
2 |
3 | before :ensure_authenticated, :exclude => [:index]
4 | before :prepare_source, :exclude => [:index]
5 |
6 | def index
7 | only_provides :ics
8 | raise Forbidden unless params[:access_key] == Setting.free_days_access_key
9 | render FreeDay.to_ical
10 | end
11 |
12 | def create
13 | @free_day = @free_days.new :date => params[:date]
14 | if @free_day.save
15 | render_success "You have just taken vacation at #{@free_day.date}"
16 | else
17 | render_failure "Couldn't take vacation"
18 | end
19 | end
20 |
21 | # note: it's a collection style action, because we identify the FreeDay by :date, not by its record id
22 | def delete
23 | @free_day = @free_days.first :date => params[:date]
24 | if @free_day && @free_day.destroy
25 | render_success "Vacation at #{params[:date]} was removed"
26 | else
27 | render_failure "Couldn't remove vacation"
28 | end
29 | end
30 |
31 |
32 | private
33 |
34 | def prepare_source
35 | user = Employee.get(params[:user_id]) if current_user.admin? && !params[:user_id].blank?
36 | user ||= current_user
37 | @free_days = user.free_days
38 | end
39 |
40 | end
41 |
--------------------------------------------------------------------------------
/app/views/activity_types/index.html.erb:
--------------------------------------------------------------------------------
1 | <% throw_content :secondary, partial(:new_activity_type, :with => @new_activity_type, :as => :activity_type) if current_user.is_admin? %>
2 |
3 | List of activity types
4 |
5 | <% if @activity_types.empty? %>
6 | The list is empty.
7 | <% else %>
8 |
9 |
10 | Name
11 | Sub activity types
12 |
13 |
14 | <% @activity_types.each do |activity_type| %>
15 |
16 | <%= link_to activity_type.name, resource(activity_type) %>
17 | <%= activity_type_children_summary(activity_type) %>
18 |
19 | <% if current_user.is_admin? %>
20 | <%= link_to image_tag("icons/pencil.png", :title => "Edit", :alt => "E"), resource(activity_type, :edit) %>
21 | <% if activity_type.destroy_allowed? %>
22 | <%= link_to image_tag("icons/cross.png", :title => "Remove", :alt => "R"), resource(activity_type), :class => "delete_row" %>
23 | <% end %>
24 | <% end %>
25 |
26 |
27 | <% end %>
28 |
29 | <% end %>
30 |
--------------------------------------------------------------------------------
/app/models/activity_type.rb:
--------------------------------------------------------------------------------
1 | class ActivityType
2 | include DataMapper::Resource
3 |
4 | property :id, Serial
5 | property :name, String, :required => true, :index => true
6 | property :parent_id, Integer
7 | property :position, Integer
8 | property :updated_at, DateTime
9 | property :created_at, DateTime
10 |
11 | is :tree, :order => :position
12 | is :list, :scope => [:parent_id]
13 | has n, :project_activity_types
14 | has n, :projects, :through => :project_activity_types
15 | has n, :activities
16 |
17 | validates_is_unique :name, :scope => :parent_id
18 |
19 | def breadcrumb_name
20 | parent ? "#{parent.breadcrumb_name} -> #{name}" : "#{name}"
21 | end
22 |
23 | before :destroy do
24 | children.each { |at| at.destroy }
25 | end
26 |
27 | def parent_id=(id)
28 | self.attribute_set :parent_id, (id.blank?) ? nil : id
29 | end
30 |
31 | def destroy_allowed?
32 | projects.empty? and activities.empty? and children.all? { |at| at.destroy_allowed? }
33 | end
34 |
35 | def destroy(force = false)
36 | return false unless destroy_allowed? or force
37 | super()
38 | end
39 |
40 | def json_hash
41 | { :id => id, :name => name, :position => position }
42 | end
43 |
44 | end
45 |
--------------------------------------------------------------------------------
/app/models/currency.rb:
--------------------------------------------------------------------------------
1 | class Currency
2 | include DataMapper::Resource
3 |
4 | PREFIX_REGEX = SUFFIX_REGEX = /^[^0-9]*$/
5 |
6 | property :id, Serial
7 | property :singular_name, String, :required => true
8 | property :plural_name, String, :required => true
9 | property :prefix, String
10 | property :suffix, String
11 |
12 | validates_is_unique :singular_name, :plural_name
13 | validates_format :prefix, :with => PREFIX_REGEX, :if => proc { |c| not c.prefix.nil? }
14 | validates_format :suffix, :with => SUFFIX_REGEX, :if => proc { |c| not c.suffix.nil? }
15 | validates_with_method :singular_name, :method => :validates_singular_name_format
16 | validates_with_method :plural_name, :method => :validates_plural_name_format
17 |
18 | default_scope(:default).update(:order => [:singular_name])
19 |
20 | def render(value)
21 | "#{prefix}#{format('%.2f',value)}#{suffix}"
22 | end
23 |
24 | [:singular_name, :plural_name].each do |attr|
25 | define_method "validates_#{attr}_format" do
26 | if self.send(attr)=~ /^[\w ]*$/ && self.send(attr) !~ /[\d_]/
27 | true
28 | else
29 | [false, "#{attr.to_s.gsub(/_/, ' ').capitalize} has an invalid format."]
30 | end
31 | end
32 | end
33 |
34 | end
35 |
--------------------------------------------------------------------------------
/app/models/observers/user_observer.rb:
--------------------------------------------------------------------------------
1 | class UserObserver
2 | include DataMapper::Observer
3 |
4 | observe User
5 |
6 | before :save do
7 | self.version(DateTime.now) unless new? # force creation of first version if it should exist but it doesn't
8 |
9 | # not using updated_at because of user versioning
10 | # for sake of simplicity we want this property to be called the same in User and UserVersion
11 | # if it was called updated_at, it would be overwritten when saving the UserVersion object
12 | self.modified_at = DateTime.now
13 | @role_changed = self.attribute_dirty?(:role_id)
14 | end
15 |
16 | after :save do
17 | save_new_version if self.versions.count == 0 || @role_changed
18 | end
19 |
20 | after :create do
21 | m = UserMailer.new(:user => self)
22 | m.dispatch_and_deliver(:welcome, :to => self.email, :from => Rubytime::CONFIG[:mail_from], :subject => "Welcome to Rubytime!")
23 | end
24 |
25 | before :destroy do
26 | versions.all.destroy!
27 | end
28 |
29 | after :generate_password_reset_token do
30 | m = UserMailer.new(:user => self)
31 | m.dispatch_and_deliver(:password_reset_link, :to => self.email,
32 | :from => Rubytime::CONFIG[:mail_from], :subject => "Password reset request from Rubytime")
33 | end
34 |
35 | end
36 |
--------------------------------------------------------------------------------
/slices/merb-auth-slice-password/app/views/exceptions/unauthenticated.html.erb:
--------------------------------------------------------------------------------
1 | <% @login_param = Merb::Authentication::Strategies::Basic::Base.login_param %>
2 | <% @password_param = Merb::Authentication::Strategies::Basic::Base.password_param %>
3 |
4 | Welcome to Rubytime, please sign in:
5 |
6 | <%= error_messages_for session.authentication, :header => '' %>
7 |
8 |
27 |
28 |
29 | Forgot your password? Click <%= link_to 'here', resource(:users, :request_password) %>
30 |
31 |
--------------------------------------------------------------------------------
/app/models/free_day.rb:
--------------------------------------------------------------------------------
1 | class FreeDay
2 | include DataMapper::Resource
3 |
4 | property :id, Serial
5 | property :user_id, Integer, :required => true, :index => true
6 | property :date, Date, :required => true, :index => true
7 |
8 | belongs_to :user
9 |
10 | def self.ranges
11 | ranges = []
12 | all.group_by { |free_day| free_day.user }.each do |user, free_days|
13 | ranges += user_ranges(free_days)
14 | end
15 | ranges
16 | end
17 |
18 | def self.to_ical
19 | cal = Icalendar::Calendar.new
20 |
21 | all.ranges.each do |range|
22 | cal.event do
23 | dtstart range[:start_date]
24 | dtend range[:end_date] + 1 # DTEND property specifies the non-inclusive end of the event, that's why "+1"
25 | summary range[:user].name
26 | end
27 | end
28 |
29 | cal.to_ical
30 | end
31 |
32 | private
33 |
34 | def self.user_ranges(free_days)
35 | free_days.sort_by{|fd|fd.date}.inject([]) do |ranges,free_day|
36 | if ranges.last and ranges.last[:end_date] + 1 == free_day.date
37 | ranges.last[:end_date] = free_day.date
38 | else
39 | ranges << { :start_date => free_day.date, :end_date => free_day.date, :user => free_day.user }
40 | end
41 | ranges
42 | end
43 | end
44 |
45 | end
--------------------------------------------------------------------------------
/app/controllers/currencies.rb:
--------------------------------------------------------------------------------
1 | class Currencies < Application
2 | before :ensure_admin
3 | before :load_currencies, :only => [:index, :create]
4 | before :load_currency, :only => [:destroy]
5 |
6 | def index
7 | @currency = Currency.new
8 | render
9 | end
10 |
11 | def create
12 | @currency = Currency.new(params[:currency])
13 | if @currency.save
14 | redirect resource(:currencies)
15 | else
16 | render :index
17 | end
18 | end
19 |
20 | def edit
21 | @currency = Currency.get(params[:id]) or raise NotFound
22 | render
23 | end
24 |
25 | def update
26 | @currency = Currency.get(params[:id]) or raise NotFound
27 | if @currency.update(params[:currency]) || !@currency.dirty?
28 | redirect resource(:currencies)
29 | else
30 | render :edit
31 | end
32 | end
33 |
34 | def destroy
35 | if @currency.destroy
36 | render_success
37 | else
38 | render_failure "Currency is in use and cannot be deleted."
39 | end
40 | end
41 |
42 |
43 | private
44 |
45 | def load_currencies
46 | @currencies = Currency.all
47 | end
48 |
49 | def load_currency
50 | @currency = Currency.get(params[:id]) or raise NotFound
51 | end
52 |
53 | def number_of_columns
54 | params[:action] == "edit" ? 1 : super
55 | end
56 |
57 | end # Currencies
58 |
--------------------------------------------------------------------------------
/app/views/projects/index.html.erb:
--------------------------------------------------------------------------------
1 | <% throw_content :secondary, partial(:new_project) if current_user.is_admin? %>
2 | List of projects
3 |
4 | <% if @projects.empty? %>
5 | No projects found.
6 | <% else %>
7 |
8 |
9 | Name
10 | <% if current_user.is_admin? %>
11 | Client
12 | <% end %>
13 | Active
14 |
15 |
16 | <% @projects.each do |project| %>
17 |
18 | <%= current_user.is_admin? ? link_to(project.name, resource(project)) : project.name %>
19 | <% if current_user.is_admin? %>
20 | <%= link_to project.client.name, resource(project.client) %>
21 | <% end %>
22 | <%= project.active ? 'Yes' : 'No' %>
23 |
24 | <%= link_to image_tag("icons/calendar.png", :title => "Show calendar for this project", :alt => "C"), resource(project, :calendar) %>
25 | <% if current_user.is_admin? %>
26 | <%= link_to image_tag("icons/pencil.png", :title => "Edit", :alt => "E"), resource(project, :edit) %>
27 | <%= link_to image_tag("icons/cross.png", :title => "Remove", :alt => "R"), resource(project), :class => "delete_row" %>
28 | <% end %>
29 |
30 |
31 | <% end %>
32 |
33 | <% end %>
34 |
--------------------------------------------------------------------------------
/app/views/invoices/index.html.erb:
--------------------------------------------------------------------------------
1 | <% throw_content :secondary, partial(:new_invoice) if current_user.is_admin? %>
2 | List of <%= (params[:filter] || 'all').downcase %> invoices
3 |
4 | <% if @invoices.empty? %>
5 | No invoices found.
6 | <% else %>
7 |
8 |
9 | Name
10 | <% unless current_user.is_client_user? %>Client <% end %>
11 | Created at
12 | Issued at
13 |
14 |
15 | <% @invoices.each do |invoice| %>
16 |
17 | <%= link_to(invoice.name, resource(invoice, filter_hash)) %>
18 | <% unless current_user.is_client_user? %><%= invoice.client.name %> <% end %>
19 | <%= invoice.created_at.to_time.to_date.formatted(current_user.date_format) %>
20 | <%= invoice.issued? ? invoice.issued_at.to_time.to_date.formatted(current_user.date_format) : "-" %>
21 |
22 | <%= link_to(image_tag("icons/pencil.png", :alt => "E", :title => "Edit"), resource(invoice, :edit, filter_hash)) if current_user.is_admin? %>
23 | <%= link_to(image_tag("icons/cross.png", :alt => "R", :title => "Remove"), resource(invoice, filter_hash), :class => :delete_row) unless current_user.is_client_user? %>
24 |
25 | <% end %>
26 |
27 | <% end %>
28 |
--------------------------------------------------------------------------------
/app/views/clients/_new_client.html.erb:
--------------------------------------------------------------------------------
1 | <%= error_messages_for @client %>
2 | <%= error_messages_for @client_user %>
3 | <%= form_for @client, :action => resource(:clients), :id => "client_form" do %>
4 |
5 | Add new client
6 |
7 | <%= text_field :name, :label => "Name" %>
8 |
9 |
10 | <%= text_area :description, :label => "Description", :class => "text", :rows => 5, :cols => 20 %>
11 |
12 |
13 | <%= text_field :email, :label => "Email" %>
14 |
15 |
16 | Default user for client:
17 |
18 | <%= fields_for @client_user do %>
19 |
20 | <%= text_field :name, :label => "Name" %>
21 |
22 |
23 | <%= text_field :login, :label => "Login" %>
24 |
25 |
26 | <%= text_field :email, :label => "Email" %>
27 |
28 |
29 | <%= password_field :password, :label => "Password" %>
30 |
31 | Generated password: <%= @client_user.password %>
32 |
33 |
34 |
35 | <%= password_field :password_confirmation, :label => "Confirm password" %>
36 |
37 | <% end =%>
38 |
39 | <%= submit "Create", :class => "button" %>
40 |
41 |
42 | <% end =%>
43 |
--------------------------------------------------------------------------------
/app/views/clients/show.html.erb:
--------------------------------------------------------------------------------
1 | Client
2 |
3 | Name: <%= @client.name %>
4 |
5 |
6 | Description: <%= @client.description %>
7 |
8 |
9 | Email: <%= @client.email %>
10 |
11 |
12 | Active: <%= @client.active ? "Yes" : "No" %>
13 |
14 |
15 | <%= link_to "Edit", url(:edit_client, @client) %>
16 |
17 |
18 |
19 | Client's projects:
20 |
21 | <% @client.projects.each do |project| %>
22 |
23 | <%= project.name %>
24 | <%= link_to "Show", url(:project, project) %>
25 | <%= link_to "Edit", url(:edit_project, project) %>
26 |
27 | <% end %>
28 |
29 |
30 | <% if current_user.is_admin? %>
31 | <%= link_to 'Create new project', resource(:projects, :client_id => @client.id) %>
32 | <% end %>
33 |
34 |
35 | Client's users:
36 |
37 | <% @client.client_users.each do |user| %>
38 |
39 | <%= user.name %>
40 | <%= user.login %>
41 | <%= link_to "Show", url(:user, user) %>
42 | <%= link_to "Edit", url(:edit_user, user) %>
43 |
44 | <% end %>
45 |
46 |
47 | <% if current_user.is_admin? %>
48 | <%= link_to 'Create new user', resource(:users, :client_id => @client.id) %>
49 | <% end %>
50 |
--------------------------------------------------------------------------------
/spec/controller_helper.rb:
--------------------------------------------------------------------------------
1 | module Rubytime
2 | module Test
3 | module ControllerHelper
4 | private
5 |
6 | class As
7 | def initialize(user, spec)
8 | @user = case user
9 | when :admin then Employee.generate(:admin)
10 | when :employee then Employee.generate
11 | when :client then ClientUser.generate
12 | when :guest then nil
13 | else user
14 | end
15 | @spec = spec
16 | end
17 |
18 | def dispatch_to(controller_klass, action, params = {}, env = {}, &blk)
19 | @spec.dispatch_to(controller_klass, action, params, env) do |controller|
20 | controller.session.user = @user
21 | blk.call(controller) if block_given?
22 | controller
23 | end
24 | end
25 | end
26 |
27 | def as(user)
28 | As.new(user, self)
29 | end
30 |
31 | def raise_not_found
32 | raise_error Merb::Controller::NotFound
33 | end
34 |
35 | def raise_forbidden
36 | raise_error Merb::Controller::Forbidden
37 | end
38 |
39 | def raise_unauthenticated
40 | raise_error Merb::Controller::Unauthenticated
41 | end
42 |
43 | def raise_bad_request
44 | raise_error Merb::Controller::BadRequest
45 | end
46 | end
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/app/views/activity_custom_properties/index.html.erb:
--------------------------------------------------------------------------------
1 | <% throw_content :secondary, partial(:new_activity_custom_property, :with => @new_activity_custom_property, :as => :activity_custom_property) if current_user.is_admin? %>
2 |
3 | List of custom activity properties
4 |
5 | <% if @activity_custom_properties.empty? %>
6 | The list is empty.
7 | <% else %>
8 |
9 |
10 | Name
11 | Unit
12 | Required
13 | Show as column
14 |
15 |
16 | <% @activity_custom_properties.each do |custom_property| %>
17 |
18 | <%= custom_property.name %>
19 | <%= custom_property.unit %>
20 | <%= custom_property.required ? 'Yes' : 'No' %>
21 | <%= custom_property.show_as_column_in_tables ? 'Yes' : 'No' %>
22 |
23 | <% if current_user.is_admin? %>
24 | <%= link_to image_tag("icons/pencil.png", :title => "Edit", :alt => "E"), resource(custom_property, :edit) %>
25 | <% if custom_property.destroy_allowed? %>
26 | <%= link_to image_tag("icons/cross.png", :title => "Remove", :alt => "R"), resource(custom_property), :class => "delete_row" %>
27 | <% end %>
28 | <% end %>
29 |
30 |
31 | <% end %>
32 |
33 | <% end %>
34 |
--------------------------------------------------------------------------------
/app/mailers/user_mailer.rb:
--------------------------------------------------------------------------------
1 | class UserMailer < Merb::MailController
2 | def welcome
3 | @user = params[:user]
4 | @url = Rubytime::CONFIG[:site_url]
5 | render_mail
6 | end
7 |
8 | def password_reset_link
9 | @user = params[:user]
10 | @url = Rubytime::CONFIG[:site_url]
11 | render_mail
12 | end
13 |
14 | def notice
15 | @missed_days = params[:missed_days]
16 | @user = params[:user]
17 | @url = Rubytime::CONFIG[:site_url]
18 | render_mail
19 | end
20 |
21 | def timesheet_nagger
22 | @day_without_activities = params[:day_without_activities]
23 | @user = params[:user]
24 | @url = Rubytime::CONFIG[:site_url]
25 | render_mail
26 | end
27 |
28 | def timesheet_reporter
29 | @day_without_activities = params[:day_without_activities]
30 | @employees_without_activities = params[:employees_without_activities]
31 | @url = Rubytime::CONFIG[:site_url]
32 | render_mail
33 | end
34 |
35 | def timesheet_summary
36 | @dates_range = params[:dates_range]
37 | @activities_by_dates_and_projects = params[:activities_by_dates_and_projects]
38 | @user = params[:user]
39 | @url = Rubytime::CONFIG[:site_url]
40 | render_mail
41 | end
42 |
43 | def timesheet_changes_notifier
44 | @project_manager = params[:project_manager]
45 | @kind_of_change = params[:kind_of_change]
46 | @activity = params[:activity]
47 | @url = Rubytime::CONFIG[:site_url]
48 | render_mail
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/app/controllers/clients.rb:
--------------------------------------------------------------------------------
1 | class Clients < Application
2 | before :ensure_admin
3 | before :load_client, :only => [:show, :edit, :destroy, :update]
4 | before :load_clients, :only => [:index, :create]
5 |
6 | def show
7 | render
8 | end
9 |
10 | def create
11 | @client = Client.new(params[:client])
12 | @client_user = ClientUser.new(params[:client_user].merge(:client => @client))
13 | if @client_user.valid? && @client.valid?
14 | @client_user.save
15 | @client.save
16 | redirect resource(@client)
17 | else
18 | render :index
19 | end
20 | end
21 |
22 | def index
23 | @client_user = ClientUser.new
24 | @client_user.generate_password!
25 | @client = Client.new
26 | render
27 | end
28 |
29 | def edit
30 | render
31 | end
32 |
33 | def update
34 | if @client.update(params[:client]) || !@client.dirty?
35 | redirect resource(@client)
36 | else
37 | render :edit
38 | end
39 | end
40 |
41 | def destroy
42 | if @client.destroy
43 | render_success
44 | else
45 | render_failure("Couldn't delete client which has invoices")
46 | end
47 | end
48 |
49 | protected
50 |
51 | def load_client
52 | raise NotFound unless @client = Client.get(params[:id])
53 | end
54 |
55 | def load_clients
56 | @clients = Client.all(:order => [:name])
57 | end
58 |
59 | def number_of_columns
60 | params[:action] == "show" || params[:action] == "edit" ? 1 : super
61 | end
62 | end # Clients
--------------------------------------------------------------------------------
/public/stylesheets/master.css:
--------------------------------------------------------------------------------
1 | @import '/stylesheets/reset.css';
2 | @import '/stylesheets/layout.css';
3 | @import '/stylesheets/tables.css';
4 | @import '/stylesheets/forms.css';
5 | @import '/stylesheets/thickbox.css';
6 | @import '/stylesheets/jquery-ui-1.8.custom.css';
7 | @import '/stylesheets/hourly_rates.css';
8 |
9 | body {
10 | margin: 0 0;
11 | background-color: #dddddd;
12 | color: #555;
13 | font-family: 'Lucida Grande','Lucida Sans Unicode',Helvetica,Arial,sans-serif;
14 | }
15 | h1,h2,h3,h4,h5,h6 {
16 | font-weight:900;
17 | margin-bottom:20px;
18 | }
19 | h2 {
20 | color: #083056;
21 | font-size:24px;
22 | }
23 | h3 {
24 | color: #0c457c;
25 | font-size:16px;
26 | }
27 | p {
28 | line-height:1.25em;
29 | }
30 | strong {
31 | font-weight: 900;
32 | }
33 | /* clearfix - http://www.webtoolkit.info/css-clearfix.html */
34 | .clearfix:after {
35 | content: ".";
36 | display: block;
37 | clear: both;
38 | visibility: hidden;
39 | line-height: 0;
40 | height: 0;
41 | }
42 | .clearfix {
43 | display: inline-block;
44 | }
45 | html[xmlns] .clearfix {
46 | display: block;
47 | }
48 | * html .clearfix {
49 | height: 1%;
50 | }
51 | /* common elements */
52 | .hidden {
53 | display:none;
54 | visibility:hidden;
55 | }
56 | .box {
57 | background:#eee;
58 | border:1px solid #ddd;
59 | padding:10px;
60 | }
61 | h1.logo {
62 | margin:0 0 0 -70px;
63 | height:100px;
64 | }
65 | a {
66 | color:#0f5ba3;
67 | text-decoration:underline;
68 | }
69 | a:hover {
70 | color:#0b3f6f;
71 | text-decoration:none;
72 | }
73 |
--------------------------------------------------------------------------------
/merb/merb-auth/strategies.rb:
--------------------------------------------------------------------------------
1 | # This file is specifically for you to define your strategies
2 | #
3 | # You should declare you strategies directly and/or use
4 | # Merb::Authentication.activate!(:label_of_strategy)
5 | #
6 | # To load and set the order of strategy processing
7 |
8 | Merb::Authentication.activate!(:default_password_form)
9 | Merb::Authentication.activate!(:default_basic_auth)
10 |
11 | module Merb::Authentication::Strategies
12 | class CookieStrategy < Merb::Authentication::Strategy
13 | def run!
14 | (token = cookies[:remember_me_token]) && User.authenticate_with_token(token)
15 | end
16 | end
17 |
18 | class PasswordFormWithTokenStrategy < Merb::Authentication::Strategies::Basic::Form
19 | def run!
20 | user = super
21 |
22 | if user && params[:remember_me] == "1"
23 | user.remember_me!
24 | token = user.remember_me_token
25 | expires = user.remember_me_token_expiration.to_time
26 |
27 | cookies.set_cookie('remember_me_token', token, :expires => expires)
28 | end
29 |
30 | user
31 | end
32 | end
33 |
34 | class RequestedBasicAuthStrategy < Merb::Authentication::Strategies::Basic::BasicAuth
35 | def run!
36 | (user = super) || (request.params["auth"] == "basic") && request_basic_auth!
37 | end
38 | end
39 | end
40 |
41 | Merb::Authentication.default_strategy_order = [
42 | Merb::Authentication::Strategies::CookieStrategy,
43 | Merb::Authentication::Strategies::PasswordFormWithTokenStrategy,
44 | Merb::Authentication::Strategies::RequestedBasicAuthStrategy
45 | ]
46 |
--------------------------------------------------------------------------------
/app/views/activities/_invoice_forms.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <% clients_to_invoice = @uninvoiced_activities.map { |a| a.project.client }.uniq %>
3 | <% if clients_to_invoice.size == 1 %>
4 | <% client = clients_to_invoice.first %>
5 | <%= form_for @invoice, :action => url(:invoices), :id => "create_invoice_form" do %>
6 | <%= hidden_field :client_id, :value => client.id %>
7 |
8 |
9 | <%= image_tag("icons/notebook_plus.png", :alt => "I") %> Create invoice for <%= client.name %> from selected activities
10 |
11 |
12 | <%= text_field :name, :label => "Invoice name" %>
13 |
14 |
15 | <%= submit "Create", :class => "button", :id => "create_invoice_button" %>
16 |
17 |
18 |
19 |
20 |
21 | <%= image_tag("icons/notebook_plus.png", :alt => "I") %> Add selected activities to existing invoice for <%= client.name %>
22 |
23 |
24 | <%= select :id => "invoice_id", :label => "Invoice", :collection => client.invoices.pending.all.map { |i| [i.id, i.name] }, :prompt => "Select an invoice..." %>
25 |
26 |
27 | <%= submit "Add", :class => "button", :id => "update_invoice_button" %>
28 |
29 |
30 | <% end =%>
31 | <% else %>
32 | If you want to create invoice from selected activities please refine your search to include activities for only one client.
33 | <% end %>
34 |
35 |
--------------------------------------------------------------------------------
/CHANGELOG-API.txt:
--------------------------------------------------------------------------------
1 | Please update this file and increment Application::JSON_API_VERSION if any changes are made to the JSON API (this
2 | includes only the functionality used by external clients (Plasma widget, iPhone app, etc.) - JSON methods used by AJAX
3 | actions are irrelevant in this context).
4 |
5 | This is done so that it's possible to handle incompatibilities between client and server versions (e.g. iPhone app has
6 | been updated to the newest version which uses some new API actions, but the server wasn't).
7 |
8 | Version 4 - June 16, 2011
9 |
10 | Changes:
11 | * Added price_as_json (with currency) and role_name to Activity JSON response in Activities#index
12 | * Added JSON response type in Roles#index and Users#index
13 |
14 | -----
15 |
16 | Version 3 - January 5, 2011
17 |
18 | Changes:
19 | * Added include_activity_types option in /projects.json
20 |
21 | -----
22 |
23 | Version 2 - December 16, 2010
24 |
25 | Changes:
26 | * Added price_as_json and role_name to /activities.json
27 |
28 | -----
29 |
30 | Version 1 - November 16, 2009
31 |
32 | API actions available:
33 | * Activities index
34 | - with locked? field added
35 | - also /users/x/activities and /projects/x/activities
36 | * Activities create
37 | * Activities update
38 | * Activities destroy
39 | * Projects index
40 | - adds has_activities flag which says if current user has at least one activity in that project
41 | * Users with_activities
42 | - for admin, returns all users with any activities
43 | - for client user, returns all users with any activities in any of client's projects
44 | * Users authenticate
45 | - returns current_user as JSON
46 | - adds user_type field
47 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 |
3 | require 'dm-core'
4 | require 'merb-core'
5 | require 'merb-mailer'
6 | require 'spec'
7 |
8 | # this loads all plugins required in your init file so don't add them
9 | # here again, Merb will do it for you
10 | Merb.start_environment(:testing => true, :adapter => 'runner', :environment => ENV['MERB_ENV'] || 'test')
11 | Merb::Mailer.delivery_method = :test_send
12 |
13 | require Merb.root / 'spec/factory_patch'
14 | require Merb.root / 'spec/rubytime_factories' # note: not named 'factories' to disable auto-loading
15 | require Merb.root / 'spec/matchers'
16 | require Merb.root / 'spec/rubytime_specs_helper'
17 | require Merb.root / 'spec/controller_helper'
18 | require Merb.root / 'spec/mailer_helper'
19 | require Merb.root / 'spec/model_helper'
20 |
21 | DataMapper.auto_migrate!
22 | DataMapper::Model.append_extensions(Rubytime::Test::ModelHelper)
23 |
24 | Spec::Runner.configure do |config|
25 | config.include(Merb::Test::RouteHelper)
26 | config.include(Merb::Test::ControllerHelper)
27 | config.include(Rubytime::Test::ControllerHelper)
28 | config.include(Rubytime::Test::SpecsHelper)
29 | config.include(Rubytime::Test::MailerHelper)
30 | config.include(Delorean)
31 |
32 | config.after(:each) do
33 | repository(:default) do
34 | while repository.adapter.current_transaction
35 | repository.adapter.current_transaction.rollback
36 | repository.adapter.pop_transaction
37 | end
38 | end
39 | end
40 |
41 | config.before(:each) do
42 | repository(:default) do
43 | transaction = DataMapper::Transaction.new(repository)
44 | transaction.begin
45 | repository.adapter.push_transaction(transaction)
46 | end
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/app/models/invoice.rb:
--------------------------------------------------------------------------------
1 | class Invoice
2 | include DataMapper::Resource
3 |
4 | property :id, Serial
5 | property :name, String, :required => true, :unique => true, :index => true
6 | property :notes, Text
7 | property :user_id, Integer, :required => true, :index => true
8 | property :client_id, Integer, :required => true, :index => true
9 | property :issued_at, DateTime
10 | property :created_at, DateTime
11 |
12 | belongs_to :client
13 | belongs_to :user
14 | has n, :activities
15 |
16 | attr_accessor :new_activities
17 |
18 | validates_with_method :activities, :method => :validate_activities
19 |
20 | after :save do
21 | unless new_activities.blank?
22 | new_activities.each { |activity| activity.update(:invoice_id => id) }
23 | end
24 | end
25 |
26 | before :destroy do
27 | throw :halt if issued?
28 | activities.update!(:invoice_id => nil)
29 | end
30 |
31 | def validate_activities
32 | if new_activities.blank? || new_activities.all?(&:valid?)
33 | true
34 | else
35 | errors = new_activities.map { |a| a.errors.full_messages.uniq }.flatten
36 | [false, "Some of activities are invalid (#{errors.join("; ")})."]
37 | end
38 | end
39 |
40 | def self.pending
41 | all(:issued_at => nil)
42 | end
43 |
44 | def self.issued
45 | all(:issued_at.not => nil)
46 | end
47 |
48 | def issue!
49 | # transaction do
50 | activities.each { |a| a.freeze_price! }
51 | update(:issued_at => DateTime.now)
52 | # end
53 | end
54 |
55 | def issued?
56 | !self.issued_at.nil?
57 | end
58 |
59 | def activity_id=(ids)
60 | self.new_activities = Activity.all(:id => ids, :invoice_id => nil)
61 | end
62 | end
63 |
--------------------------------------------------------------------------------
/app/models/hourly_rate_log.rb:
--------------------------------------------------------------------------------
1 | class HourlyRateLog
2 | include DataMapper::Resource
3 |
4 | VALID_OPERATION_TYPES = ['create', 'update', 'destroy']
5 | ATTRIBUTES_TO_LOG = [:project_id, :role_id, :takes_effect_at, :value, :currency_id]
6 |
7 | property :id, Serial
8 | property :logged_at, DateTime
9 | property :operation_type, String, :required => true
10 | property :operation_author_id, Integer, :required => true
11 | property :hr_id, Integer, :writer => :private
12 | property :hr_project_id, Integer, :writer => :private
13 | property :hr_role_id, Integer, :writer => :private
14 | property :hr_takes_effect_at, Date, :writer => :private
15 | property :hr_value, BigDecimal, :writer => :private, :scale => 2, :precision => 10
16 | property :hr_currency_id, Integer, :writer => :private
17 |
18 | belongs_to :operation_author, :model => User, :child_key => [:operation_author_id]
19 | belongs_to :hr_currency, :model => Currency, :child_key => [:hr_currency_id]
20 |
21 | default_scope(:default).update(:order => [:logged_at])
22 |
23 | validates_within :operation_type, :set => VALID_OPERATION_TYPES
24 | validates_present :hr_id, :message => 'No hourly rate assigned'
25 |
26 | def hourly_rate=(hourly_rate)
27 | (ATTRIBUTES_TO_LOG + [:id]).each do |attr_to_log|
28 | attribute_set("hr_#{attr_to_log}", hourly_rate ? hourly_rate.attribute_get(attr_to_log) : nil)
29 | end
30 | end
31 |
32 | before :save do
33 | self.logged_at ||= DateTime.now
34 |
35 | if operation_type == 'destroy'
36 | ATTRIBUTES_TO_LOG.each do |attr_not_to_log|
37 | attribute_set("hr_#{attr_not_to_log}", nil)
38 | end
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/config/init.rb:
--------------------------------------------------------------------------------
1 | Bundler.require :default, ENV['RACK_ENV'] || ENV['MERB_ENV'] || :development
2 |
3 | use_orm :datamapper
4 | use_test :rspec
5 | use_template_engine :erb
6 |
7 | Merb::Config.use do |c|
8 | c[:use_mutex] = false
9 | c[:session_store] = 'cookie' # can also be 'memory', 'memcache', 'container', 'datamapper
10 | # cookie session store configuration
11 | c[:session_secret_key] = '1205346b9baa87cf8e49f78124c8d17a31ac0971' # required for cookie session store
12 | # c[:session_id_key] = '_session_id' # cookie session id key, defaults to "_session_id"
13 | end
14 |
15 | Merb::BootLoader.before_app_loads do
16 | # This will get executed after dependencies have been loaded but before your app's classes have loaded.
17 | Merb.add_mime_type(:csv, :to_csv, %w[text/csv])
18 | Merb.add_mime_type(:ics, :to_ics, %w[text/calendar])
19 | Merb::Mailer.delivery_method = :sendmail
20 | require Merb.root / "lib/rubytime/misc"
21 | require Merb.root / "lib/auth/ldap"
22 | end
23 |
24 | Merb::BootLoader.after_app_loads do
25 | # This will get executed after your app's classes have been loaded.
26 | Rubytime::DATE_FORMATS.each do |name, options|
27 | Date.add_format(name, options[:format])
28 | end
29 |
30 | Rubytime::Misc.check_activity_roles
31 |
32 | require Merb.root / "config/local_config.rb"
33 |
34 | if Rubytime::CONFIG[:exception_report_email]
35 | Merb::Plugins.config[:exceptions] = {
36 | :email_addresses => [Rubytime::CONFIG[:exception_report_email]],
37 | :app_name => "RubyTime",
38 | :environments => ['production', 'staging'],
39 | :email_from => Rubytime::CONFIG[:mail_from],
40 | :mailer_config => nil,
41 | :mailer_delivery_method => :sendmail
42 | }
43 | end
44 |
45 | Dir[ Merb.root / "lib/extensions/*.rb" ].each { |filename| require filename }
46 | end
47 |
--------------------------------------------------------------------------------
/app/controllers/activity_custom_properties.rb:
--------------------------------------------------------------------------------
1 | class ActivityCustomProperties < Application
2 |
3 | before :ensure_admin
4 | before :load_activity_custom_property, :only => [:edit, :update, :destroy]
5 |
6 | def index
7 | @new_activity_custom_property = ActivityCustomProperty.new
8 | @activity_custom_properties = ActivityCustomProperty.all
9 | display @activity_custom_properties
10 | end
11 |
12 | def create
13 | @new_activity_custom_property = ActivityCustomProperty.new(params[:activity_custom_property])
14 | if @new_activity_custom_property.save
15 | redirect resource(:activity_custom_properties),
16 | :message => {:notice => "Custom property was successfully created"}
17 | else
18 | @activity_custom_properties = ActivityCustomProperty.all
19 | render :index
20 | end
21 | end
22 |
23 | def edit
24 | display @activity_custom_property
25 | end
26 |
27 | def update
28 | if @activity_custom_property.update(params[:activity_custom_property])
29 | redirect resource(:activity_custom_properties),
30 | :message => {:notice => "Custom property was successfully created"}
31 | else
32 | render :edit
33 | end
34 | end
35 |
36 | def destroy
37 | if @activity_custom_property.destroy
38 | redirect resource(:activity_custom_properties),
39 | :message => {:notice => "Custom property was successfully deleted"}
40 | else
41 | redirect resource(:activity_custom_properties),
42 | :message => {:error => "Unable to delete"}
43 | end
44 | end
45 |
46 | protected
47 |
48 | def load_activity_custom_property
49 | @activity_custom_property = ActivityCustomProperty.get(params[:id]) or raise NotFound
50 | end
51 |
52 | def number_of_columns
53 | ["edit", "update"].include?(params[:action]) ? 1 : super
54 | end
55 |
56 | end
57 |
--------------------------------------------------------------------------------
/merb/merb-auth/setup.rb:
--------------------------------------------------------------------------------
1 | # This file is specifically setup for use with the merb-auth plugin.
2 | # This file should be used to setup and configure your authentication stack.
3 | # It is not required and may safely be deleted.
4 | #
5 | # To change the parameter names for the password or login field you may set either of these two options
6 | #
7 | # Merb::Plugins.config[:"merb-auth"][:login_param] = :email
8 | # Merb::Plugins.config[:"merb-auth"][:password_param] = :my_password_field_name
9 |
10 | begin
11 | # Sets the default class ofr authentication. This is primarily used for
12 | # Plugins and the default strategies
13 | Merb::Authentication.user_class = User
14 |
15 | # Mixin the salted user mixin
16 | require 'merb-auth-more/mixins/salted_user'
17 | Merb::Authentication.user_class.class_eval{ include Merb::Authentication::Mixins::SaltedUser }
18 |
19 | # Setup the session serialization
20 | class Merb::Authentication
21 | def authenticated?
22 | if !!session[:user] && user
23 | user.active || auth_error('Your account is not active')
24 | else
25 | auth_error('You need to login to perform this action')
26 | end
27 | end
28 |
29 | def fetch_user(session_user_id)
30 | Merb::Authentication.user_class.get(session_user_id)
31 | end
32 |
33 | def store_user(user)
34 | user.nil? ? user : user.id
35 | end
36 |
37 | def auth_error(message)
38 | errors.add(nil, message) && false
39 | end
40 | end
41 | rescue
42 | Merb.logger.error <<-TEXT
43 |
44 | You need to setup some kind of user class with merb-auth.
45 | Merb::Authentication.user_class = User
46 |
47 | If you want to fully customize your authentication you should use merb-core directly.
48 |
49 | See merb/merb-auth/setup.rb and strategies.rb to customize your setup
50 |
51 | TEXT
52 | end
53 |
--------------------------------------------------------------------------------
/app/views/exceptions/not_found.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
Exception:
11 |
<%= request.exceptions.first.message %>
12 |
13 |
14 |
15 |
Welcome to Merb!
16 |
Merb is a light-weight MVC framework written in Ruby. We hope you enjoy it.
17 |
18 |
Where can I find help?
19 |
If you have any questions or if you can't figure something out, please take a
20 | look at our project page ,
21 | feel free to come chat at irc.freenode.net, channel #merb,
22 | or post to merb mailing list
23 | on Google Groups.
24 |
25 |
What if I've found a bug?
26 |
If you want to file a bug or make your own contribution to Merb,
27 | feel free to register and create a ticket at our
28 | project development page
29 | on Lighthouse.
30 |
31 |
How do I edit this page?
32 |
You're seeing this page because you need to edit the following files:
33 |
34 | config/router.rb (recommended)
35 | app/views/exceptions/not_found.html.erb (recommended)
36 | app/views/layout/application.html.erb (change this layout)
37 |
38 |
39 |
40 |
41 |
47 |
48 |
--------------------------------------------------------------------------------
/app/views/projects/edit.html.erb:
--------------------------------------------------------------------------------
1 | Project editing
2 |
3 | <%= error_messages_for(@project) %>
4 | <%= form_for @project, :action => resource(@project), :id => "project_form" do %>
5 |
6 |
7 | <%= text_field :name, :label => "Name" %>
8 |
9 |
10 | <%= text_area :description, :label => "Description" %>
11 |
12 |
13 | <%= select :client_id, :label => "Client", :collection => @clients.map { |c| [c.id, c.name] }, :prompt => "Select a client..." %>
14 |
15 |
16 | Active?
17 | <%= check_box :active %>
18 |
19 |
20 |
21 |
Assigned activity types
22 | <%= tag :input, :type => :hidden, :name => 'project[activity_type_ids][]' %>
23 |
24 |
25 | <% ActivityType.roots.each do |activity_type| %>
26 |
27 | <%= activity_type_check_box(activity_type, @project.activity_types.include?(activity_type), @project.used_activity_types.include?(activity_type)) %>
28 | <% unless activity_type.children.empty? %>
29 | >
30 | <% activity_type.children.each do |sub_activity_type| %>
31 |
32 | <%= activity_type_check_box(sub_activity_type, @project.activity_types.include?(sub_activity_type), @project.used_activity_types.include?(sub_activity_type)) %>
33 |
34 | <% end %>
35 |
36 | <% end %>
37 |
38 | <% end %>
39 |
40 |
41 |
42 |
43 | <%= submit 'Save', :class => "button" %> or <%= link_to 'Cancel', resource(:projects) %>
44 |
45 |
46 | <% end =%>
47 |
--------------------------------------------------------------------------------
/public/javascripts/users.js:
--------------------------------------------------------------------------------
1 | var Users = {
2 | init: function() {
3 | Users._initValidation();
4 | Users._initUserTypeCombo();
5 | if ($('.box').html() == "") {
6 | $('#secondary .box').hide();
7 | }
8 | },
9 |
10 | _initValidation: function() {
11 | // add login validation method
12 | function loginFormat(value, element, params) {
13 | return this.optional(element) || (/^[\w\.\-]{3,20}$/).test(value);
14 | }
15 |
16 | $.validator.addMethod('login', loginFormat,
17 | "Login should have between 3 and 20 characters including letters, digits, hyphen or underscore.");
18 |
19 | // add validation to form
20 | $('#user_form').validate({
21 | rules: {
22 | "user[name]": {
23 | required: true
24 | },
25 | "user[login]": {
26 | required: true,
27 | login: true
28 | },
29 | "user[email]": {
30 | required: true,
31 | email: true
32 | },
33 | "user[password]": {
34 | required: function(element) {
35 | var password_entered = element && element.value != "";
36 | var action = $("#user_form").attr("action");
37 | var editing = (/\d+$/).test(action);
38 | return password_entered || !editing;
39 | },
40 | minlength: 6
41 | },
42 | "user[password_confirmation]": {
43 | equalTo: "#user_password"
44 | }
45 | }
46 | });
47 | },
48 |
49 | _initUserTypeCombo: function() {
50 | $("#user_class_name").change(function() {
51 | var client = $("#user_client_id").parent();
52 | var role = $("#user_role_id").parent();
53 |
54 | if ($(this).val() == 'Employee') {
55 | client.hide();
56 | role.show();
57 | } else {
58 | role.hide();
59 | client.show();
60 | }
61 | });
62 | }
63 | };
64 |
65 | $(Users.init);
66 |
--------------------------------------------------------------------------------
/spec/controllers/roles_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Roles do
4 |
5 | it "shouldn't show any action for guest, employee and client's user" do
6 | [:index, :create, :edit, :update, :destroy].each do |action|
7 | block_should(raise_unauthenticated) { as(:guest).dispatch_to(Roles, action) }
8 | block_should(raise_forbidden) { as(:employee).dispatch_to(Roles, action) }
9 | block_should(raise_forbidden) { as(:client).dispatch_to(Roles, action) }
10 | end
11 | end
12 |
13 | describe "#index" do
14 | it "should show list of roles" do
15 | as(:admin).dispatch_to(Roles, :index).should be_successful
16 | end
17 | end
18 |
19 | describe "#create" do
20 | it "should create new role successfully and redirect to index" do
21 | block_should(change(Role, :count)) do
22 | controller = as(:admin).dispatch_to(Roles, :create, { :role => { :name => "Mastah" }})
23 | controller.should redirect_to(resource(:roles))
24 | end
25 | end
26 | end
27 |
28 | describe "#edit" do
29 | it "should show role edit form" do
30 | role = Role.generate
31 | as(:admin).dispatch_to(Roles, :edit, :id => role.id).should be_successful
32 | end
33 | end
34 |
35 | describe "#update" do
36 | it "should update the role" do
37 | role = Role.generate :name => 'OriginalName'
38 | response = as(:admin).dispatch_to(Roles, :update, :id => role.id, :role => {
39 | :can_manage_financial_data => true
40 | })
41 |
42 | response.should redirect_to(resource(:roles))
43 | Role.get(role.id).can_manage_financial_data.should be_true
44 | end
45 | end
46 |
47 | describe "#destroy" do
48 | it "should delete role" do
49 | role = Role.generate
50 | admin = Employee.generate :admin
51 | block_should(change(Role, :count).by(-1)) do
52 | as(admin).dispatch_to(Roles, :destroy, :id => role.id)
53 | end
54 | end
55 | end
56 |
57 | end
58 |
--------------------------------------------------------------------------------
/app/views/projects/show.html.erb:
--------------------------------------------------------------------------------
1 | <%= @project.name %>
2 |
3 |
4 | Description: <%= @project.description %>
5 |
6 |
7 | Active: <%= @project.active %>
8 |
9 |
10 | Client: <%= link_to @project.client.name, url(:client, @project.client) %>
11 |
12 |
13 | Created at: <%= @project.created_at.to_date.formatted(current_user.date_format) %>
14 |
15 |
16 | <%= link_to "Edit", url(:edit_project, @project) %>
17 |
18 |
19 | <% unless @activities_without_types.blank? %>
20 |
21 |
Warning:
22 |
23 |
24 | <% @one = (@activities_without_types.length == 1) %>
25 | <%= @one ? "There's one activity" : "There are #{@activities_without_types.length} activities" %>
26 | in this project that <%= @one ? "doesn't have a type" : "don't have any types" %> assigned, which
27 | <%= @one ? 'is' : 'are' %> currently invalid. You should assign a default activity type
28 | to <%= @one ? 'it' : 'them' %>.
29 |
30 |
31 | <%= form_for @project, :action => resource(@project, :set_default_activity_type) do %>
32 |
33 |
34 | Default activity type:
35 | <%= activity_type_select(@project) %>
36 |
37 |
38 |
39 | <%= submit 'Update activities', :class => "button" %>
40 |
41 |
42 | <% end =%>
43 |
44 | <% end %>
45 |
46 |
47 |
Hourly rates
48 |
>
49 | <%= partial 'projects/hourly_rates', :project => @project %>
50 |
51 |
52 |
53 |
58 |
--------------------------------------------------------------------------------
/app/views/activities/_filter_form.html.erb:
--------------------------------------------------------------------------------
1 | <%= form_for @search_criteria, :action => "/activities", :method => :get do %>
2 |
3 |
4 | <% if current_user.can_see_clients? %>
5 | <%= partial(:criteria_group, :with => :client, :as => :group) %>
6 | <% end %>
7 |
8 | <%= partial(:criteria_group, :with => :project, :as => :group) %>
9 |
10 | <%= partial(:criteria_group, :with => :activity_type, :as => :group) %>
11 |
12 | >
13 | <%= label "Include activities without types", :for => :include_activities_without_types %>
14 | <%= check_box :include_activities_without_types, :class => 'include_activities_without_types' %>
15 |
16 |
17 | <% if current_user.can_see_users? %>
18 | <%= partial(:criteria_group, :with => :role, :as => :group) %>
19 | <%= partial(:criteria_group, :with => :user, :as => :group) %>
20 | <% end %>
21 |
22 |
23 | <% date_from = @search_criteria.date_from ? @search_criteria.date_from.formatted(current_user.date_format) : "" %>
24 | <%= text_field :date_from, :value => date_from, :label => "From", :class => 'datepicker', :size => 9 %>
25 |
26 |
27 |
28 | <% date_to = @search_criteria.date_to ? @search_criteria.date_to.formatted(current_user.date_format) : "" %>
29 | <%= text_field :date_to, :value => date_to, :label => "To", :class => 'datepicker', :size => 9 %>
30 |
31 |
32 |
33 | Include
34 |
35 | <%= radio_group :invoiced, [{ :value => "all", :label => "All" },
36 | { :value => "invoiced", :label => "Invoiced" },
37 | { :value => "not_invoiced", :label => "Not invoiced" }] %>
38 |
39 |
40 |
41 |
42 | <%= submit "Submit", :class => "button" %>
43 |
44 |
45 |
46 | <% end =%>
47 |
--------------------------------------------------------------------------------
/app/views/users/_user_form.html.erb:
--------------------------------------------------------------------------------
1 | <%= @user.new? ? "Add new user" : "User editing" %>
2 | <%= error_messages_for @user %>
3 | <%= form_for(@user, :action => @user.new? ? url(:users) : url(:user, @user), :id => "user_form") do %>
4 |
5 | <%= fields_for :user do %>
6 |
7 | <%= select :class_name, :label => "Type", :collection => [["Employee", "Employee"], ["ClientUser", "Client's user"]] %>
8 |
9 |
10 | <%= select :role_id, :label => "Role", :collection => @roles.map { |r| [r.id, r.name] }, :prompt => "Select role..." %>
11 |
12 |
13 | <%= select :client_id, :label => "Client", :collection => @clients.map { |c| [c.id, c.name] }, :prompt => "Select a client..." %>
14 |
15 |
16 | <%= text_field :name, :label => "Name" %>
17 |
18 |
19 | <%= text_field :login, :label => "Login" %>
20 |
21 |
22 | <%= text_field :ldap_login, :label => "LDAP Login" %>
23 |
24 |
25 | <%= text_field :email, :label => "Email" %>
26 |
27 | <% if !@user.new? && @user != current_user%>
28 |
29 | Admin?
30 | <%= check_box :admin %>
31 |
32 |
33 | Active?
34 | <%= check_box :active %>
35 |
36 | <% end %>
37 |
38 | <%= password_field :password, :label => "Password" %>
39 |
40 |
41 | <%= password_field :password_confirmation, :label => "Password confirmation" %>
42 |
43 |
44 | <%= submit @user.new? ? "Create" : "Update", :class => "button" %>
45 |
46 | <% end =%>
47 |
48 | <% end =%>
49 |
--------------------------------------------------------------------------------
/app/controllers/hourly_rates.rb:
--------------------------------------------------------------------------------
1 | class HourlyRates < Application
2 |
3 | before :ensure_user_that_can_manage_financial_data
4 |
5 | only_provides :json
6 |
7 | def index
8 | @project = Project.get(params[:project_id])
9 | grouped_rates = @project.hourly_rates_grouped_by_roles
10 | @hourly_rates = grouped_rates.keys.sort_by { |role| role.name }.map do |role|
11 | {
12 | :project_id => @project.id,
13 | :role_id => role.id,
14 | :role_name => role.name,
15 | :hourly_rates => grouped_rates[role].each { |hr| hr.date_format_for_json = current_user.date_format }
16 | }
17 | end
18 | display @hourly_rates
19 | end
20 |
21 | def create
22 | @hourly_rate = HourlyRate.new(params[:hourly_rate])
23 | @hourly_rate.operation_author = current_user
24 | if @hourly_rate.save
25 | @hourly_rate.date_format_for_json = current_user.date_format
26 | display :status => :ok, :hourly_rate => @hourly_rate
27 | else
28 | display :status => :invalid, :hourly_rate => { :error_messages => @hourly_rate.error_messages }
29 | end
30 | end
31 |
32 | def update
33 | @hourly_rate = HourlyRate.get(params[:id])
34 | raise NotFound unless @hourly_rate
35 | @hourly_rate.operation_author = current_user
36 | if @hourly_rate.update(params[:hourly_rate])
37 | @hourly_rate.date_format_for_json = current_user.date_format
38 | display :status => :ok, :hourly_rate => @hourly_rate
39 | else
40 | display :status => :invalid, :hourly_rate => { :error_messages => @hourly_rate.error_messages }
41 | end
42 | end
43 |
44 | def destroy
45 | @hourly_rate = HourlyRate.get(params[:id])
46 | raise NotFound unless @hourly_rate
47 | @hourly_rate.operation_author = current_user
48 | if @hourly_rate.destroy
49 | display :status => :ok
50 | else
51 | display :status => :error, :hourly_rate => { :error_messages => @hourly_rate.error_messages }
52 | end
53 | end
54 |
55 | end # HourlyRates
56 |
--------------------------------------------------------------------------------
/app/views/projects/_hourly_rates.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
[role name]
3 |
4 |
5 | Takes effect at
6 | Value
7 | Currency
8 |
9 |
10 |
11 |
12 |
13 | <%= form_for HourlyRate.new do %>
14 | <%= text_field :takes_effect_at %>
15 | <% end =%>
16 |
17 |
18 | <%= form_for HourlyRate.new do %>
19 | <%= text_field :value %>
20 | <% end =%>
21 |
22 |
23 | <%= form_for HourlyRate.new do %>
24 | <%= select :currency_id, :collection => currency_options_for_hourly_rate, :prompt => "Select..." %>
25 | <% end =%>
26 |
27 |
28 | <%= form_for HourlyRate.new do %>
29 | <%= button "OK", :class => 'submit' %>
30 | <%= button "Cancel", :class => 'cancel' %>
31 |
32 | <%= hidden_field :project_id %>
33 | <%= hidden_field :role_id %>
34 | <% end =%>
35 |
36 |
37 |
38 |
39 |
40 | [takes_effect_at]
41 | [value]
42 | [currency]
43 |
44 | <%= link_to 'Edit', '#', :class => 'edit' %>
45 | <%= link_to 'Delete', '#', :class => 'delete' %>
46 |
47 |
48 |
49 |
50 | <%= link_to 'New', '', :class => 'new_hourly_rate' %>
51 |
52 |
--------------------------------------------------------------------------------
/public/javascripts/lib/jquery-scrollto.js:
--------------------------------------------------------------------------------
1 | /**
2 | * jQuery.ScrollTo - Easy element scrolling using jQuery.
3 | * Copyright (c) 2007-2008 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com
4 | * Dual licensed under MIT and GPL.
5 | * Date: 9/11/2008
6 | * @author Ariel Flesler
7 | * @version 1.4
8 | *
9 | * http://flesler.blogspot.com/2007/10/jqueryscrollto.html
10 | */
11 | ;(function(h){var m=h.scrollTo=function(b,c,g){h(window).scrollTo(b,c,g)};m.defaults={axis:'y',duration:1};m.window=function(b){return h(window).scrollable()};h.fn.scrollable=function(){return this.map(function(){var b=this.parentWindow||this.defaultView,c=this.nodeName=='#document'?b.frameElement||b:this,g=c.contentDocument||(c.contentWindow||c).document,i=c.setInterval;return c.nodeName=='IFRAME'||i&&h.browser.safari?g.body:i?g.documentElement:this})};h.fn.scrollTo=function(r,j,a){if(typeof j=='object'){a=j;j=0}if(typeof a=='function')a={onAfter:a};a=h.extend({},m.defaults,a);j=j||a.speed||a.duration;a.queue=a.queue&&a.axis.length>1;if(a.queue)j/=2;a.offset=n(a.offset);a.over=n(a.over);return this.scrollable().each(function(){var k=this,o=h(k),d=r,l,e={},p=o.is('html,body');switch(typeof d){case'number':case'string':if(/^([+-]=)?\d+(px)?$/.test(d)){d=n(d);break}d=h(d,this);case'object':if(d.is||d.style)l=(d=h(d)).offset()}h.each(a.axis.split(''),function(b,c){var g=c=='x'?'Left':'Top',i=g.toLowerCase(),f='scroll'+g,s=k[f],t=c=='x'?'Width':'Height',v=t.toLowerCase();if(l){e[f]=l[i]+(p?0:s-o.offset()[i]);if(a.margin){e[f]-=parseInt(d.css('margin'+g))||0;e[f]-=parseInt(d.css('border'+g+'Width'))||0}e[f]+=a.offset[i]||0;if(a.over[i])e[f]+=d[v]()*a.over[i]}else e[f]=d[i];if(/^\d+$/.test(e[f]))e[f]=e[f]<=0?0:Math.min(e[f],u(t));if(!b&&a.queue){if(s!=e[f])q(a.onAfterFirst);delete e[f]}});q(a.onAfter);function q(b){o.animate(e,j,a.easing,b&&function(){b.call(this,r,a)})};function u(b){var c='scroll'+b,g=k.ownerDocument;return p?Math.max(g.documentElement[c],g.body[c]):k[c]}}).end()};function n(b){return typeof b=='object'?b:{top:b,left:b}}})(jQuery);
--------------------------------------------------------------------------------
/app/views/users/settings.html.erb:
--------------------------------------------------------------------------------
1 | Account settings
2 |
3 | <%= error_messages_for @user %>
4 |
5 | <%= form_for(@user, :action => url(:user, @user), :id => "user_form") do %>
6 |
7 | <%= fields_for :user do %>
8 |
9 | Login <%= @user.login %>
10 |
11 |
12 | <%= text_field :name, :label => "Name" %>
13 |
14 |
15 | <%= text_field :ldap_login, :label => "LDAP Login" %>
16 |
17 |
18 | <%= text_field :email, :label => "Email" %>
19 |
20 |
21 | <%= password_field :password, :label => "Password" %>
22 |
23 |
24 | <%= password_field :password_confirmation, :label => "Password confirmation" %>
25 |
26 |
27 | Show activities for last
28 | <%= radio_options(@user, :recent_days_on_list, Rubytime::RECENT_DAYS_ON_LIST) { |v| recent_days_on_list_desc(v) }%>
29 |
30 |
31 | Date format
32 | <%= radio_options(@user, :date_format, Rubytime::DATE_FORMAT_NAMES) { |v| date_format_desc(v) } %>
33 |
34 |
35 | Decimal separator
36 | <%= radio_options(@user, :decimal_separator, Rubytime::DECIMAL_SEPARATORS) { |v| v } %>
37 |
38 | <% if @user.is_employee? %>
39 |
40 | Email reminder
41 | <%= hidden_field :id => "user_remind_by_email_hidden", :name => 'user[remind_by_email]', :value => '0' %>
42 | <%= check_box :id => "user_remind_by_email", :name => "user[remind_by_email]", :value => '1', :checked => @user.remind_by_email %>
43 |
44 | <% end %>
45 |
46 | <%= submit "Update", :class => "button" %> or <%= link_to 'Cancel', url(:activities) %>
47 |
48 | <% end =%>
49 |
50 | <% end =%>
51 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'rake/rdoctask'
3 |
4 | require 'merb-core'
5 | require 'merb-core/tasks/merb'
6 |
7 | include FileUtils
8 |
9 | # Load the basic runtime dependencies; this will include
10 | # any plugins and therefore plugin rake tasks.
11 | init_env = ENV['MERB_ENV'] || 'rake'
12 | Merb.load_dependencies(:environment => init_env)
13 |
14 | # Get Merb plugins and dependencies
15 | Merb::Plugins.rakefiles.each { |r| require r }
16 |
17 | # Load any app level custom rakefile extensions from lib/tasks
18 | tasks_path = File.join(File.dirname(__FILE__), "lib", "tasks")
19 | rake_files = Dir["#{tasks_path}/*.rake"]
20 | rake_files.each{|rake_file| load rake_file }
21 |
22 | desc "Start runner environment"
23 | task :merb_env do
24 | Merb.start_environment(:environment => init_env, :adapter => 'runner')
25 | end
26 |
27 | require 'spec/rake/spectask'
28 | require 'merb-core/test/tasks/spectasks'
29 |
30 | begin
31 | require 'jslint/tasks'
32 | JSLint.config_path = (Merb.root / "config" / "jslint.yml").to_s
33 | rescue LoadError
34 | # ignore, probably it's on production
35 | end
36 |
37 | desc 'Default: run spec examples'
38 | task :default => 'spec'
39 |
40 | # Hack for top-level name clash between vlad and datamapper.
41 | if Rake.application.options.show_tasks or Rake.application.top_level_tasks.any? {|t| t == 'deploy' or t =~ /^vlad:/}
42 | begin
43 | $TESTING = true # Required to bypass check for reserved_name? in vlad. DataMapper 0.9.x defines Kernel#repository...
44 | require 'vlad'
45 | Vlad.load :scm => "git", :app => "passenger", :web => nil
46 | rescue Exception => e
47 | p e
48 | puts e.backtrace.join("\n")
49 | end
50 | end
51 |
52 | ##############################################################################
53 | # ADD YOUR CUSTOM TASKS IN /lib/tasks
54 | # NAME YOUR RAKE FILES file_name.rake
55 | ##############################################################################
56 |
57 | begin
58 | require 'rubygems'
59 | gem 'ci_reporter'
60 | require 'ci/reporter/rake/rspec'
61 | rescue LoadError; end
62 |
--------------------------------------------------------------------------------
/spec/models/activity_custom_property_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe ActivityCustomProperty do
4 |
5 | describe "#destroy_allowed?" do
6 | before { @activity_custom_property = ActivityCustomProperty.generate }
7 |
8 | context "when doesn't have any activity_custom_property_values assigned" do
9 | before { @activity_custom_property.activity_custom_property_values.count.should == 0 }
10 |
11 | it { @activity_custom_property.destroy_allowed?.should be_true }
12 | end
13 |
14 | context "when have activity_custom_property_values assigned" do
15 | before do
16 | @value = ActivityCustomPropertyValue.generate :activity_custom_property => @activity_custom_property
17 | @activity_custom_property.activity_custom_property_values << @value
18 | @activity_custom_property.save
19 | end
20 |
21 | it { @activity_custom_property.destroy_allowed?.should be_false }
22 | end
23 | end
24 |
25 | describe "#destroy" do
26 | before { @activity_custom_property = ActivityCustomProperty.generate }
27 |
28 | context "when destroy allowed" do
29 | before { @activity_custom_property.stub!(:destroy_allowed? => true) }
30 |
31 | it "should return true" do
32 | @activity_custom_property.destroy.should be_true
33 | end
34 |
35 | it "should destroy the record" do
36 | @activity_custom_property.destroy
37 | ActivityCustomProperty.get(@activity_custom_property.id).should be_nil
38 | end
39 | end
40 |
41 | context "when destroy not allowed" do
42 | before { @activity_custom_property.stub!(:destroy_allowed? => false) }
43 |
44 | it "should return false" do
45 | @activity_custom_property.destroy.should be_false
46 | end
47 |
48 | it "should not destroy the record" do
49 | @activity_custom_property.destroy
50 | ActivityCustomProperty.get(@activity_custom_property.id).should == @activity_custom_property
51 | end
52 | end
53 | end
54 |
55 | end
56 |
--------------------------------------------------------------------------------
/spec/models/setting_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Setting do
4 |
5 | before { Setting.all.destroy! }
6 |
7 | describe '.enable_notifications method' do
8 |
9 | context 'when a Setting record exists' do
10 | before { @setting = Setting.create! }
11 | it 'should return the value of the setting' do
12 | Setting.first.update :enable_notifications => true
13 | Setting.enable_notifications.should == true
14 | end
15 | end
16 |
17 | context 'when no Setting record exists' do
18 | it 'should create a Setting record' do
19 | expect { Setting.enable_notifications }.to change { Setting.count }.by(1)
20 | end
21 |
22 | it 'should return the value of the setting' do
23 | Setting.enable_notifications.should == Setting.properties[:enable_notifications].default
24 | end
25 | end
26 | end
27 |
28 | describe '.free_days_access_key method' do
29 |
30 | context 'when a Setting record exists' do
31 | before { @setting = Setting.create! }
32 |
33 | it 'should return the value of the setting' do
34 | Setting.first.update :free_days_access_key => 'thekey'
35 | Setting.free_days_access_key.should == 'thekey'
36 | end
37 | end
38 |
39 | context 'when no Setting record exists' do
40 | before { Setting.stub!(:generate_free_days_access_key => 'generated_key') }
41 |
42 | it 'should create a Setting record' do
43 | expect { Setting.free_days_access_key }.to change { Setting.count }.by(1)
44 | end
45 |
46 | it 'should return the value of the setting' do
47 | Setting.free_days_access_key.should == 'generated_key'
48 | end
49 | end
50 | end
51 |
52 | describe '#generate_free_days_access_key method' do
53 | before do
54 | Setting.stub! :generate_free_days_access_key => 'generated_key'
55 | @setting = Setting.create! :free_days_access_key => 'one'
56 | end
57 |
58 | it 'should generate new value of :free_days_access_key' do
59 | @setting.generate_free_days_access_key
60 | @setting.free_days_access_key.should == 'generated_key'
61 | end
62 | end
63 |
64 | end
65 |
--------------------------------------------------------------------------------
/public/stylesheets/hourly_rates.css:
--------------------------------------------------------------------------------
1 | .hourly_rate_list {
2 | background: #f5f5f5;
3 | padding: 5px;
4 | float: left;
5 | clear: left;
6 | width: 100%;
7 | }
8 | .hourly_rate_list h3 {
9 | margin: 10px 0;
10 | }
11 | .hourly_rate_list table {
12 | border-spacing: 2px;
13 | margin-left: -2px;
14 | width: 100%;
15 | }
16 | .hourly_rate_list table tr th {
17 | padding: 4px;
18 | font-size: 0.8em;
19 | background: #e0e0e0;
20 | }
21 | .hourly_rate_list table tr th.takes_effect_at,
22 | .hourly_rate_list table tr th.value,
23 | .hourly_rate_list table tr th.currency {
24 | width: 20%;
25 | }
26 | .hourly_rate_list table tr th.manage {
27 | width: 40%;
28 | }
29 | .hourly_rate_list table tr td {
30 | background: #eee;
31 | }
32 | .hourly_rate_list table tr.hourly_rate:hover td,
33 | .hourly_rate_list table tr.hourly_rate.form td {
34 | background: #d0d0d0;
35 | }
36 | .hourly_rate_list table tr.hourly_rate td.links a {
37 | display: none;
38 | }
39 | .hourly_rate_list table tr.hourly_rate:hover td.links a {
40 | display: inline;
41 | }
42 | .hourly_rate_list table tr.hourly_rate.view td {
43 | padding: 6px 4px 5px 4px;
44 | font-size: 1em;
45 | }
46 | .hourly_rate_list table tr.hourly_rate.view.justUpdated td {
47 | background: yellow;
48 | }
49 | .hourly_rate_list table tr.hourly_rate.form td {
50 | padding: 0;
51 | }
52 | .hourly_rate_list table tr.hourly_rate.form td input,
53 | .hourly_rate_list table tr.hourly_rate.form td select {
54 | font-size: inherit;
55 | border: solid 1px #ccc;
56 | margin: 2px 1px;
57 | }
58 | .hourly_rate_list table tr.hourly_rate.form td input {
59 | padding: 1px;
60 | }
61 | .hourly_rate_list table tr.hourly_rate.form td.takes_effect_at input {
62 | width: 80px;
63 | }
64 | .hourly_rate_list table tr.hourly_rate td.value {
65 | text-align: right;
66 | }
67 | .hourly_rate_list table tr.hourly_rate.form td.value input {
68 | width: 80px;
69 | text-align: inherit;
70 | }
71 | .hourly_rate_list table tr.hourly_rate.view td.currency {
72 | padding-left: 5px;
73 | }
74 | .hourly_rate_list table tr.hourly_rate.form td.currency select {
75 | width: 80px;
76 | }
77 | .hourly_rate_list table tr.hourly_rate .error_messages {
78 | padding: 0.5em;
79 | margin: 0;
80 | background: #f88;
81 | border: solid 1px #f00;
82 | }
83 |
--------------------------------------------------------------------------------
/spec/models/free_day_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe FreeDay do
4 |
5 | it "should be created" do
6 | block_should(change(FreeDay, :count).by(1)) do
7 | FreeDay.prepare.save.should be_true
8 | end
9 | end
10 |
11 | it "should be created correctly" do
12 | user = Employee.generate
13 | FreeDay.prepare(:user => user, :date => date("2009-11-30")).save.should be_true
14 | user.has_free_day_on(date("2009-11-30")).should be_true
15 | end
16 |
17 | describe ".ranges" do
18 | it "should create ranges of free days" do
19 | user1 = Employee.generate
20 | user2 = Employee.generate
21 |
22 | FreeDay.all.destroy!
23 | ['2009-09-05', '2009-09-06', '2009-09-08', '2009-09-11', '2009-09-12', '2009-09-13'].each do |d|
24 | FreeDay.generate :user => user1, :date => date(d)
25 | end
26 | ['2009-09-05', '2009-09-08', '2009-09-11', '2009-09-12'].each do |d|
27 | FreeDay.generate :user => user2, :date => date(d)
28 | end
29 |
30 | ranges = FreeDay.ranges
31 | ranges.size.should == 6
32 |
33 | ranges.should include({ :user => user1, :start_date => date('2009-09-05'), :end_date => date('2009-09-06') })
34 | ranges.should include({ :user => user1, :start_date => date('2009-09-08'), :end_date => date('2009-09-08') })
35 | ranges.should include({ :user => user1, :start_date => date('2009-09-11'), :end_date => date('2009-09-13') })
36 | ranges.should include({ :user => user2, :start_date => date('2009-09-05'), :end_date => date('2009-09-05') })
37 | ranges.should include({ :user => user2, :start_date => date('2009-09-08'), :end_date => date('2009-09-08') })
38 | ranges.should include({ :user => user2, :start_date => date('2009-09-11'), :end_date => date('2009-09-12') })
39 | end
40 | end
41 |
42 | describe ".to_ical" do
43 | it "should render .ics file with free days iCalendar" do
44 | user = Employee.generate
45 | FreeDay.stub!(:ranges => [
46 | { :start_date => date('2009-09-05'), :end_date => date('2009-09-06'), :user => user },
47 | ])
48 |
49 | ical = FreeDay.to_ical
50 | ical.should =~ /DTSTART:20090905/
51 | ical.should =~ /DTEND:20090907/
52 | ical.should =~ /SUMMARY:#{user.name}/
53 | end
54 | end
55 |
56 | end
57 |
--------------------------------------------------------------------------------
/spec/models/client_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Client do
4 |
5 | it "should be deleted with associated users and projects if has no invoices and no activities" do
6 | client = Client.generate
7 | 3.times { ClientUser.generate(:client => client) }
8 | 2.times { Project.generate(:client => client) }
9 |
10 | block_should(change(Project, :count).by(-2)) do
11 | block_should(change(ClientUser, :count).by(-3)) do
12 | client.destroy.should be_true
13 | end
14 | end
15 | end
16 |
17 | it "should find active clients" do
18 | active = Client.generate :active => true
19 | inactive = Client.generate :active => false
20 | active_clients = Client.active.all
21 | active_clients.should include(active)
22 | active_clients.should_not include(inactive)
23 | end
24 |
25 | it "shouldn't be deleted if has no invoices but has activities" do
26 | client = Client.generate
27 | project = Project.generate :client => client
28 | Activity.generate :project => project
29 |
30 | block_should_not(change(ClientUser, :count)) do
31 | block_should_not(change(Activity, :count)) do
32 | block_should_not(change(Project, :count)) do
33 | block_should_not(change(Client, :count)) do
34 | client.destroy.should be_nil
35 | end
36 | end
37 | end
38 | end
39 | end
40 |
41 | it "shouldn't be deleted if has invoices" do
42 | client = Client.generate
43 | Invoice.generate :client => client
44 |
45 | block_should_not(change(ClientUser, :count)) do
46 | block_should_not(change(Activity, :count)) do
47 | block_should_not(change(Project, :count)) do
48 | block_should_not(change(Client, :count)) do
49 | client.destroy.should be_nil
50 | end
51 | end
52 | end
53 | end
54 | end
55 |
56 | it "should have default order by :name" do
57 | prefix = 'test for order of '
58 | client1 = Client.generate :name => prefix + 'D'
59 | client2 = Client.generate :name => prefix + 'B'
60 | client3 = Client.generate :name => prefix + 'A'
61 | client4 = Client.generate :name => prefix + 'C'
62 |
63 | Client.all(:conditions => ["name LIKE ?", prefix + "%"]).should == [client3, client2, client4, client1]
64 | end
65 | end
66 |
--------------------------------------------------------------------------------
/lib/tasks/cron.rake:
--------------------------------------------------------------------------------
1 | desc "send emails for RubyTime users"
2 | namespace :rubytime do
3 | # personal notification sent to all employees that missed a day this month,
4 | # with a list of days without activities
5 | # TODO: rename this?...
6 | task :send_emails => :merb_env do
7 | for user in User.all(:remind_by_email => true)
8 | missed_days = user.days_without_activities
9 | if missed_days.count > 0
10 | puts "Emailing #{user.name}"
11 | m = UserMailer.new(:user => user, :missed_days => missed_days)
12 | m.dispatch_and_deliver(:notice,
13 | :from => Rubytime::CONFIG[:mail_from],
14 | :to => user.email,
15 | :subject => "RubyTime reminder!"
16 | )
17 | end
18 | end
19 | end
20 |
21 | # personal notification sent to all employees that didn't add an activity yesterday
22 | desc 'Send timesheet nagger emails for previous weekday'
23 | task :send_timesheet_nagger_emails_for_previous_weekday => :merb_env do
24 | if Date.today.weekday?
25 | logger = daily_logger('timesheet_nagger')
26 | date = Date.today.previous_weekday
27 | Employee.send_timesheet_naggers_for__if_enabled(date, logger)
28 | end
29 | end
30 |
31 | # list of employees that didn't add an activity yesterday, sent to the manager
32 | desc 'Send timesheet report email for previous weekday'
33 | task :send_timesheet_report_email_for_previous_weekday => :merb_env do
34 | if Date.today.weekday?
35 | logger = Logger.new(Merb.root / "log/timesheet_reporter.log")
36 | date = Date.today.previous_weekday
37 | Employee.send_timesheet_reporter_for__if_enabled(date, logger)
38 | end
39 | end
40 |
41 | # summary for activities from last five days, sent to the employee
42 | desc 'Send timesheet summary emails for last five days'
43 | task :send_timesheet_summary_emails_for_last_five_days => :merb_env do
44 | logger = daily_logger('timesheet_summary')
45 | date_range = (Date.today - 4)..(Date.today)
46 | Employee.send_timesheet_summary_for__if_enabled(date_range, logger)
47 | end
48 |
49 | end
50 |
51 | def daily_logger(relative_dir, date = Date.today)
52 | log_dir = Merb.root / "log" / relative_dir
53 | Dir.mkdir(log_dir) unless File.directory?(log_dir)
54 | Logger.new(log_dir / "#{date}.log")
55 | end
56 |
--------------------------------------------------------------------------------
/app/models/project.rb:
--------------------------------------------------------------------------------
1 | class Project
2 | include DataMapper::Resource
3 |
4 | property :id, Serial
5 | property :name, String, :required => true, :unique => true, :index => true
6 | property :description, Text
7 | property :client_id, Integer, :required => true, :index => true
8 | property :active, Boolean, :required => true, :default => true
9 | property :created_at, DateTime
10 |
11 | attr_accessor :has_activities
12 | belongs_to :client
13 | has n, :project_activity_types
14 | has n, :activity_types, :through => :project_activity_types
15 | has n, :activities
16 | has n, :users, :through => :activities
17 | has n, :hourly_rates
18 |
19 | before :destroy do
20 | throw :halt if activities.count > 0
21 |
22 | hourly_rates.all.destroy!
23 | end
24 |
25 | # class methods
26 |
27 | def self.active
28 | all(:active => true)
29 | end
30 |
31 | def self.visible_for(user)
32 | if user.is_admin?
33 | all
34 | elsif user.is_employee?
35 | active
36 | else # client
37 | user.client.projects
38 | end
39 | end
40 |
41 | def self.with_activities_for(user)
42 | all('activities.user_id' => user.id, :unique => true)
43 | end
44 |
45 | # instance methods
46 |
47 | def available_activity_types
48 | self.activity_types(:parent => nil).map do |at|
49 | subactivity_types_array = self.activity_types(:parent => at).map(&:json_hash)
50 | at.json_hash.merge({ :available_subactivity_types => subactivity_types_array })
51 | end
52 | end
53 |
54 | def calendar_viewable?(user)
55 | user.client == self.client || user.is_admin?
56 | end
57 |
58 | def hourly_rates_grouped_by_roles
59 | Role.all.inject({}) { |hash, role| hash[role] = []; hash }.update(hourly_rates.group_by { |hr| hr.role })
60 | end
61 |
62 | def activity_type_ids
63 | activity_types.map { |at| at.id }
64 | end
65 |
66 | def activity_type_ids=(activity_type_ids)
67 | project_activity_types.all.destroy!
68 | (activity_type_ids + used_activity_type_ids).uniq.each do |activity_type_id|
69 | project_activity_types.create(:activity_type_id => activity_type_id)
70 | end
71 | end
72 |
73 | def used_activity_types
74 | ActivityType.all("activities.project_id" => id).map { |at| at.ancestors + [at] }.flatten
75 | end
76 |
77 | def used_activity_type_ids
78 | used_activity_types.map { |at| at.id }
79 | end
80 |
81 | end
82 |
--------------------------------------------------------------------------------
/app/views/exceptions/not_acceptable.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
Exception:
11 |
<%= request.exceptions.first.message %>
12 |
13 |
14 |
15 |
Why am I seeing this page?
16 |
Merb couldn't find an appropriate content_type to return,
17 | based on what you said was available via provides() and
18 | what the client requested.
19 |
20 |
How to add a mime-type
21 |
22 | Merb.add_mime_type :pdf, :to_pdf, %w[application/pdf], "Content-Encoding" => "gzip"
23 |
24 |
What this means is:
25 |
26 | Add a mime-type for :pdf
27 | Register the method for converting objects to PDF as #to_pdf.
28 | Register the incoming mime-type "Accept" header as application/pdf.
29 | Specify a new header for PDF types so it will set Content-Encoding to gzip.
30 |
31 |
32 |
You can then do:
33 |
34 | class Foo < Application
35 | provides :pdf
36 | end
37 |
38 |
39 |
Where can I find help?
40 |
If you have any questions or if you can't figure something out, please take a
41 | look at our project page ,
42 | feel free to come chat at irc.freenode.net, channel #merb,
43 | or post to merb mailing list
44 | on Google Groups.
45 |
46 |
What if I've found a bug?
47 |
If you want to file a bug or make your own contribution to Merb,
48 | feel free to register and create a ticket at our
49 | project development page
50 | on Lighthouse.
51 |
52 |
How do I edit this page?
53 |
You can change what people see when this happens by editing app/views/exceptions/not_acceptable.html.erb .
54 |
55 |
56 |
57 |
63 |
64 |
--------------------------------------------------------------------------------
/spec/models/purse_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Purse do
4 |
5 | before :each do
6 | @purse = Purse.new
7 | end
8 |
9 | before :all do
10 | @euro = Currency.first_or_generate :singular_name => 'euro'
11 | @dollar = Currency.first_or_generate :singular_name => 'dollar'
12 | end
13 |
14 | context "after initialize" do
15 | it "should be empty" do
16 | @purse.currencies.should == []
17 | end
18 | end
19 |
20 | it "should allow to put money inside and show content" do
21 | @purse.currencies.should == []
22 | @purse[@euro].should == Money.new(0.00, @euro)
23 | @purse[@dollar].should == Money.new(0.00, @dollar)
24 |
25 | @purse << Money.new(11.25, @euro)
26 |
27 | @purse.currencies.should == [@euro]
28 | @purse[@euro].should == Money.new(11.25, @euro)
29 | @purse[@dollar].should == Money.new(0.00, @dollar)
30 |
31 | @purse << Money.new(2.50, @euro)
32 | @purse << Money.new(9.98, @dollar)
33 |
34 | @purse.currencies.should == [@dollar, @euro]
35 | @purse[@euro].should == Money.new(13.75, @euro)
36 | @purse[@dollar].should == Money.new(9.98, @dollar)
37 | end
38 |
39 | it "should merge two purses together" do
40 | zl = Currency.first_or_generate(:singular_name => 'zloty')
41 | pound = Currency.first_or_generate(:singular_name => 'pound')
42 |
43 | @purse << Money.new(15, @euro)
44 | @purse << Money.new(3, zl)
45 | @purse << Money.new(14, pound)
46 |
47 | other = Purse.new
48 | other << Money.new(5, @euro)
49 | other << Money.new(10, @dollar)
50 | other << Money.new(20, zl)
51 |
52 | @purse.merge(other)
53 | @purse.currencies.should == [@dollar, @euro, pound, zl]
54 | @purse[@dollar].should == Money.new(10, @dollar)
55 | @purse[@euro].should == Money.new(20, @euro)
56 | @purse[pound].should == Money.new(14, pound)
57 | @purse[zl].should == Money.new(23, zl)
58 |
59 | # other should remain unchanged
60 | other.currencies.should == [@dollar, @euro, zl]
61 | other[@euro].should == Money.new(5, @euro)
62 | other[@dollar].should == Money.new(10, @dollar)
63 | other[zl].should == Money.new(20, zl)
64 | end
65 |
66 | describe "#to_s" do
67 | it "should return content as string" do
68 | @purse << Money.new(2.50, @euro)
69 | @purse << Money.new(9.98, @dollar)
70 |
71 | @purse.to_s.should == Money.new(9.98, @dollar).to_s + ' and ' + Money.new(2.50, @euro).to_s
72 | end
73 | end
74 |
75 | end
76 |
--------------------------------------------------------------------------------
/lib/rubytime/misc.rb:
--------------------------------------------------------------------------------
1 | module Rubytime
2 | module Misc
3 | def self.generate_password(size = 3)
4 | c = %w(b c d f g h j k l m n p qu r s t v w x z ch cr fr nd ng nk nt ph pr rd sh sl sp st th tr lt)
5 | v = %w(a e i o u y)
6 | f, r = true, ''
7 | (size * 2).times do
8 | r << (f ? c[rand * c.size] : v[rand * v.size])
9 | f = !f
10 | end
11 | r
12 | end
13 |
14 | def self.check_activity_roles
15 | return if ARGV.include?("db:autoupgrade") || ARGV.include?("db:automigrate")
16 |
17 | begin
18 | return unless Activity.first(:role_id => -1)
19 | rescue Exception => e
20 | p e
21 | raise "*** Please run db:autoupgrade"
22 | end
23 |
24 | puts "*** Updating activity role_id fields - please wait..."
25 | activities = Activity.all(:role_id => -1)
26 | count = activities.count
27 | activities.each_with_index do |a, i|
28 | puts "#{i} / #{count}"
29 | a.role = a.role_for_date
30 | a.save! # saving without validations on purpose, because some activities may be invalid
31 | # e.g. because types or properties were defined after they were created
32 | end
33 | end
34 | end
35 |
36 | decimal_separator_formats = {
37 | :dot => { :number => { :delimiter => '', :separator => '.' } },
38 | :comma => { :number => { :delimiter => '', :separator => ',' } }
39 | }
40 |
41 | Numeric::Transformer.add_format(decimal_separator_formats)
42 |
43 | DATE_FORMAT_NAMES = [:european, :american]
44 | DATE_FORMATS = { :european => { :format => "%d-%m-%Y", :description => "DD-MM-YYYY" }, :american => { :format => "%m/%d/%Y", :description => "MM/DD/YYYY" } }
45 | RECENT_DAYS_ON_LIST = [7, 14, 30]
46 | PASSWORD_RESET_LINK_EXP_TIME = 1.day
47 | DECIMAL_SEPARATORS = [:dot, :comma]
48 |
49 | CONFIG = {}
50 | end
51 |
52 | class DataMapper::Validate::ValidationErrors
53 | def to_json
54 | @errors.to_json
55 | end
56 | end
57 |
58 | if RUBY_VERSION < "1.8.7"
59 | class Fixnum
60 | def pred
61 | self - 1
62 | end
63 | end
64 |
65 | class Array
66 | def group_by
67 | inject({}) do |groups, element|
68 | (groups[yield(element)] ||= []) << element
69 | groups
70 | end
71 | end
72 | end
73 | end
74 |
75 | # TODO: this is temporary until merb-auth-more is updated to the latest dm-core API - solnic
76 | module DataMapper
77 | module Resource
78 | def new_record?
79 | new?
80 | end
81 | end
82 | end
83 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "http://rubygems.org"
2 |
3 | gem "do_mysql"
4 |
5 | dm_gems_version = "0.10.2"
6 |
7 | gem "dm-core", dm_gems_version
8 | gem "dm-aggregates", dm_gems_version
9 | gem "dm-timestamps", dm_gems_version
10 | gem "dm-types" , dm_gems_version
11 | gem "dm-validations", dm_gems_version
12 | gem "dm-migrations", dm_gems_version
13 | gem "dm-observer", dm_gems_version
14 | gem "dm-serializer", dm_gems_version
15 | # gem "dm-constraints", dm_gems_version # TODO: this doesn't work, throws some kind of SQL error during migration
16 | gem "dm-is-tree", dm_gems_version
17 | gem "dm-is-list", dm_gems_version
18 |
19 | merb_gems_version = "1.1.0.pre" # TODO: update to 1.1.1 when available; 1.1.0 final breaks mongrel with rack middlewares
20 |
21 | gem "merb-core", merb_gems_version
22 | gem "merb_datamapper", merb_gems_version
23 | gem "merb-assets", merb_gems_version
24 | gem "merb-helpers", merb_gems_version
25 | gem "merb-mailer", merb_gems_version
26 | gem "merb-slices", merb_gems_version
27 | gem "merb-auth-core", merb_gems_version
28 | gem "merb-auth-more", merb_gems_version
29 | gem "merb-auth-slice-password", merb_gems_version
30 | gem "merb-param-protection", merb_gems_version
31 | gem "merb-exceptions", merb_gems_version
32 | gem "net-ldap"
33 |
34 | git "git://github.com/mikeymicrophone/merb_dm_xss_terminate.git" do
35 | gem "merb_dm_xss_terminate"
36 | end
37 |
38 | gem "thin"
39 | gem "icalendar", "~>1.1.0"
40 | if RUBY_VERSION.include?('1.8')
41 | gem "fastercsv", '1.5.3'
42 | end
43 | gem 'rack_revision_info'
44 | gem 'nokogiri', '1.4.1' # for rack_revision_info
45 | # TODO: revision info doesn't work on the production now (which was the whole point) because Vlad deletes .git
46 | # directory from deployed code so there's no way to check current revision... this should be fixed when we switch
47 | # to Capistrano
48 | gem 'whenever', :require => false
49 | gem 'i18n' # for whenever
50 |
51 | group :development do
52 | gem 'vlad', '2.0.0', :require => []
53 | gem 'vlad-git', '2.0.0', :require => []
54 | end
55 |
56 | group :development, :test do
57 | gem "dm-factory_girl", "1.2.3", :require => "factory_girl", :git => "git://github.com/psionides/factory_girl_dm.git"
58 | gem "rspec", '1.3.0', :require => "spec"
59 | gem "rcov"
60 | gem "rcov_stats"
61 | gem "ci_reporter"
62 | gem "jslint_on_rails"
63 | if RUBY_VERSION.include?('1.9')
64 | gem 'ruby-debug19'
65 | else
66 | gem 'ruby-debug'
67 | gem 'linecache', '0.43'
68 | end
69 | gem 'delorean'
70 | end
71 |
72 | group :production do
73 | gem 'juicer'
74 | end
75 |
--------------------------------------------------------------------------------
/lib/tasks/install.rake:
--------------------------------------------------------------------------------
1 | desc "configure and install RubyTime"
2 | namespace :rubytime do
3 |
4 | task :install => :merb_env do
5 | Rake::Task['db:automigrate'].invoke
6 | puts "---------------- creating database structure \t [ \e[32mDONE\e[0m ]"
7 | Rake::Task['rubytime:kickstart'].invoke
8 | puts "---------------- populate database tables with initial data \t [ \e[32mDONE\e[0m ]"
9 | puts 'Roles automatically created:'
10 | Role.all.each { |role| puts " - #{role.name}"}
11 | puts 'Users automatically created:'
12 | User.all.each do |user|
13 | print " - #{user.name}"
14 | puts " (#{user.role.name})" rescue puts ' '
15 | end
16 | print 'Woud you like to create another role? [y/N] '
17 | Rake::Task['rubytime:create_role'].invoke if STDIN.gets.chomp =~ /[Yy]/
18 | print 'Woud you like to create new account? [y/N] '
19 | Rake::Task['rubytime:create_account'].invoke if STDIN.gets.chomp =~ /[Yy]/
20 | puts 'Default password for users is password'
21 | puts 'Thank you for installing RubyTime'
22 | end
23 |
24 | task :create_account do
25 | print '- login for new account: '
26 | account_login = STDIN.gets.chomp
27 | print '- password for new account (6 characters at least): '
28 | account_passwd = STDIN.gets.chomp
29 | print '- your name for new account: '
30 | account_name = STDIN.gets.chomp
31 | print '- email for new account: '
32 | account_email = STDIN.gets.chomp
33 | puts '- available roles:'
34 | Role.all.map do |role|
35 | puts "\t #{role.id}. #{role.name}"
36 | end
37 | puts "\t #{Role.all.count+1}. < without role >"
38 | print '- select number of role: '
39 | account_role_id = STDIN.gets.chomp.to_i
40 | settings = {}
41 | if (1..Role.all.count).include?(account_role_id)
42 | settings = {:role_id => account_role_id}
43 | else
44 | puts 'wrong role id !'
45 | exit
46 | end
47 | settings.merge!(:name => account_name, :login => account_login, :password => account_passwd, :password_confirmation => account_passwd, :email => account_email)
48 | new_user = Employee.new(settings)
49 | print "creating new account\t"
50 | puts new_user.save ? "[ \e[32mDONE\e[0m ]" : "[\e[31mFAILED\e[0m]"
51 | end
52 |
53 | task :create_role do
54 | print 'name of new role: '
55 | role_name = STDIN.gets.chomp
56 | role = Role.new(:name => role_name)
57 | print "creating new role\t"
58 | puts role.save ? "[ \e[32mDONE\e[0m ]" : "[\e[31mFAILED\e[0m]"
59 | end
60 |
61 | end
62 |
--------------------------------------------------------------------------------
/spec/models/activity_custom_property_value_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe ActivityCustomPropertyValue do
4 |
5 | describe "#value=" do
6 | before { @activity_custom_property_value = ActivityCustomPropertyValue.new }
7 |
8 | context "for numeric argument" do
9 | before { @activity_custom_property_value.value = 12.5 }
10 | it "should assign the value to :numeric_value" do
11 | @activity_custom_property_value.numeric_value.should == 12.5
12 | end
13 | end
14 |
15 | context "for nil argument" do
16 | before { @activity_custom_property_value.value = nil }
17 | it "should assign nil to :numeric_value" do
18 | @activity_custom_property_value.numeric_value.should == nil
19 | end
20 | end
21 |
22 | context "for blank string argument" do
23 | before { @activity_custom_property_value.value = "" }
24 | it "should assign nil to :numeric_value" do
25 | @activity_custom_property_value.numeric_value.should == nil
26 | end
27 | end
28 | end
29 |
30 | describe "#value" do
31 | before { @activity_custom_property_value = ActivityCustomPropertyValue.new }
32 |
33 | context "when :numeric_value has integer value" do
34 | before { @activity_custom_property_value.numeric_value = 59 }
35 |
36 | it "should return that value" do
37 | @activity_custom_property_value.value.should == 59
38 | end
39 |
40 | it "should return fixnum value" do
41 | @activity_custom_property_value.value.should be_instance_of(Fixnum)
42 | end
43 | end
44 |
45 | context "when :numeric_value has float value" do
46 | before { @activity_custom_property_value.numeric_value = 12.97 }
47 |
48 | it "should return that value" do
49 | @activity_custom_property_value.value.should == 12.97
50 | end
51 |
52 | it "should return float value" do
53 | @activity_custom_property_value.value.should be_instance_of(Float)
54 | end
55 | end
56 |
57 | context "when :numeric_value is nil" do
58 | before { @activity_custom_property_value.numeric_value = nil }
59 |
60 | it "should return nil" do
61 | @activity_custom_property_value.value.should == nil
62 | end
63 | end
64 | end
65 |
66 | context "with blank :value" do
67 | before { @activity_custom_property_value = ActivityCustomPropertyValue.new(:value => nil) }
68 |
69 | it { @activity_custom_property_value.should have_errors_on(:value) }
70 | end
71 |
72 | end
73 |
--------------------------------------------------------------------------------
/app/controllers/application.rb:
--------------------------------------------------------------------------------
1 | class Application < Merb::Controller
2 | before :ensure_authenticated
3 | before :set_api_version_header
4 |
5 | JSON_API_VERSION = 4 # see CHANGELOG-API.txt
6 |
7 | def current_user
8 | session.user
9 | end
10 |
11 | private
12 |
13 | # overriding param protection code from merb-param-protection, because it's stupid and can't handle nested params
14 | def self._filter_params(params)
15 | return params if self.log_params_args.nil?
16 | result = { }
17 | params.each do |k,v|
18 | if v.is_a?(Hash)
19 | result[k] = self._filter_params(v)
20 | else
21 | result[k] = (self.log_params_args.include?(k.to_sym) ? '[FILTERED]' : v)
22 | end
23 | end
24 | result
25 | end
26 |
27 | def self.protect_fields_for(record, fields = {})
28 | if fields[:in]
29 | before(nil, :only => fields[:in]) do |c|
30 | c.params[record] ||= {}
31 | fields_to_delete = []
32 | fields_to_delete += fields[:always] if fields[:always]
33 | fields_to_delete += fields[:admin] if fields[:admin] && !c.current_user.is_admin?
34 | fields_to_delete.each { |f| c.params[record].delete(f) }
35 | end
36 | end
37 | end
38 |
39 | def ensure_admin
40 | raise Forbidden unless current_user.is_admin?
41 | end
42 |
43 | def ensure_not_client_user
44 | raise Forbidden if current_user.is_client_user?
45 | end
46 |
47 | def ensure_user_that_can_manage_financial_data
48 | raise Forbidden unless current_user.can_manage_financial_data?
49 | end
50 |
51 | def ensure_not_client_user
52 | raise Forbidden if current_user.is_client_user?
53 | end
54 |
55 | def render_success(content = "", status = 200)
56 | render content, :layout => false, :status => status
57 | end
58 |
59 | def render_failure(content = "", status = 400)
60 | render content, :layout => false, :status => status
61 | end
62 |
63 | def number_of_columns
64 | 2
65 | end
66 |
67 | def client_api_version
68 | request.env["HTTP_X_API_VERSION"].to_i
69 | end
70 |
71 | def set_api_version_header
72 | if request.env["HTTP_X_API_VERSION"]
73 | headers["X-API-Version"] = JSON_API_VERSION.to_s
74 | end
75 | end
76 |
77 | def send_api_version_error
78 | render_failure("", 412) # :precondition_failed
79 | end
80 |
81 | def load_column_properties
82 | @custom_properties = ActivityCustomProperty.all
83 | @column_properties = @custom_properties.select(&:show_as_column_in_tables)
84 | @non_column_properties = @custom_properties.reject(&:show_as_column_in_tables)
85 | end
86 |
87 | end
88 |
--------------------------------------------------------------------------------
/app/controllers/invoices.rb:
--------------------------------------------------------------------------------
1 | class Invoices < Application
2 | before :ensure_admin, :exclude => [:index, :show]
3 | before :load_invoice, :only => [:edit, :update, :destroy, :show, :issue]
4 | before :load_invoices, :only => [:index, :create]
5 | before :load_clients, :only => [:index, :create, :edit]
6 | before :load_column_properties, :only => [:show]
7 |
8 | def index
9 | raise Forbidden unless current_user.is_client_user? || current_user.is_admin?
10 | @invoice = Invoice.new
11 | render
12 | end
13 |
14 | def show
15 | raise Forbidden unless current_user.can_see_invoice?(@invoice)
16 | @activities = @invoice.activities.all(:order => [:created_at.desc])
17 | render
18 | end
19 |
20 | def edit
21 | raise Forbidden unless current_user.is_admin?
22 | render
23 | end
24 |
25 | def update
26 | if @invoice.update(params[:invoice]||{})
27 | if request.xhr?
28 | @invoice.to_json
29 | else
30 | redirect resource(@invoice, filter_hash), :message => { :notice => "Invoice has been updated" }
31 | end
32 | else
33 | load_clients
34 |
35 | render :edit
36 | end
37 | end
38 |
39 | def create
40 | @invoice = Invoice.new(params[:invoice].merge(:user_id => current_user.id))
41 | if @invoice.save
42 | request.xhr? ? "" : redirect(resource(:invoices))
43 | else
44 | if request.xhr?
45 | render_failure smart_errors_format(@invoice.errors)
46 | else
47 | render :index
48 | end
49 | end
50 | end
51 |
52 | def destroy
53 | if @invoice.destroy
54 | render_success
55 | else
56 | render_failure "This invoice has been issued. Couldn't delete."
57 | end
58 | end
59 |
60 | def issue
61 | @invoice.issue!
62 | redirect resource(@invoice), :message => { :notice => "Invoice has been issued" }
63 | rescue Exception => e
64 | load_column_properties
65 | @activities = @invoice.activities.all(:order => [:created_at.desc])
66 | message[:error] = e.to_s
67 | render :show
68 | end
69 |
70 | protected
71 | def load_invoice
72 | @invoice = Invoice.get(params[:id]) or raise NotFound
73 | end
74 |
75 | def load_invoices
76 | filter = params[:filter] || :all
77 | @invoices = (current_user.is_client_user? ? current_user.client.invoices : Invoice.all).send(filter).all(:order => [:created_at.desc])
78 | end
79 |
80 | def load_clients
81 | @clients = Client.active.all(:order => [:name])
82 | end
83 |
84 | def number_of_columns
85 | params[:action] == "show" || (params[:action] == "index" || params[:action] == "create") && !current_user.is_admin? ? 1 : super
86 | end
87 | end
88 |
--------------------------------------------------------------------------------
/app/views/activities/_activity_form.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%= form_for @activity, :action => @activity.new? ? url(:activities) : resource(@activity), :class => "activity_form #{@activity.new? ? 'dark': ''}" do %>
4 |
5 | <% if @activity.new? %>New activity <% end %>
6 | <% if current_user.is_admin? %>
7 |
8 | <%= select :user_id, :label => "User", :collection => @users.map { |u| [u.id, u.name] } %>
9 |
10 | <% end %>
11 |
12 | <%= text_field :date, :label => "Date", :class => 'datepicker', :value => @activity.date.formatted(current_user.date_format) %>
13 |
14 |
15 | <%= select :project_id, :label => "Project", :collection => {
16 | "Recent projects" => @recent_projects.map { |p| [p.id, p.name] },
17 | "Other projects" => @other_projects.map { |p| [p.id, p.name] } } %>
18 |
19 | <%= hidden_field :main_activity_type_id, :value => nil %>
20 | >
21 | <%= select :main_activity_type_id, :label => "Type", :collection => @activity.available_main_activity_types.map { |at| [at.id, at.name] }, :selected => @activity.main_activity_type_id.to_s, :disabled => @activity.available_main_activity_types.empty? %>
22 |
23 | >
24 | <%= select :sub_activity_type_id, :label => "Sub-type", :collection => @activity.available_sub_activity_types.map { |at| [at.id, at.name] }, :selected => @activity.sub_activity_type_id.to_s, :disabled => @activity.available_sub_activity_types.empty? %>
25 |
26 |
27 | <%= text_field :hours, :label => "Time spent" %>
28 | e.g. 7:30, 7.5, or 7.5h for seven hours and thirty minutes;
29 | 0.5, .5h or 30m for half an hour
30 |
31 | <% @activity_custom_properties.each do |activity_custom_property| %>
32 |
33 | <%= label activity_custom_property.name, :for => "activity[custom_properties][#{activity_custom_property.id}]" %>
34 | <%= tag :input, nil, :name => "activity[custom_properties][#{activity_custom_property.id}]", :value => @activity.custom_properties[activity_custom_property.id], :class => (activity_custom_property.required ? 'required' : nil) %>
35 | <%= activity_custom_property.unit %>
36 |
37 | <% end %>
38 |
39 | <%= text_area :comments, :label => "Comments" %>
40 |
41 |
42 | <%= submit((@activity.new? ? "Add" : "Save"), :class => "button") %> or <%= link_to "Cancel", "#", :id => "close_activity_form" %>
43 |
44 |
45 | <% end =%>
46 |
--------------------------------------------------------------------------------
/config/deploy.rb.example:
--------------------------------------------------------------------------------
1 | set :repository, 'git://github.com/LunarLogicPolska/rubytime.git'
2 | set :use_sudo, false
3 | set :revision, ENV['TAG'] || "origin/stable"
4 | set :merb_env, 'production'
5 |
6 | task :staging do
7 | set :application, "rubytime"
8 | set :domain, "user@staging.server.com"
9 | set :deploy_to, "/var/www/#{application}"
10 | end
11 |
12 | task :production do
13 | set :application, "rubytime"
14 | set :domain, "user@server.com"
15 | set :deploy_to, "/var/www/#{application}"
16 | end
17 |
18 | namespace :vlad do
19 | remote_task :update_crontab, :roles => [:app] do
20 | run "cd #{current_path}; MERB_ENV=#{merb_env} bundle exec whenever --update-crontab #{application}"
21 | end
22 |
23 | remote_task :symlink_configs, :roles => [:app] do
24 | run "ln -s #{shared_path}/config/database.yml #{current_path}/config/database.yml"
25 | run "ln -s #{shared_path}/config/local_config.rb #{current_path}/config/local_config.rb"
26 | end
27 |
28 | remote_task :install_gems, :roles => [:app] do
29 | run "cd #{current_path}; bundle install --without test development"
30 | end
31 |
32 | remote_task :start_web, :roles => [:web] do
33 | run "if [ -f #{shared_path}/system/maintenance.html ]; then rm -f #{shared_path}/system/maintenance.html; fi"
34 | end
35 |
36 | remote_task :stop_web, :roles => [:web] do
37 | run "cp -f #{shared_path}/config/maintenance.html #{shared_path}/system/"
38 | end
39 |
40 | remote_task :restart_app, :roles => [:app] do
41 | run "touch #{current_path}/tmp/restart.txt"
42 | end
43 |
44 | remote_task :start do
45 | Rake::Task['vlad:restart_app'].invoke
46 | Rake::Task['vlad:start_web'].invoke
47 | end
48 |
49 | remote_task :stop do
50 | Rake::Task['vlad:stop_web'].invoke
51 | end
52 |
53 | remote_task :build_asset_bundles do
54 | run "juicer merge #{current_path}/public/stylesheets/master.css -d #{current_path}/public"
55 | end
56 |
57 | namespace :db do
58 | remote_task :autoupgrade, :roles => [:app] do
59 | run "cd #{current_path}; MERB_ENV=#{merb_env} bundle exec rake db:autoupgrade"
60 | end
61 | end
62 |
63 | namespace :db do
64 | remote_task :reset, :roles => [:app] do
65 | run "cd #{current_path}; MERB_ENV=#{merb_env} bundle exec rake db:automigrate"
66 | end
67 | end
68 |
69 | remote_task :tail, :roles => [:app] do
70 | run "tail -n1000 -f #{current_path}/log/#{merb_env}.log"
71 | end
72 |
73 | PREPARING_TASKS = [
74 | 'vlad:stop', 'vlad:update', 'vlad:symlink_configs', 'vlad:install_gems', 'vlad:build_asset_bundles', 'vlad:update_crontab'
75 | ]
76 |
77 | task :deploy => PREPARING_TASKS + ['vlad:start']
78 | task :deploy_with_autoupgrade => PREPARING_TASKS + ['vlad:db:autoupgrade', 'vlad:start']
79 | task :deploy_with_db_reset => PREPARING_TASKS + ['vlad:db:reset', 'vlad:start']
80 | end
81 |
--------------------------------------------------------------------------------
/spec/controllers/activity_custom_properties_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe ActivityCustomProperties do
4 |
5 | it "shouldn't show any action for guest, employee or client's user" do
6 | [:index, :create, :edit, :update, :destroy].each do |action|
7 | block_should(raise_unauthenticated) { as(:guest).dispatch_to(ActivityCustomProperties, action) }
8 | block_should(raise_forbidden) { as(:employee).dispatch_to(ActivityCustomProperties, action) }
9 | block_should(raise_forbidden) { as(:client).dispatch_to(ActivityCustomProperties, action) }
10 | end
11 | end
12 |
13 | describe "#index" do
14 | it "should show a list of all custom properties" do
15 | ActivityCustomProperty.all.destroy!
16 | properties = (0..2).map { ActivityCustomProperty.generate }
17 | response = as(:admin).dispatch_to(ActivityCustomProperties, :index)
18 | response.should be_successful
19 | response.instance_variable_get("@new_activity_custom_property").should be_an(ActivityCustomProperty)
20 | response.instance_variable_get("@activity_custom_properties").should == properties
21 | end
22 | end
23 |
24 | describe "#create" do
25 | it "should create a new custom property successfully and redirect to index" do
26 | block_should(change(ActivityCustomProperty, :count)) do
27 | response = as(:admin).dispatch_to(ActivityCustomProperties, :create, {
28 | :activity_custom_property => {
29 | :name => "Kill count",
30 | :unit => "Enemies",
31 | :required => false
32 | }
33 | })
34 | response.should redirect_to(resource(:activity_custom_properties))
35 | ActivityCustomProperty.last.name.should == "Kill count"
36 | end
37 | end
38 | end
39 |
40 | describe "#edit" do
41 | it "should show property edit form" do
42 | property = ActivityCustomProperty.generate
43 | as(:admin).dispatch_to(ActivityCustomProperties, :edit, :id => property.id).should be_successful
44 | end
45 | end
46 |
47 | describe "#update" do
48 | it "should update the property" do
49 | property = ActivityCustomProperty.generate :name => 'OriginalName'
50 | response = as(:admin).dispatch_to(ActivityCustomProperties, :update, {
51 | :id => property.id,
52 | :activity_custom_property => {
53 | :name => 'NewName'
54 | }
55 | })
56 | response.should redirect_to(resource(:activity_custom_properties))
57 | ActivityCustomProperty.get(property.id).name.should == 'NewName'
58 | end
59 | end
60 |
61 | describe "#destroy" do
62 | it "should delete the property" do
63 | property = ActivityCustomProperty.generate
64 | block_should(change(ActivityCustomProperty, :count).by(-1)) do
65 | as(:admin).dispatch_to(ActivityCustomProperties, :destroy, :id => property.id)
66 | end
67 | end
68 | end
69 |
70 | end
71 |
--------------------------------------------------------------------------------
/spec/models/role_activities_in_project_summary_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe RoleActivitiesInProjectSummary do
4 |
5 | before :all do
6 | @euro = Currency.first_or_generate :singular_name => 'euro'
7 | @dollar = Currency.first_or_generate :singular_name => 'dollar'
8 | end
9 |
10 | context "without any activities" do
11 | before do
12 | @user = mock('user', :role => mock('role'))
13 | @summary = RoleActivitiesInProjectSummary.new(@user.role, [])
14 | end
15 |
16 | it "should create the summary" do
17 | @summary.role.should == @user.role
18 | @summary.non_billable_time.should == 0
19 | @summary.billable_time.should == 0
20 | @summary.price[@euro].should == Money.new(0, @euro)
21 | @summary.price[@dollar].should == Money.new(0, @dollar)
22 | end
23 | end
24 |
25 | context "with some activities" do
26 | before do
27 | @role = Role.first_or_generate
28 | @user = mock('user', :role => @role)
29 | @summary = RoleActivitiesInProjectSummary.new(@user.role, [
30 | mock('activity', :role_id => @role.id, :duration => 1.hour, :price => Money.new(20, @euro)),
31 | mock('activity', :role_id => @role.id, :duration => 1.hour + 20.minutes, :price => nil),
32 | mock('activity', :role_id => @role.id, :duration => 15.minutes, :price => Money.new( 7, @dollar)),
33 | mock('activity', :role_id => @role.id, :duration => 45.minutes, :price => nil),
34 | mock('activity', :role_id => @role.id, :duration => 5.minutes, :price => Money.new(11, @euro))
35 | ])
36 | end
37 |
38 | it "should create the summary" do
39 | @summary.role.should == @user.role
40 | @summary.non_billable_time.should == 2.hours + 5.minutes
41 | @summary.billable_time.should == 1.hour + 20.minutes
42 | @summary.price[@euro].should == Money.new(31, @euro)
43 | @summary.price[@dollar].should == Money.new(7, @dollar)
44 | end
45 | end
46 |
47 | describe "#<<" do
48 | before do
49 | @role = Role.first_or_generate
50 | @summary = RoleActivitiesInProjectSummary.new(@role, [])
51 | end
52 |
53 | context "if called with activity of proper role" do
54 | before { @summary << mock('activity', :role_id => @role.id, :duration => 1.hour, :price => nil) }
55 | it "should add activity" do
56 | @summary.non_billable_time.should == 1.hour
57 | end
58 | end
59 |
60 | context "if called with activity of improper role" do
61 | it "should add activity" do
62 | block_should(raise_error(ArgumentError)) do
63 | @summary << mock('activity',
64 | :role_id => Role.generate.id,
65 | :duration => 1.hour,
66 | :price => nil
67 | )
68 | end
69 | end
70 | end
71 | end
72 |
73 | end
74 |
--------------------------------------------------------------------------------
/app/views/layout/application.html.erb:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 | Rubytime
6 |
7 |
8 |
9 | <%= css_include_tag Merb.env?('production') ? 'master.min' : 'master' %>
10 | <%= js_include_tag 'lib/jquery.min.js',
11 | 'lib/jquery-ui-1.8.custom.min.js',
12 | 'lib/jquery.validate.min.js',
13 | 'lib/jquery-extensions.js',
14 | 'lib/jquery-scrollto.js',
15 | 'application.js',
16 | 'lib/rubytime/input-filler.js',
17 | 'lib/thickbox',
18 | 'indicator.js',
19 | :bundle => :base %>
20 | <%= auto_link %>
21 | <% if session.user %>
22 |
25 | <% end %>
26 |
27 |
28 |
29 |
30 |
35 | <% if session.authenticated? && params[:controller] != 'exceptions' -%>
36 |
37 | <%= partial 'layout/session_info' %>
38 |
39 | <%= partial 'layout/navigation' %>
40 | <% end -%>
41 |
42 |
43 |
44 |
45 |
46 |
47 | <%= h message[message.keys.first] %>
48 |
49 |
50 | <%= catch_content :for_layout %>
51 |
52 | <% if number_of_columns == 2 %>
53 |
54 |
<%= catch_content :secondary %>
55 |
56 | <% end %>
57 |
58 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/config/router.rb:
--------------------------------------------------------------------------------
1 | # Merb::Router is the request routing mapper for the merb framework.
2 | #
3 | # You can route a specific URL to a controller / action pair:
4 | #
5 | # r.match("/contact").
6 | # to(:controller => "info", :action => "contact")
7 | #
8 | # You can define placeholder parts of the url with the :symbol notation. These
9 | # placeholders will be available in the params hash of your controllers. For example:
10 | #
11 | # r.match("/books/:book_id/:action").
12 | # to(:controller => "books")
13 | #
14 | # Or, use placeholders in the "to" results for more complicated routing, e.g.:
15 | #
16 | # r.match("/admin/:module/:controller/:action/:id").
17 | # to(:controller => ":module/:controller")
18 | #
19 | # You can also use regular expressions, deferred routes, and many other options.
20 | # See merb/specs/merb/router.rb for a fairly complete usage sample.
21 |
22 | Merb.logger.info("Compiling routes...")
23 |
24 | Merb::Router.prepare do
25 |
26 | match("/users/:user_id/calendar").
27 | to(:controller => "activities", :action => "calendar")
28 | match("/projects/:project_id/calendar").
29 | to(:controller => "activities", :action => "calendar")
30 |
31 | match("/free_days/:access_key(.:format)", :method => 'get').
32 | to(:controller => "free_days", :action => "index").name(:free_days_index)
33 |
34 | match("/invoices/issued").
35 | to(:controller => "invoices", :action => "index", :filter => "issued").name(:issued_invoices)
36 | match("/invoices/pending").
37 | to(:controller => "invoices", :action => "index", :filter => "pending").name(:pending_invoices)
38 |
39 | match('/signin').
40 | to(:controller => 'exceptions', :action => 'unauthenticated').name(:signin)
41 |
42 | resources :users,
43 | :member => {
44 | "settings" => :get
45 | },
46 | :collection => {
47 | "with_roles" => :get,
48 | "request_password" => :get,
49 | "reset_password" => :get,
50 | "authenticate" => :get,
51 | "with_activities" => :get
52 | } do
53 | resource :calendar
54 | resources :activities
55 | end
56 |
57 | resources :sessions
58 | resources :currencies
59 | resources :activities, :collection => { "day" => :get }
60 | resources :clients
61 | resources :projects,
62 | :collection => { "for_clients" => :get },
63 | :member => { "set_default_activity_type" => :put } do
64 | resource :calendar
65 | resources :activities
66 | end
67 | resources :roles
68 | resources :free_days, :collection => { "delete" => :delete }
69 | resources :hourly_rates
70 | resources :invoices, :member => { "issue" => :put }
71 | resource :settings, :controller => 'settings' # TODO conflict with line 30, # .name(:settings)
72 |
73 | resources :activity_types, :collection => { "available" => :get, "for_projects" => :get }
74 | resources :activity_custom_properties
75 |
76 | # Adds the required routes for merb-auth using the password slice
77 | slice(:merb_auth_slice_password, :name_prefix => nil, :path_prefix => "")
78 |
79 | default_routes
80 |
81 | match('/').to(:controller => 'home', :action => 'index').name(:root)
82 | end
83 |
--------------------------------------------------------------------------------
/app/models/hourly_rate.rb:
--------------------------------------------------------------------------------
1 | class HourlyRate
2 | include DataMapper::Resource
3 |
4 | property :id, Serial
5 | property :project_id, Integer, :required => true
6 | property :role_id, Integer, :required => true
7 | property :takes_effect_at, Date, :required => true
8 | property :value, BigDecimal, :scale => 2, :precision => 10, :required => true
9 | property :currency_id, Integer, :required => true
10 |
11 | belongs_to :project
12 | belongs_to :role
13 | belongs_to :currency
14 |
15 | validates_is_unique :takes_effect_at, :scope => [:project_id, :role_id], :message => 'There already exists hourly rate for that project, role and date.'
16 | validates_present :operation_author
17 |
18 | default_scope(:default).update(:order => [:takes_effect_at])
19 |
20 | attr_accessor :operation_author
21 | attr_accessor :date_format_for_json
22 |
23 | def value=(value)
24 | attribute_set(:value, value.blank? ? nil : value)
25 | end
26 |
27 | def value_formatted
28 | value.to_s('F')
29 | end
30 |
31 | def succ
32 | HourlyRate.first(:takes_effect_at.gt => takes_effect_at, :project_id => project.id, :role_id => role.id, :order => [:takes_effect_at.asc])
33 | end
34 |
35 | def activities
36 | conditions = {
37 | :project_id => project_id,
38 | :role_id => role_id,
39 | :date.gte => takes_effect_at,
40 | :order => [:date.asc, :project_id.asc, :id.asc]
41 | }
42 | conditions[:date.lte] = succ.takes_effect_at - 1 if succ
43 | Activity.all(conditions)
44 | end
45 |
46 | def self.find_for(params)
47 | # TODO optimize this method, so it does not query DB each time activity.price is called (see any report showing prices )
48 | return nil if params[:role_id].nil? || params[:project_id].nil? || params[:date].nil?
49 | first(
50 | :conditions => ["takes_effect_at <= ? AND project_id = ? AND role_id = ? ", params[:date], params[:project_id], params[:role_id] ],
51 | :order => [:takes_effect_at.desc]
52 | )
53 | end
54 |
55 | def self.find_for_activity(activity)
56 | return nil unless activity.user_id && activity.date && activity.project_id
57 | find_for(
58 | :role_id => activity.role_id,
59 | :project_id => activity.project_id,
60 | :date => activity.date
61 | )
62 | end
63 |
64 | def to_json
65 | { :id => id,
66 | :project_id => project_id,
67 | :role_id => role_id,
68 | :takes_effect_at => (date_format_for_json ? takes_effect_at.formatted(date_format_for_json) : takes_effect_at),
69 | :takes_effect_at_unformatted => takes_effect_at,
70 | :value => value_formatted,
71 | :currency => currency,
72 | :error_messages => error_messages
73 | }.to_json
74 | end
75 |
76 | def to_money
77 | Money.new(value, currency)
78 | end
79 |
80 | def error_messages
81 | errors.full_messages.join('. ')
82 | end
83 |
84 | def *(numeric)
85 | to_money * numeric
86 | end
87 |
88 | before :destroy do
89 | if activities.count > 0
90 | errors.add(:base, 'Cannot destroy: There are activities that use this hourly rate.')
91 | throw :halt
92 | end
93 | end
94 |
95 | end
96 |
--------------------------------------------------------------------------------