Revision 1297:0a574315af3e .svn/pristine/83

View differences:

.svn/pristine/83/83300e07f9013be5c854520eee231d3e094963c6.svn-base
1
require 'SVG/Graph/Graph'
2

  
3
module SVG
4
  module Graph
5
    # === For creating SVG plots of scalar data
6
    # 
7
    # = Synopsis
8
    # 
9
    #   require 'SVG/Graph/Plot'
10
    # 
11
    #   # Data sets are x,y pairs
12
    #   # Note that multiple data sets can differ in length, and that the
13
    #   # data in the datasets needn't be in order; they will be ordered
14
    #   # by the plot along the X-axis.
15
    #   projection = [
16
    #     6, 11,    0, 5,   18, 7,   1, 11,   13, 9,   1, 2,   19, 0,   3, 13,
17
    #     7, 9 
18
    #   ]
19
    #   actual = [
20
    #     0, 18,    8, 15,    9, 4,   18, 14,   10, 2,   11, 6,  14, 12,   
21
    #     15, 6,   4, 17,   2, 12
22
    #   ]
23
    #   
24
    #   graph = SVG::Graph::Plot.new({
25
    #   	:height => 500,
26
    #    	:width => 300,
27
    #     :key => true,
28
    #     :scale_x_integers => true,
29
    #     :scale_y_integerrs => true,
30
    #   })
31
    #   
32
    #   graph.add_data({
33
    #   	:data => projection
34
    # 	  :title => 'Projected',
35
    #   })
36
    # 
37
    #   graph.add_data({
38
    #   	:data => actual,
39
    # 	  :title => 'Actual',
40
    #   })
41
    #   
42
    #   print graph.burn()
43
    # 
44
    # = Description
45
    # 
46
    # Produces a graph of scalar data.
47
    # 
48
    # This object aims to allow you to easily create high quality
49
    # SVG[http://www.w3c.org/tr/svg] scalar plots. You can either use the
50
    # default style sheet or supply your own. Either way there are many options
51
    # which can be configured to give you control over how the graph is
52
    # generated - with or without a key, data elements at each point, title,
53
    # subtitle etc.
54
    #
55
    # = Examples
56
    # 
57
    # http://www.germane-software/repositories/public/SVG/test/plot.rb
58
    # 
59
    # = Notes
60
    # 
61
    # The default stylesheet handles upto 10 data sets, if you
62
    # use more you must create your own stylesheet and add the
63
    # additional settings for the extra data sets. You will know
64
    # if you go over 10 data sets as they will have no style and
65
    # be in black.
66
    #
67
    # Unlike the other types of charts, data sets must contain x,y pairs:
68
    #
69
    #   [ 1, 2 ]    # A data set with 1 point: (1,2)
70
    #   [ 1,2, 5,6] # A data set with 2 points: (1,2) and (5,6)  
71
    # 
72
    # = See also
73
    # 
74
    # * SVG::Graph::Graph
75
    # * SVG::Graph::BarHorizontal
76
    # * SVG::Graph::Bar
77
    # * SVG::Graph::Line
78
    # * SVG::Graph::Pie
79
    # * SVG::Graph::TimeSeries
80
    #
81
    # == Author
82
    #
83
    # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
84
    #
85
    # Copyright 2004 Sean E. Russell
86
    # This software is available under the Ruby license[LICENSE.txt]
87
    #
88
    class Plot < Graph
89

  
90
      # In addition to the defaults set by Graph::initialize, sets
91
      # [show_data_values] true
92
      # [show_data_points] true
93
      # [area_fill] false
94
      # [stacked] false
95
      def set_defaults
96
        init_with(
97
                  :show_data_values  => true,
98
                  :show_data_points  => true,
99
                  :area_fill         => false,
100
                  :stacked           => false
101
                 )
102
                 self.top_align = self.right_align = self.top_font = self.right_font = 1
103
      end
104

  
105
      # Determines the scaling for the X axis divisions.
106
      #
107
      #   graph.scale_x_divisions = 2
108
      #
109
      # would cause the graph to attempt to generate labels stepped by 2; EG:
110
      # 0,2,4,6,8...
111
      attr_accessor :scale_x_divisions
112
      # Determines the scaling for the Y axis divisions.
113
      #
114
      #   graph.scale_y_divisions = 0.5
115
      #
116
      # would cause the graph to attempt to generate labels stepped by 0.5; EG:
117
      # 0, 0.5, 1, 1.5, 2, ...
118
      attr_accessor :scale_y_divisions 
119
      # Make the X axis labels integers
120
      attr_accessor :scale_x_integers 
121
      # Make the Y axis labels integers
122
      attr_accessor :scale_y_integers 
123
      # Fill the area under the line
124
      attr_accessor :area_fill 
