├── 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 | 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 | 6 | 7 | 8 | 9 | 10 | <%= partial :client, :with => @clients %> 11 |
    NameEmailActive
    -------------------------------------------------------------------------------- /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 | 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 | 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 | 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 | 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 |
    11 |

    Comments:

    12 |

    <%= activity.comments.strip.gsub("\n", "
    ") %>

    13 |
    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 |
    2 | <%= link_to("« Previous", prev_day_url, :id => "prev_day_link") %> 3 | <%= @day %> 4 | <%= link_to("Next »", next_day_url, :id => "next_day_link") %> 5 |
    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 | 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 | 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 | 10 | 11 | 12 | 13 | <% @roles.each do |role| %> 14 | 15 | 16 | 17 | 21 | 22 | <% end %> 23 |
    NameCan manage financial data?
    <%= role.name %><%= role.can_manage_financial_data ? 'yes' : 'no' %> 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 |
    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 | 4 | 5 | 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 | 15 | 16 | 17 | 18 | <% end %> 19 | 20 | <% if data.length > 1 %> 21 | 22 | 23 | 24 | 25 | 26 | <% end %> 27 | 28 |
    RoleHoursPrice
    <%= summary.role.name %><%= summary.billable_time %><%= summary.price %>
    Total:<%= data.inject(0.0) { |sum, r| sum + r.billable_time } %><%= data.inject(Purse.new) { |sum, r| sum.merge(r.price) } %>
    29 | -------------------------------------------------------------------------------- /app/views/users/_users.html.erb: -------------------------------------------------------------------------------- 1 |

    List of <%= type %>

    2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | <% users.each do |user| %> 11 | 12 | 13 | 14 | 15 | 20 | 21 | <% end %> 22 |
    Name<%= type == 'employees' ? 'Role' : 'Client' %>Active
    <%= link_to(user.name, url(:user, user)) %><%= type == 'employees' ? link_to(user.role.name, resource(user.role)) : link_to(user.client.name, resource(user.client)) %><%= user.active? ? 'Yes' : 'No' %> 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 |
    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 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | <% @currencies.each do |currency| %> 17 | 18 | 19 | 20 | 21 | 22 | 23 | 27 | 28 | <% end %> 29 |
    Singular namePlural namePrefixSuffixExample
    <%= currency.singular_name %><%= currency.plural_name %><%= currency.prefix %><%= currency.suffix %><%= currency.render(12345.67) %> 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 |
    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 | 11 | 12 | 13 | <% @activity_type.children.each do |activity_type| %> 14 | 15 | 16 | 24 | 25 | <% end %> 26 |
    Name
    <%= activity_type.name %> 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 |
    27 | <% end %> 28 | -------------------------------------------------------------------------------- /app/views/activities/_grouped_projects.html.erb: -------------------------------------------------------------------------------- 1 | 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 | 11 | 12 | 13 | 14 | <% @activity_types.each do |activity_type| %> 15 | 16 | 17 | 18 | 26 | 27 | <% end %> 28 |
    NameSub activity types
    <%= link_to activity_type.name, resource(activity_type) %><%= activity_type_children_summary(activity_type) %> 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 |
    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 |
    9 | 10 |
    11 |

    12 | 13 | <%= tag :input, :name => @login_param, :id => @login_param, :value => params[@login_param] %> 14 |

    15 |

    16 | 17 | <%= tag :input, :name => @password_param, :id => @password_param, :type => 'password' %> 18 |

    19 |

    20 | <%= check_box :name => "remember_me", :id => 'remember_me', :value => "1", :label => "Remember me" %> 21 |

    22 |

    23 | 24 |

    25 |
    26 |
    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 | 10 | <% if current_user.is_admin? %> 11 | 12 | <% end %> 13 | 14 | 15 | 16 | <% @projects.each do |project| %> 17 | 18 | 19 | <% if current_user.is_admin? %> 20 | 21 | <% end %> 22 | 23 | 30 | 31 | <% end %> 32 |
    NameClientActive
    <%= current_user.is_admin? ? link_to(project.name, resource(project)) : project.name %><%= link_to project.client.name, resource(project.client) %><%= project.active ? 'Yes' : 'No' %> 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 |
    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 | 10 | <% unless current_user.is_client_user? %><% end %> 11 | 12 | 13 | 14 | 15 | <% @invoices.each do |invoice| %> 16 | 17 | 18 | <% unless current_user.is_client_user? %><% end %> 19 | 20 | 21 | 24 | 25 | <% end %> 26 |
    NameClientCreated atIssued at
    <%= link_to(invoice.name, resource(invoice, filter_hash)) %><%= invoice.client.name %><%= invoice.created_at.to_time.to_date.formatted(current_user.date_format) %><%= invoice.issued? ? invoice.issued_at.to_time.to_date.formatted(current_user.date_format) : "-" %> 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? %>
    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 | 24 | 25 | 26 | 27 | <% end %> 28 |
    <%= project.name %><%= link_to "Show", url(:project, project) %><%= link_to "Edit", url(:edit_project, project) %>
    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 | 40 | 41 | 42 | 43 | 44 | <% end %> 45 |
    <%= user.name %><%= user.login %><%= link_to "Show", url(:user, user) %><%= link_to "Edit", url(:edit_user, user) %>
    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 | 11 | 12 | 13 | 14 | 15 | 16 | <% @activity_custom_properties.each do |custom_property| %> 17 | 18 | 19 | 20 | 21 | 22 | 30 | 31 | <% end %> 32 |
    NameUnitRequiredShow as column
    <%= custom_property.name %><%= custom_property.unit %><%= custom_property.required ? 'Yes' : 'No' %><%= custom_property.show_as_column_in_tables ? 'Yes' : 'No' %> 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 |
    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 |
    3 | 4 | 5 |

    pocket rocket web framework

    6 |
    7 |
    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 |

    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 | 17 | <%= check_box :active %> 18 |

    19 | 20 |
    21 | Assigned activity types 22 | <%= tag :input, :type => :hidden, :name => 'project[activity_type_ids][]' %> 23 | 24 | 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 | 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 | 30 | <%= check_box :admin %> 31 |

    32 |

    33 | 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 | 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 | <%= @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 |
    3 | 4 | 5 |

    pocket rocket web framework

    6 |
    7 |
    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 | 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 |

    31 | 32 | Rubytime 33 | 34 |

    35 | <% if session.authenticated? && params[:controller] != 'exceptions' -%> 36 |
    37 | <%= partial 'layout/session_info' %> 38 |
    39 | <%= partial 'layout/navigation' %> 40 | <% end -%> 41 |
    42 |
    43 |
    44 | 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 | --------------------------------------------------------------------------------