public
Description: Tracks is a GTD(TM) web application, built with Ruby on Rails
Homepage: http://www.rousette.org.uk/projects/
Clone URL: git://github.com/bsag/tracks.git
Click here to lend your support to: tracks and make a donation at www.pledgie.com !
tracks / app / controllers / application.rb
100644 267 lines (223 sloc) 8.878 kb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
# The filters added to this controller will be run for all controllers in the
# application. Likewise will all the methods added be available for all
# controllers.
 
require_dependency "login_system"
require_dependency "tracks/source_view"
require "redcloth"
 
require 'date'
require 'time'
 
# Commented the following line because of #744. It prevented rake db:migrate to
# run because this tag went looking for the taggings table that did not exist
# when you feshly create a new database Old comment: We need this in development
# mode, or you get 'method missing' errors
#
# Tag
 
class ApplicationController < ActionController::Base
 
  protect_from_forgery :secret => SALT
 
  helper :application
  include LoginSystem
  helper_method :current_user, :prefs
 
  layout proc{ |controller| controller.mobile? ? "mobile" : "standard" }
  
  before_filter :set_session_expiration
  before_filter :set_time_zone
  prepend_before_filter :login_required
  prepend_before_filter :enable_mobile_content_negotiation
  after_filter :restore_content_type_for_mobile
  after_filter :set_charset
  
 
 
  include ActionView::Helpers::TextHelper
  include ActionView::Helpers::SanitizeHelper
  helper_method :format_date, :markdown
 
  # By default, sets the charset to UTF-8 if it isn't already set
  def set_charset
    headers["Content-Type"] ||= "text/html; charset=UTF-8"
  end
  
  def set_session_expiration
    # http://wiki.rubyonrails.com/rails/show/HowtoChangeSessionOptions
    unless session == nil
      return if @controller_name == 'feed' or session['noexpiry'] == "on"
      # If the method is called by the feed controller (which we don't have
      # under session control) or if we checked the box to keep logged in on
      # login don't set the session expiry time.
      if session
        # Get expiry time (allow ten seconds window for the case where we have
        # none)
        expiry_time = session['expiry_time'] || Time.now + 10
        if expiry_time < Time.now
          # Too late, matey... bang goes your session!
          reset_session
        else
          # Okay, you get another hour
          session['expiry_time'] = Time.now + (60*60)
        end
      end
    end
  end
  
  def render_failure message, status = 404
    render :text => message, :status => status
  end
  
  # def rescue_action(exception)
  # log_error(exception) if logger
  # respond_to do |format|
  # format.html do
  # notify :warning, "An error occurred on the server."
  # render :action => "index"
  # end
  # format.js { render :action => 'error' }
  # format.xml { render :text => 'An error occurred on the server.' + $! }
  # end
  # end
  
  # Returns a count of next actions in the given context or project The result
  # is count and a string descriptor, correctly pluralised if there are no
  # actions or multiple actions
  #
  def count_undone_todos_phrase(todos_parent, string="actions")
    count = count_undone_todos(todos_parent)
    if count == 1
      word = string.singularize
    else
      word = string.pluralize
    end
    return count.to_s + "&nbsp;" + word
  end
  
  def count_undone_todos(todos_parent)
    if todos_parent.nil?
      count = 0
    elsif (todos_parent.is_a?(Project) && todos_parent.hidden?)
      count = eval "@project_project_hidden_todo_counts[#{todos_parent.id}]"
    else
      count = eval "@#{todos_parent.class.to_s.downcase}_not_done_counts[#{todos_parent.id}]"
    end
    count || 0
  end
 
  # Convert a date object to the format specified in the user's preferences in
  # config/settings.yml
  #
  def format_date(date)
    if date
      date_format = prefs.date_format
      formatted_date = date.strftime("#{date_format}")
    else
      formatted_date = ''
    end
    formatted_date
  end
 
  # Uses RedCloth to transform text using either Textile or Markdown Need to
  # require redcloth above RedCloth 3.0 or greater is needed to use Markdown,
  # otherwise it only handles Textile
  #
  def markdown(text)
    RedCloth.new(text).to_html
  end
  
  def build_default_project_context_name_map(projects)
    Hash[*projects.reject{ |p| p.default_context.nil? }.map{ |p| [p.name, p.default_context.name] }.flatten].to_json
  end
  
  # Here's the concept behind this "mobile content negotiation" hack: In
  # addition to the main, AJAXy Web UI, Tracks has a lightweight low-feature
  # 'mobile' version designed to be suitablef or use from a phone or PDA. It
  # makes some sense that tne pages of that mobile version are simply alternate
  # representations of the same Todo resources. The implementation goal was to
  # treat mobile as another format and be able to use respond_to to render both
  # versions. Unfortunately, I ran into a lot of trouble simply registering a
  # new mime type 'text/html' with format :m because :html already is linked to
  # that mime type and the new registration was forcing all html requests to be
  # rendered in the mobile view. The before_filter and after_filter hackery
  # below accomplishs that implementation goal by using a 'fake' mime type
  # during the processing and then setting it to 'text/html' in an
  # 'after_filter' -LKM 2007-04-01
  def mobile?
    return params[:format] == 'm' || response.content_type == MOBILE_CONTENT_TYPE
  end
 
  def enable_mobile_content_negotiation
    if mobile?
      request.accepts.unshift(Mime::Type::lookup(MOBILE_CONTENT_TYPE))
    end
  end
 
  def restore_content_type_for_mobile
    if mobile?
      response.content_type = 'text/html'
    end
  end
  
  def create_todo_from_recurring_todo(rt, date=nil)
    # create todo and initialize with data from recurring_todo rt
    todo = current_user.todos.build( { :description => rt.description, :notes => rt.notes, :project_id => rt.project_id, :context_id => rt.context_id})
    
    # set dates
    todo.recurring_todo_id = rt.id
    todo.due = rt.get_due_date(date)
    # make sure that show_from is not in the past
    show_from_date = rt.get_show_from_date(date)
    if show_from_date.nil?
      todo.show_from=nil
    else
      todo.show_from = show_from_date < Time.now.utc ? nil : show_from_date
    end
    
    saved = todo.save
    if saved
      todo.tag_with(rt.tag_list, current_user)
      todo.tags.reload
    end
 
    # increate number of occurences created from recurring todo
    rt.inc_occurences
    
    # mark recurring todo complete if there are no next actions left
    checkdate = todo.due.nil? ? todo.show_from : todo.due
    rt.toggle_completion! unless rt.has_next_todo(checkdate)
    
    return saved ? todo : nil
  end
     
  protected
  
  def admin_login_required
    unless User.find_by_id_and_is_admin(session['user_id'], true)
      render :text => "401 Unauthorized: Only admin users are allowed access to this function.", :status => 401
      return false
    end
  end
  
  def redirect_back_or_home
    respond_to do |format|
      format.html { redirect_back_or_default home_url }
      format.m { redirect_back_or_default mobile_url }
    end
  end
  
  def boolean_param(param_name)
    return false if param_name.blank?
    s = params[param_name]
    return false if s.blank? || s == false || s =~ /^false$/i
    return true if s == true || s =~ /^true$/i
    raise ArgumentError.new("invalid value for Boolean: \"#{s}\"")
  end
  
  def self.openid_enabled?
    Tracks::Config.openid_enabled?
  end
  
  def openid_enabled?
    self.class.openid_enabled?
  end
 
  private
        
  def parse_date_per_user_prefs( s )
    prefs.parse_date(s)
  end
    
  def init_data_for_sidebar
    @projects = @projects || current_user.projects.find(:all, :include => [:default_context ])
    @contexts = @contexts || current_user.contexts
    init_not_done_counts
    if prefs.show_hidden_projects_in_sidebar
      init_project_hidden_todo_counts(['project'])
    end
  end
  
  def init_not_done_counts(parents = ['project','context'])
    parents.each do |parent|
      eval("@#{parent}_not_done_counts = @#{parent}_not_done_counts || Todo.count(:conditions => ['user_id = ? and state = ?', current_user.id, 'active'], :group => :#{parent}_id)")
    end
  end
  
  def init_project_hidden_todo_counts(parents = ['project','context'])
    parents.each do |parent|
      eval("@#{parent}_project_hidden_todo_counts = @#{parent}_project_hidden_todo_counts || Todo.count(:conditions => ['user_id = ? and (state = ? or state = ?)', current_user.id, 'project_hidden', 'active'], :group => :#{parent}_id)")
    end
  end
  
  # Set the contents of the flash message from a controller Usage: notify
  # :warning, "This is the message" Sets the flash of type 'warning' to "This is
  # the message"
  def notify(type, message)
    flash[type] = message
    logger.error("ERROR: #{message}") if type == :error
  end
  
  def set_time_zone
    Time.zone = current_user.prefs.time_zone if logged_in?
  end
  
end