125
      # Show a small circle on the graph where the line
126
      # goes from one point to the next.
127
      attr_accessor :show_data_points
128
      # Set the minimum value of the X axis
129
      attr_accessor :min_x_value 
130
      # Set the minimum value of the Y axis
131
      attr_accessor :min_y_value
132

  
133

  
134
      # Adds data to the plot.  The data must be in X,Y pairs; EG
135
      #   [ 1, 2 ]    # A data set with 1 point: (1,2)
136
      #   [ 1,2, 5,6] # A data set with 2 points: (1,2) and (5,6)  
137
      def add_data data
138
        @data = [] unless @data
139

  
140
        raise "No data provided by #{conf.inspect}" unless data[:data] and
141
        data[:data].kind_of? Array
142
        raise "Data supplied must be x,y pairs!  "+
143
          "The data provided contained an odd set of "+
144
          "data points" unless data[:data].length % 2 == 0
145
        return if data[:data].length == 0
146

  
147
        x = []
148
        y = []
149
        data[:data].each_index {|i|
150
          (i%2 == 0 ? x : y) << data[:data][i]
151
        }
152
        sort( x, y )
153
        data[:data] = [x,y]
154
        @data << data
155
      end
156

  
157

  
158
      protected
159

  
160
      def keys
161
        @data.collect{ |x| x[:title] }
162
      end
163

  
164
      def calculate_left_margin
165
        super
166
        label_left = get_x_labels[0].to_s.length / 2 * font_size * 0.6
167
        @border_left = label_left if label_left > @border_left
168
      end
169

  
170
      def calculate_right_margin
171
        super
172
        label_right = get_x_labels[-1].to_s.length / 2 * font_size * 0.6
173
        @border_right = label_right if label_right > @border_right
174
      end
175

  
176

  
177
      X = 0
178
      Y = 1
179
      def x_range
180
        max_value = @data.collect{|x| x[:data][X][-1] }.max
181
        min_value = @data.collect{|x| x[:data][X][0] }.min
182
        min_value = min_value<min_x_value ? min_value : min_x_value if min_x_value
183

  
184
        range = max_value - min_value
185
        right_pad = range == 0 ? 10 : range / 20.0
186
        scale_range = (max_value + right_pad) - min_value
187

  
188
        scale_division = scale_x_divisions || (scale_range / 10.0)
189

  
190
        if scale_x_integers
191
          scale_division = scale_division < 1 ? 1 : scale_division.round
192
        end
193

  
194
        [min_value, max_value, scale_division]
195
      end
196

  
197
      def get_x_values
198
        min_value, max_value, scale_division = x_range
199
        rv = []
200
        min_value.step( max_value, scale_division ) {|v| rv << v}
201
        return rv
202
      end
203
      alias :get_x_labels :get_x_values
204

  
205
      def field_width
206
        values = get_x_values
207
        max = @data.collect{|x| x[:data][X][-1]}.max
208
        dx = (max - values[-1]).to_f / (values[-1] - values[-2])
209
        (@graph_width.to_f - font_size*2*right_font) /
210
          (values.length + dx - right_align)
211
      end
212

  
213

  
214
      def y_range
215
        max_value = @data.collect{|x| x[:data][Y].max }.max
216
        min_value = @data.collect{|x| x[:data][Y].min }.min
217
        min_value = min_value<min_y_value ? min_value : min_y_value if min_y_value
218

  
219
        range = max_value - min_value
220
        top_pad = range == 0 ? 10 : range / 20.0
221
        scale_range = (max_value + top_pad) - min_value
222

  
223
        scale_division = scale_y_divisions || (scale_range / 10.0)
224

  
225
        if scale_y_integers
226
          scale_division = scale_division < 1 ? 1 : scale_division.round
227
        end
228

  
229
        return [min_value, max_value, scale_division]
230
      end
231

  
232
      def get_y_values
233
        min_value, max_value, scale_division = y_range
234
        rv = []
235
        min_value.step( max_value, scale_division ) {|v| rv << v}
236
        return rv
237
      end
238
      alias :get_y_labels :get_y_values
239

  
240
      def field_height
241
        values = get_y_values
242
        max = @data.collect{|x| x[:data][Y].max }.max
243
        if values.length == 1
244
          dx = values[-1]
245
        else
246
          dx = (max - values[-1]).to_f / (values[-1] - values[-2])
247
        end
248
        (@graph_height.to_f - font_size*2*top_font) /
249
          (values.length + dx - top_align)
250
      end
251

  
252
      def draw_data
253
        line = 1
254

  
255
        x_min, x_max, x_div = x_range
256
        y_min, y_max, y_div = y_range
257
        x_step = (@graph_width.to_f - font_size*2) / (x_max-x_min)
258
        y_step = (@graph_height.to_f -  font_size*2) / (y_max-y_min)
259

  
260
        for data in @data
261
          x_points = data[:data][X]
262
          y_points = data[:data][Y]
263

  
264
          lpath = "L"
265
          x_start = 0
266
          y_start = 0
267
          x_points.each_index { |idx|
268
            x = (x_points[idx] -  x_min) * x_step
269
            y = @graph_height - (y_points[idx] -  y_min) * y_step
270
            x_start, y_start = x,y if idx == 0
271
            lpath << "#{x} #{y} "
272
          }
273

  
274
          if area_fill
275
            @graph.add_element( "path", {
276
              "d" => "M#{x_start} #@graph_height #{lpath} V#@graph_height Z",
277
              "class" => "fill#{line}"
278
            })
279
          end
280

  
281
          @graph.add_element( "path", {
282
            "d" => "M#{x_start} #{y_start} #{lpath}",
283
            "class" => "line#{line}"
284
          })
285

  
286
          if show_data_points || show_data_values
287
            x_points.each_index { |idx|
288
              x = (x_points[idx] -  x_min) * x_step
289
              y = @graph_height - (y_points[idx] -  y_min) * y_step
290
              if show_data_points
291
                @graph.add_element( "circle", {
292
                  "cx" => x.to_s,
293
                  "cy" => y.to_s,
294
                  "r" => "2.5",
295
                  "class" => "dataPoint#{line}"
296
                })
297
                add_popup(x, y, format( x_points[idx], y_points[idx] )) if add_popups
298
              end
299
              make_datapoint_text( x, y-6, y_points[idx] ) if show_data_values
300
            }
301
          end
302
          line += 1
303
        end
304
      end
305

  
306
      def format x, y
307
        "(#{(x * 100).to_i / 100}, #{(y * 100).to_i / 100})"
308
      end
309
      
310
      def get_css
311
        return <<EOL
312
/* default line styles */
313
.line1{
314
	fill: none;
315
	stroke: #ff0000;
316
	stroke-width: 1px;	
317
}
318
.line2{
319
	fill: none;
320
	stroke: #0000ff;
321
	stroke-width: 1px;	
322
}
323
.line3{
324
	fill: none;
325
	stroke: #00ff00;
326
	stroke-width: 1px;	
327
}
328
.line4{
329
	fill: none;
330
	stroke: #ffcc00;
331
	stroke-width: 1px;	
332
}
333
.line5{
334
	fill: none;
335
	stroke: #00ccff;
336
	stroke-width: 1px;	
337
}
338
.line6{
339
	fill: none;
340
	stroke: #ff00ff;
341
	stroke-width: 1px;	
342
}
343
.line7{
344
	fill: none;
345
	stroke: #00ffff;
346
	stroke-width: 1px;	
347
}
348
.line8{
349
	fill: none;
350
	stroke: #ffff00;
351
	stroke-width: 1px;	
352
}
353
.line9{
354
	fill: none;
355
	stroke: #ccc6666;
356
	stroke-width: 1px;	
357
}
358
.line10{
359
	fill: none;
360
	stroke: #663399;
361
	stroke-width: 1px;	
362
}
363
.line11{
364
	fill: none;
365
	stroke: #339900;
366
	stroke-width: 1px;	
367
}
368
.line12{
369
	fill: none;
370
	stroke: #9966FF;
371
	stroke-width: 1px;	
372
}
373
/* default fill styles */
374
.fill1{
375
	fill: #cc0000;
376
	fill-opacity: 0.2;
377
	stroke: none;
378
}
379
.fill2{
380
	fill: #0000cc;
381
	fill-opacity: 0.2;
382
	stroke: none;
383
}
384
.fill3{
385
	fill: #00cc00;
386
	fill-opacity: 0.2;
387
	stroke: none;
388
}
389
.fill4{
390
	fill: #ffcc00;
391
	fill-opacity: 0.2;
392
	stroke: none;
393
}
394
.fill5{
395
	fill: #00ccff;
396
	fill-opacity: 0.2;
397
	stroke: none;
398
}
399
.fill6{
400
	fill: #ff00ff;
401
	fill-opacity: 0.2;
402
	stroke: none;
403
}
404
.fill7{
405
	fill: #00ffff;
406
	fill-opacity: 0.2;
407
	stroke: none;
408
}
409
.fill8{
410
	fill: #ffff00;
411
	fill-opacity: 0.2;
412
	stroke: none;
413
}
414
.fill9{
415
	fill: #cc6666;
416
	fill-opacity: 0.2;
417
	stroke: none;
418
}
419
.fill10{
420
	fill: #663399;
421
	fill-opacity: 0.2;
422
	stroke: none;
423
}
424
.fill11{
425
	fill: #339900;
426
	fill-opacity: 0.2;
427
	stroke: none;
428
}
429
.fill12{
430
	fill: #9966FF;
431
	fill-opacity: 0.2;
432
	stroke: none;
433
}
434
/* default line styles */
435
.key1,.dataPoint1{
436
	fill: #ff0000;
437
	stroke: none;
438
	stroke-width: 1px;	
439
}
440
.key2,.dataPoint2{
441
	fill: #0000ff;
442
	stroke: none;
443
	stroke-width: 1px;	
444
}
445
.key3,.dataPoint3{
446
	fill: #00ff00;
447
	stroke: none;
448
	stroke-width: 1px;	
449
}
450
.key4,.dataPoint4{
451
	fill: #ffcc00;
452
	stroke: none;
453
	stroke-width: 1px;	
454
}
455
.key5,.dataPoint5{
456
	fill: #00ccff;
457
	stroke: none;
458
	stroke-width: 1px;	
459
}
460
.key6,.dataPoint6{
461
	fill: #ff00ff;
462
	stroke: none;
463
	stroke-width: 1px;	
464
}
465
.key7,.dataPoint7{
466
	fill: #00ffff;
467
	stroke: none;
468
	stroke-width: 1px;	
469
}
470
.key8,.dataPoint8{
471
	fill: #ffff00;
472
	stroke: none;
473
	stroke-width: 1px;	
474
}
475
.key9,.dataPoint9{
476
	fill: #cc6666;
477
	stroke: none;
478
	stroke-width: 1px;	
479
}
480
.key10,.dataPoint10{
481
	fill: #663399;
482
	stroke: none;
483
	stroke-width: 1px;	
484
}
485
.key11,.dataPoint11{
486
	fill: #339900;
487
	stroke: none;
488
	stroke-width: 1px;	
489
}
490
.key12,.dataPoint12{
491
	fill: #9966FF;
492
	stroke: none;
493
	stroke-width: 1px;	
494
}
495
EOL
496
      end
497

  
498
    end
499
  end
500
end
.svn/pristine/83/8336e9f46dcb5eb005a350a17c0dd1aa44840422.svn-base
1
<fieldset id="date-range" class="collapsible">
2
<legend onclick="toggleFieldset(this);"><%= l(:label_date_range) %></legend>
3
<div>
4
<p>
5
<%= label_tag "period_type_list", l(:description_date_range_list), :class => "hidden-for-sighted" %>
6
<%= radio_button_tag 'period_type', '1', !@free_period, :onclick => '$("#from,#to").attr("disabled", true);$("#period").removeAttr("disabled");', :id => "period_type_list"%>
7
<%= select_tag 'period', options_for_period_select(params[:period]),
8
                         :onchange => 'this.form.submit();',
9
                         :onfocus => '$("#period_type_1").attr("checked", true);',
10
                         :disabled => @free_period %>
11
</p>
12
<p>
13
<%= label_tag "period_type_interval", l(:description_date_range_interval), :class => "hidden-for-sighted" %>
14
<%= radio_button_tag 'period_type', '2', @free_period, :onclick => '$("#from,#to").removeAttr("disabled");$("#period").attr("disabled", true);', :id => "period_type_interval" %>
15
<%= l(:label_date_from_to,
16
        :start => ((label_tag "from", l(:description_date_from), :class => "hidden-for-sighted") + 
17
            text_field_tag('from', @from, :size => 10, :disabled => !@free_period) + calendar_for('from')),
18
        :end => ((label_tag "to", l(:description_date_to), :class => "hidden-for-sighted") +
19
            text_field_tag('to', @to, :size => 10, :disabled => !@free_period) + calendar_for('to'))).html_safe %>
20
</p>
21
</div>
22
</fieldset>
23
<p class="buttons">
24
  <%= link_to_function l(:button_apply), '$("#query_form").submit()', :class => 'icon icon-checked' %>
25
  <%= link_to l(:button_clear), {:controller => controller_name, :action => action_name, :project_id => @project, :issue_id => @issue}, :class => 'icon icon-reload' %>
26
</p>
27

  
28
<div class="tabs">
29
<% url_params = @free_period ? { :from => @from, :to => @to } : { :period => params[:period] } %>
30
<ul>
31
    <li><%= link_to(l(:label_details), url_params.merge({:controller => 'timelog', :action => 'index', :project_id => @project, :issue_id => @issue }),
32
                                       :class => (action_name == 'index' ? 'selected' : nil)) %></li>
33
    <li><%= link_to(l(:label_report), url_params.merge({:controller => 'timelog', :action => 'report', :project_id => @project, :issue_id => @issue}),
34
                                       :class => (action_name == 'report' ? 'selected' : nil)) %></li>
35
</ul>
36
</div>
37

  
38
<%= javascript_tag do %>
39
$('#from, #to').change(function(){
40
  $('#period_type_interval').attr('checked', true); $('#from,#to').removeAttr('disabled'); $('#period').attr('disabled', true);
41
});
42
<% end %>
.svn/pristine/83/83a431c356c51da0a7c44b91b4b4ad94435f65e3.svn-base
1
# Redmine - project management software
2
# Copyright (C) 2006-2012  Jean-Philippe Lang
3
#
4
# This program is free software; you can redistribute it and/or
5
# modify it under the terms of the GNU General Public License
6
# as published by the Free Software Foundation; either version 2
7
# of the License, or (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17

  
18
class MyController < ApplicationController
19
  before_filter :require_login
20

  
21
  helper :issues
22
  helper :users
23
  helper :custom_fields
24

  
25
  BLOCKS = { 'issuesassignedtome' => :label_assigned_to_me_issues,
26
             'issuesreportedbyme' => :label_reported_issues,
27
             'issueswatched' => :label_watched_issues,
28
             'news' => :label_news_latest,
29
             'calendar' => :label_calendar,
30
             'documents' => :label_document_plural,
31
             'timelog' => :label_spent_time
32
           }.merge(Redmine::Views::MyPage::Block.additional_blocks).freeze
33

  
34
  DEFAULT_LAYOUT = {  'left' => ['issuesassignedtome'],
35
                      'right' => ['issuesreportedbyme']
36
                   }.freeze
37

  
38
  def index
39
    page
40
    render :action => 'page'
41
  end
42

  
43
  # Show user's page
44
  def page
45
    @user = User.current
46
    @blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT
47
  end
48

  
49
  # Edit user's account
50
  def account
51
    @user = User.current
52
    @pref = @user.pref
53
    if request.post?
54
      @user.safe_attributes = params[:user]
55
      @user.pref.attributes = params[:pref]
56
      @user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
57
      if @user.save
58
        @user.pref.save
59
        @user.notified_project_ids = (@user.mail_notification == 'selected' ? params[:notified_project_ids] : [])
60
        set_language_if_valid @user.language
61
        flash[:notice] = l(:notice_account_updated)
62
        redirect_to :action => 'account'
63
        return
64
      end
65
    end
66
  end
67

  
68
  # Destroys user's account
69
  def destroy
70
    @user = User.current
71
    unless @user.own_account_deletable?
72
      redirect_to :action => 'account'
73
      return
74
    end
75

  
76
    if request.post? && params[:confirm]
77
      @user.destroy
78
      if @user.destroyed?
79
        logout_user
80
        flash[:notice] = l(:notice_account_deleted)
81
      end
82
      redirect_to home_path
83
    end
84
  end
85

  
86
  # Manage user's password
87
  def password
88
    @user = User.current
89
    unless @user.change_password_allowed?
90
      flash[:error] = l(:notice_can_t_change_password)
91
      redirect_to :action => 'account'
92
      return
93
    end
94
    if request.post?
95
      if @user.check_password?(params[:password])
96
        @user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation]
97
        if @user.save
98
          flash[:notice] = l(:notice_account_password_updated)
99
          redirect_to :action => 'account'
100
        end
101
      else
102
        flash[:error] = l(:notice_account_wrong_password)
103
      end
104
    end
105
  end
106

  
107
  # Create a new feeds key
108
  def reset_rss_key
109
    if request.post?
110
      if User.current.rss_token
111
        User.current.rss_token.destroy
112
        User.current.reload
113
      end
114
      User.current.rss_key
115
      flash[:notice] = l(:notice_feeds_access_key_reseted)
116
    end
117
    redirect_to :action => 'account'
118
  end
119

  
120
  # Create a new API key
121
  def reset_api_key
122
    if request.post?
123
      if User.current.api_token
124
        User.current.api_token.destroy
125
        User.current.reload
126
      end
127
      User.current.api_key
128
      flash[:notice] = l(:notice_api_access_key_reseted)
129
    end
130
    redirect_to :action => 'account'
131
  end
132

  
133
  # User's page layout configuration
134
  def page_layout
135
    @user = User.current
136
    @blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT.dup
137
    @block_options = []
138
    BLOCKS.each do |k, v|
139
      unless %w(top left right).detect {|f| (@blocks[f] ||= []).include?(k)}
140
        @block_options << [l("my.blocks.#{v}", :default => [v, v.to_s.humanize]), k.dasherize]
141
      end
142
    end
143
  end
144

  
145
  # Add a block to user's page
146
  # The block is added on top of the page
147
  # params[:block] : id of the block to add
148
  def add_block
149
    block = params[:block].to_s.underscore
150
    if block.present? && BLOCKS.key?(block)
151
      @user = User.current
152
      layout = @user.pref[:my_page_layout] || {}
153
      # remove if already present in a group
154
      %w(top left right).each {|f| (layout[f] ||= []).delete block }
155
      # add it on top
156
      layout['top'].unshift block
157
      @user.pref[:my_page_layout] = layout
158
      @user.pref.save
159
    end
160
    redirect_to :action => 'page_layout'
161
  end
162

  
163
  # Remove a block to user's page
164
  # params[:block] : id of the block to remove
165
  def remove_block
166
    block = params[:block].to_s.underscore
167
    @user = User.current
168
    # remove block in all groups
169
    layout = @user.pref[:my_page_layout] || {}
170
    %w(top left right).each {|f| (layout[f] ||= []).delete block }
171
    @user.pref[:my_page_layout] = layout
172
    @user.pref.save
173
    redirect_to :action => 'page_layout'
174
  end
175

  
176
  # Change blocks order on user's page
177
  # params[:group] : group to order (top, left or right)
178
  # params[:list-(top|left|right)] : array of block ids of the group
179
  def order_blocks
180
    group = params[:group]
181
    @user = User.current
182
    if group.is_a?(String)
183
      group_items = (params["blocks"] || []).collect(&:underscore)
184
      group_items.each {|s| s.sub!(/^block_/, '')}
185
      if group_items and group_items.is_a? Array
186
        layout = @user.pref[:my_page_layout] || {}
187
        # remove group blocks if they are presents in other groups
188
        %w(top left right).each {|f|
189
          layout[f] = (layout[f] || []) - group_items
190
        }
191
        layout[group] = group_items
192
        @user.pref[:my_page_layout] = layout
193
        @user.pref.save
194
      end
195
    end
196
    render :nothing => true
197
  end
198
end
.svn/pristine/83/83bb890c49d6486a31b5b46ae3ea92ba18f02e2b.svn-base
1
# Redmine - project management software
2
# Copyright (C) 2006-2012  Jean-Philippe Lang
3
#
4
# This program is free software; you can redistribute it and/or
5
# modify it under the terms of the GNU General Public License
6
# as published by the Free Software Foundation; either version 2
7
# of the License, or (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17

  
18
class Wiki < ActiveRecord::Base
19
  include Redmine::SafeAttributes
20
  belongs_to :project
21
  has_many :pages, :class_name => 'WikiPage', :dependent => :destroy, :order => 'title'
22
  has_many :redirects, :class_name => 'WikiRedirect', :dependent => :delete_all
23

  
24
  acts_as_watchable
25

  
26
  validates_presence_of :start_page
27
  validates_format_of :start_page, :with => /^[^,\.\/\?\;\|\:]*$/
28

  
29
  safe_attributes 'start_page'
30

  
31
  def visible?(user=User.current)
32
    !user.nil? && user.allowed_to?(:view_wiki_pages, project)
33
  end
34

  
35
  # Returns the wiki page that acts as the sidebar content
36
  # or nil if no such page exists
37
  def sidebar
38
    @sidebar ||= find_page('Sidebar', :with_redirect => false)
39
  end
40

  
41
  # find the page with the given title
42
  # if page doesn't exist, return a new page
43
  def find_or_new_page(title)
44
    title = start_page if title.blank?
45
    find_page(title) || WikiPage.new(:wiki => self, :title => Wiki.titleize(title))
46
  end
47

  
48
  # find the page with the given title
49
  def find_page(title, options = {})
50
    @page_found_with_redirect = false
51
    title = start_page if title.blank?
52
    title = Wiki.titleize(title)
53
    page = pages.first(:conditions => ["LOWER(title) = LOWER(?)", title])
54
    if !page && !(options[:with_redirect] == false)
55
      # search for a redirect
56
      redirect = redirects.first(:conditions => ["LOWER(title) = LOWER(?)", title])
57
      if redirect
58
        page = find_page(redirect.redirects_to, :with_redirect => false)
59
        @page_found_with_redirect = true
60
      end
61
    end
62
    page
63
  end
64

  
65
  # Returns true if the last page was found with a redirect
66
  def page_found_with_redirect?
67
    @page_found_with_redirect
68
  end
69

  
70
  # Finds a page by title
71
  # The given string can be of one of the forms: "title" or "project:title"
72
  # Examples:
73
  #   Wiki.find_page("bar", project => foo)
74
  #   Wiki.find_page("foo:bar")
75
  def self.find_page(title, options = {})
76
    project = options[:project]
77
    if title.to_s =~ %r{^([^\:]+)\:(.*)$}
78
      project_identifier, title = $1, $2
79
      project = Project.find_by_identifier(project_identifier) || Project.find_by_name(project_identifier)
80
    end
81
    if project && project.wiki
82
      page = project.wiki.find_page(title)
83
      if page && page.content
84
        page
85
      end
86
    end
87
  end
88

  
89
  # turn a string into a valid page title
90
  def self.titleize(title)
91
    # replace spaces with _ and remove unwanted caracters
92
    title = title.gsub(/\s+/, '_').delete(',./?;|:') if title
93
    # upcase the first letter
94
    title = (title.slice(0..0).upcase + (title.slice(1..-1) || '')) if title
95
    title
96
  end
97
end
.svn/pristine/83/83c8ea6b826f97412b09501824a3127336754f22.svn-base
1
# Redmine - project management software
2
# Copyright (C) 2006-2012  Jean-Philippe Lang
3
#
4
# This program is free software; you can redistribute it and/or
5
# modify it under the terms of the GNU General Public License
6
# as published by the Free Software Foundation; either version 2
7
# of the License, or (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17

  
18
require 'diff'
19
require 'enumerator'
20

  
21
class WikiPage < ActiveRecord::Base
22
  include Redmine::SafeAttributes
23

  
24
  belongs_to :wiki
25
  has_one :content, :class_name => 'WikiContent', :foreign_key => 'page_id', :dependent => :destroy
26
  acts_as_attachable :delete_permission => :delete_wiki_pages_attachments
27
  acts_as_tree :dependent => :nullify, :order => 'title'
28

  
29
  acts_as_watchable
30
  acts_as_event :title => Proc.new {|o| "#{l(:label_wiki)}: #{o.title}"},
31
                :description => :text,
32
                :datetime => :created_on,
33
                :url => Proc.new {|o| {:controller => 'wiki', :action => 'show', :project_id => o.wiki.project, :id => o.title}}
34

  
35
  acts_as_searchable :columns => ['title', "#{WikiContent.table_name}.text"],
36
                     :include => [{:wiki => :project}, :content],
37
                     :permission => :view_wiki_pages,
38
                     :project_key => "#{Wiki.table_name}.project_id"
39

  
40
  attr_accessor :redirect_existing_links
41

  
42
  validates_presence_of :title
43
  validates_format_of :title, :with => /^[^,\.\/\?\;\|\s]*$/
44
  validates_uniqueness_of :title, :scope => :wiki_id, :case_sensitive => false
45
  validates_associated :content
46

  
47
  validate :validate_parent_title
48
  before_destroy :remove_redirects
49
  before_save    :handle_redirects
50

  
51
  # eager load information about last updates, without loading text
52
  scope :with_updated_on, {
53
    :select => "#{WikiPage.table_name}.*, #{WikiContent.table_name}.updated_on, #{WikiContent.table_name}.version",
54
    :joins => "LEFT JOIN #{WikiContent.table_name} ON #{WikiContent.table_name}.page_id = #{WikiPage.table_name}.id"
55
  }
56

  
57
  # Wiki pages that are protected by default
58
  DEFAULT_PROTECTED_PAGES = %w(sidebar)
59

  
60
  safe_attributes 'parent_id', 'parent_title',
61
    :if => lambda {|page, user| page.new_record? || user.allowed_to?(:rename_wiki_pages, page.project)}
62

  
63
  def initialize(attributes=nil, *args)
64
    super
65
    if new_record? && DEFAULT_PROTECTED_PAGES.include?(title.to_s.downcase)
66
      self.protected = true
67
    end
68
  end
69

  
70
  def visible?(user=User.current)
71
    !user.nil? && user.allowed_to?(:view_wiki_pages, project)
72
  end
73

  
74
  def title=(value)
75
    value = Wiki.titleize(value)
76
    @previous_title = read_attribute(:title) if @previous_title.blank?
77
    write_attribute(:title, value)
78
  end
79

  
80
  def handle_redirects
81
    self.title = Wiki.titleize(title)
82
    # Manage redirects if the title has changed
83
    if !@previous_title.blank? && (@previous_title != title) && !new_record?
84
      # Update redirects that point to the old title
85
      wiki.redirects.find_all_by_redirects_to(@previous_title).each do |r|
86
        r.redirects_to = title
87
        r.title == r.redirects_to ? r.destroy : r.save
88
      end
89
      # Remove redirects for the new title
90
      wiki.redirects.find_all_by_title(title).each(&:destroy)
91
      # Create a redirect to the new title
92
      wiki.redirects << WikiRedirect.new(:title => @previous_title, :redirects_to => title) unless redirect_existing_links == "0"
93
      @previous_title = nil
94
    end
95
  end
96

  
97
  def remove_redirects
98
    # Remove redirects to this page
99
    wiki.redirects.find_all_by_redirects_to(title).each(&:destroy)
100
  end
101

  
102
  def pretty_title
103
    WikiPage.pretty_title(title)
104
  end
105

  
106
  def content_for_version(version=nil)
107
    result = content.versions.find_by_version(version.to_i) if version
108
    result ||= content
109
    result
110
  end
111

  
112
  def diff(version_to=nil, version_from=nil)
113
    version_to = version_to ? version_to.to_i : self.content.version
114
    content_to = content.versions.find_by_version(version_to)
115
    content_from = version_from ? content.versions.find_by_version(version_from.to_i) : content_to.try(:previous)
116
    return nil unless content_to && content_from
117

  
118
    if content_from.version > content_to.version
119
      content_to, content_from = content_from, content_to
120
    end
121

  
122
    (content_to && content_from) ? WikiDiff.new(content_to, content_from) : nil
123
  end
124

  
125
  def annotate(version=nil)
126
    version = version ? version.to_i : self.content.version
127
    c = content.versions.find_by_version(version)
128
    c ? WikiAnnotate.new(c) : nil
129
  end
130

  
131
  def self.pretty_title(str)
132
    (str && str.is_a?(String)) ? str.tr('_', ' ') : str
133
  end
134

  
135
  def project
136
    wiki.project
137
  end
138

  
139
  def text
140
    content.text if content
141
  end
142

  
143
  def updated_on
144
    unless @updated_on
145
      if time = read_attribute(:updated_on)
146
        # content updated_on was eager loaded with the page
147
        begin
148
          @updated_on = (self.class.default_timezone == :utc ? Time.parse(time.to_s).utc : Time.parse(time.to_s).localtime)
149
        rescue
150
        end
151
      else
152
        @updated_on = content && content.updated_on
153
      end
154
    end
155
    @updated_on
156
  end
157

  
158
  # Returns true if usr is allowed to edit the page, otherwise false
159
  def editable_by?(usr)
160
    !protected? || usr.allowed_to?(:protect_wiki_pages, wiki.project)
161
  end
162

  
163
  def attachments_deletable?(usr=User.current)
164
    editable_by?(usr) && super(usr)
165
  end
166

  
167
  def parent_title
168
    @parent_title || (self.parent && self.parent.pretty_title)
169
  end
170

  
171
  def parent_title=(t)
172
    @parent_title = t
173
    parent_page = t.blank? ? nil : self.wiki.find_page(t)
174
    self.parent = parent_page
175
  end
176

  
177
  # Saves the page and its content if text was changed
178
  def save_with_content
179
    ret = nil
180
    transaction do
181
      if new_record?
182
        # Rails automatically saves associated content
183
        ret = save
184
      else
185
        ret = save && (content.text_changed? ? content.save : true)
186
      end
187
      raise ActiveRecord::Rollback unless ret
188
    end
189
    ret
190
  end
191

  
192
  protected
193

  
194
  def validate_parent_title
195
    errors.add(:parent_title, :invalid) if !@parent_title.blank? && parent.nil?
196
    errors.add(:parent_title, :circular_dependency) if parent && (parent == self || parent.ancestors.include?(self))
197
    errors.add(:parent_title, :not_same_project) if parent && (parent.wiki_id != wiki_id)
198
  end
199
end
200

  
201
class WikiDiff < Redmine::Helpers::Diff
202
  attr_reader :content_to, :content_from
203

  
204
  def initialize(content_to, content_from)
205
    @content_to = content_to
206
    @content_from = content_from
207
    super(content_to.text, content_from.text)
208
  end
209
end
210

  
211
class WikiAnnotate
212
  attr_reader :lines, :content
213

  
214
  def initialize(content)
215
    @content = content
216
    current = content
217
    current_lines = current.text.split(/\r?\n/)
218
    @lines = current_lines.collect {|t| [nil, nil, t]}
219
    positions = []
220
    current_lines.size.times {|i| positions << i}
221
    while (current.previous)
222
      d = current.previous.text.split(/\r?\n/).diff(current.text.split(/\r?\n/)).diffs.flatten
223
      d.each_slice(3) do |s|
224
        sign, line = s[0], s[1]
225
        if sign == '+' && positions[line] && positions[line] != -1
226
          if @lines[positions[line]][0].nil?
227
            @lines[positions[line]][0] = current.version
228
            @lines[positions[line]][1] = current.author
229
          end
230
        end
231
      end
232
      d.each_slice(3) do |s|
233
        sign, line = s[0], s[1]
234
        if sign == '-'
235
          positions.insert(line, -1)
236
        else
237
          positions[line] = nil
238
        end
239
      end
240
      positions.compact!
241
      # Stop if every line is annotated
242
      break unless @lines.detect { |line| line[0].nil? }
243
      current = current.previous
244
    end
245
    @lines.each { |line|
246
      line[0] ||= current.version
247
      # if the last known version is > 1 (eg. history was cleared), we don't know the author
248
      line[1] ||= current.author if current.version == 1
249
    }
250
  end
251
end

Also available in: Unified diff