Commit b1bd3f1252eb529030f2295e4c2a991158894b64

Authored by Dmitriy Zaporozhets
1 parent 124a5e27

fix tests. added jquery.timeago.js

app/assets/javascripts/jquery.timeago.js 0 → 100644
@@ -0,0 +1,181 @@ @@ -0,0 +1,181 @@
  1 +/**
  2 + * Timeago is a jQuery plugin that makes it easy to support automatically
  3 + * updating fuzzy timestamps (e.g. "4 minutes ago" or "about 1 day ago").
  4 + *
  5 + * @name timeago
  6 + * @version 1.1.0
  7 + * @requires jQuery v1.2.3+
  8 + * @author Ryan McGeary
  9 + * @license MIT License - http://www.opensource.org/licenses/mit-license.php
  10 + *
  11 + * For usage and examples, visit:
  12 + * http://timeago.yarp.com/
  13 + *
  14 + * Copyright (c) 2008-2013, Ryan McGeary (ryan -[at]- mcgeary [*dot*] org)
  15 + */
  16 +
  17 +(function (factory) {
  18 + if (typeof define === 'function' && define.amd) {
  19 + // AMD. Register as an anonymous module.
  20 + define(['jquery'], factory);
  21 + } else {
  22 + // Browser globals
  23 + factory(jQuery);
  24 + }
  25 +}(function ($) {
  26 + $.timeago = function(timestamp) {
  27 + if (timestamp instanceof Date) {
  28 + return inWords(timestamp);
  29 + } else if (typeof timestamp === "string") {
  30 + return inWords($.timeago.parse(timestamp));
  31 + } else if (typeof timestamp === "number") {
  32 + return inWords(new Date(timestamp));
  33 + } else {
  34 + return inWords($.timeago.datetime(timestamp));
  35 + }
  36 + };
  37 + var $t = $.timeago;
  38 +
  39 + $.extend($.timeago, {
  40 + settings: {
  41 + refreshMillis: 60000,
  42 + allowFuture: false,
  43 + strings: {
  44 + prefixAgo: null,
  45 + prefixFromNow: null,
  46 + suffixAgo: "ago",
  47 + suffixFromNow: "from now",
  48 + seconds: "less than a minute",
  49 + minute: "about a minute",
  50 + minutes: "%d minutes",
  51 + hour: "about an hour",
  52 + hours: "about %d hours",
  53 + day: "a day",
  54 + days: "%d days",
  55 + month: "about a month",
  56 + months: "%d months",
  57 + year: "about a year",
  58 + years: "%d years",
  59 + wordSeparator: " ",
  60 + numbers: []
  61 + }
  62 + },
  63 + inWords: function(distanceMillis) {
  64 + var $l = this.settings.strings;
  65 + var prefix = $l.prefixAgo;
  66 + var suffix = $l.suffixAgo;
  67 + if (this.settings.allowFuture) {
  68 + if (distanceMillis < 0) {
  69 + prefix = $l.prefixFromNow;
  70 + suffix = $l.suffixFromNow;
  71 + }
  72 + }
  73 +
  74 + var seconds = Math.abs(distanceMillis) / 1000;
  75 + var minutes = seconds / 60;
  76 + var hours = minutes / 60;
  77 + var days = hours / 24;
  78 + var years = days / 365;
  79 +
  80 + function substitute(stringOrFunction, number) {
  81 + var string = $.isFunction(stringOrFunction) ? stringOrFunction(number, distanceMillis) : stringOrFunction;
  82 + var value = ($l.numbers && $l.numbers[number]) || number;
  83 + return string.replace(/%d/i, value);
  84 + }
  85 +
  86 + var words = seconds < 45 && substitute($l.seconds, Math.round(seconds)) ||
  87 + seconds < 90 && substitute($l.minute, 1) ||
  88 + minutes < 45 && substitute($l.minutes, Math.round(minutes)) ||
  89 + minutes < 90 && substitute($l.hour, 1) ||
  90 + hours < 24 && substitute($l.hours, Math.round(hours)) ||
  91 + hours < 42 && substitute($l.day, 1) ||
  92 + days < 30 && substitute($l.days, Math.round(days)) ||
  93 + days < 45 && substitute($l.month, 1) ||
  94 + days < 365 && substitute($l.months, Math.round(days / 30)) ||
  95 + years < 1.5 && substitute($l.year, 1) ||
  96 + substitute($l.years, Math.round(years));
  97 +
  98 + var separator = $l.wordSeparator || "";
  99 + if ($l.wordSeparator === undefined) { separator = " "; }
  100 + return $.trim([prefix, words, suffix].join(separator));
  101 + },
  102 + parse: function(iso8601) {
  103 + var s = $.trim(iso8601);
  104 + s = s.replace(/\.\d+/,""); // remove milliseconds
  105 + s = s.replace(/-/,"/").replace(/-/,"/");
  106 + s = s.replace(/T/," ").replace(/Z/," UTC");
  107 + s = s.replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2"); // -04:00 -> -0400
  108 + return new Date(s);
  109 + },
  110 + datetime: function(elem) {
  111 + var iso8601 = $t.isTime(elem) ? $(elem).attr("datetime") : $(elem).attr("title");
  112 + return $t.parse(iso8601);
  113 + },
  114 + isTime: function(elem) {
  115 + // jQuery's `is()` doesn't play well with HTML5 in IE
  116 + return $(elem).get(0).tagName.toLowerCase() === "time"; // $(elem).is("time");
  117 + }
  118 + });
  119 +
  120 + // functions that can be called via $(el).timeago('action')
  121 + // init is default when no action is given
  122 + // functions are called with context of a single element
  123 + var functions = {
  124 + init: function(){
  125 + var refresh_el = $.proxy(refresh, this);
  126 + refresh_el();
  127 + var $s = $t.settings;
  128 + if ($s.refreshMillis > 0) {
  129 + setInterval(refresh_el, $s.refreshMillis);
  130 + }
  131 + },
  132 + update: function(time){
  133 + $(this).data('timeago', { datetime: $t.parse(time) });
  134 + refresh.apply(this);
  135 + }
  136 + };
  137 +
  138 + $.fn.timeago = function(action, options) {
  139 + var fn = action ? functions[action] : functions.init;
  140 + if(!fn){
  141 + throw new Error("Unknown function name '"+ action +"' for timeago");
  142 + }
  143 + // each over objects here and call the requested function
  144 + this.each(function(){
  145 + fn.call(this, options);
  146 + });
  147 + return this;
  148 + };
  149 +
  150 + function refresh() {
  151 + var data = prepareData(this);
  152 + if (!isNaN(data.datetime)) {
  153 + $(this).text(inWords(data.datetime));
  154 + }
  155 + return this;
  156 + }
  157 +
  158 + function prepareData(element) {
  159 + element = $(element);
  160 + if (!element.data("timeago")) {
  161 + element.data("timeago", { datetime: $t.datetime(element) });
  162 + var text = $.trim(element.text());
  163 + if (text.length > 0 && !($t.isTime(element) && element.attr("title"))) {
  164 + element.attr("title", text);
  165 + }
  166 + }
  167 + return element.data("timeago");
  168 + }
  169 +
  170 + function inWords(date) {
  171 + return $t.inWords(distance(date));
  172 + }
  173 +
  174 + function distance(date) {
  175 + return (new Date().getTime() - date.getTime());
  176 + }
  177 +
  178 + // fix for IE6 suckage
  179 + document.createElement("abbr");
  180 + document.createElement("time");
  181 +}));
app/assets/javascripts/main.js.coffee
@@ -53,6 +53,8 @@ $ -&gt; @@ -53,6 +53,8 @@ $ -&gt;
53 $('.trigger-submit').on 'change', -> 53 $('.trigger-submit').on 'change', ->
54 $(@).parents('form').submit() 54 $(@).parents('form').submit()
55 55
  56 + $("abbr.timeago").timeago()
  57 +
56 # Flash 58 # Flash
57 if (flash = $(".flash-container")).length > 0 59 if (flash = $(".flash-container")).length > 0
58 flash.click -> $(@).fadeOut() 60 flash.click -> $(@).fadeOut()
app/assets/javascripts/wall.js.coffee
@@ -30,24 +30,13 @@ @@ -30,24 +30,13 @@
30 Wall.note_ids.push(note.id) 30 Wall.note_ids.push(note.id)
31 Wall.renderNote(note) 31 Wall.renderNote(note)
32 Wall.scrollDown() 32 Wall.scrollDown()
  33 + $("abbr.timeago").timeago()
33 34
34 complete: -> 35 complete: ->
35 $('.js-notes-busy').removeClass("loading") 36 $('.js-notes-busy').removeClass("loading")
36 beforeSend: -> 37 beforeSend: ->
37 $('.js-notes-busy').addClass("loading") 38 $('.js-notes-busy').addClass("loading")
38 39
39 - renderNote: (note) ->  
40 - author = '<strong class="wall-author">' + note.author.name + '</strong>'  
41 - body = '<span class="wall-text">' + note.body + '</span>'  
42 - file = ''  
43 -  
44 - if note.attachment  
45 - file = '<span class="wall-file"><a href="/files/note/' + note.id + '/' + note.attachment + '">' + note.attachment + '</a></span>'  
46 -  
47 - html = '<li>' + author + body + file + '</li>'  
48 -  
49 - $('ul.notes').append(html)  
50 -  
51 initRefresh: -> 40 initRefresh: ->
52 setInterval("Wall.refresh()", 10000) 41 setInterval("Wall.refresh()", 10000)
53 42
@@ -59,14 +48,9 @@ @@ -59,14 +48,9 @@
59 $('body').scrollTop(notes.height()) 48 $('body').scrollTop(notes.height())
60 49
61 initForm: -> 50 initForm: ->
62 - form = $('.new_note') 51 + form = $('.wall-note-form')
63 form.find("#target_type").val('wall') 52 form.find("#target_type").val('wall')
64 53
65 - # remove unnecessary fields and buttons  
66 - form.find("#note_line_code").remove()  
67 - form.find(".js-close-discussion-note-form").remove()  
68 - form.find('.js-notify-commit-author').remove()  
69 -  
70 form.on 'ajax:success', -> 54 form.on 'ajax:success', ->
71 Wall.refresh() 55 Wall.refresh()
72 form.find(".js-note-text").val("").trigger("input") 56 form.find(".js-note-text").val("").trigger("input")
@@ -83,3 +67,17 @@ @@ -83,3 +67,17 @@
83 form.find(".js-attachment-filename").text(filename) 67 form.find(".js-attachment-filename").text(filename)
84 68
85 form.show() 69 form.show()
  70 +
  71 + renderNote: (note) ->
  72 + author = '<strong class="wall-author">' + note.author.name + '</strong>'
  73 + body = '<span class="wall-text">' + note.body + '</span>'
  74 + file = ''
  75 + time = '<abbr class="timeago" title="' + note.created_at + '">' + note.created_at + '</time>'
  76 +
  77 + if note.attachment
  78 + file = '<span class="wall-file"><a href="/files/note/' + note.id + '/' + note.attachment + '">' + note.attachment + '</a></span>'
  79 +
  80 + html = '<li>' + author + body + file + time + '</li>'
  81 +
  82 + $('ul.notes').append(html)
  83 +
app/assets/stylesheets/sections/wall.scss
1 .wall-page { 1 .wall-page {
2 - .new_note { 2 + .wall-note-form {
3 @extend .span12; 3 @extend .span12;
4 4
5 margin: 0; 5 margin: 0;
@@ -23,7 +23,14 @@ @@ -23,7 +23,14 @@
23 } 23 }
24 24
25 .wall-file { 25 .wall-file {
  26 + margin-left: 8px;
  27 + background: #EEE;
  28 + }
  29 +
  30 + abbr {
26 float: right; 31 float: right;
  32 + color: #AAA;
  33 + border: none;
27 } 34 }
28 } 35 }
29 } 36 }
app/views/events/event/_note.html.haml
@@ -11,7 +11,7 @@ @@ -11,7 +11,7 @@
11 #{event.note_target_type} ##{truncate event.note_target_id} 11 #{event.note_target_type} ##{truncate event.note_target_id}
12 12
13 - elsif event.wall_note? 13 - elsif event.wall_note?
14 - = link_to 'wall', wall_project_path(event.project) 14 + = link_to 'wall', project_wall_path(event.project)
15 - else 15 - else
16 %strong (deleted) 16 %strong (deleted)
17 at 17 at
app/views/notify/note_wall_email.html.haml
1 %p 1 %p
2 New message on 2 New message on
3 - = link_to "Project Wall", wall_project_url(@note.project, anchor: "note_#{@note.id}") 3 + = link_to "Project Wall", project_wall_url(@note.project, anchor: "note_#{@note.id}")
4 4
5 = render 'note_message' 5 = render 'note_message'
app/views/notify/note_wall_email.text.erb
1 New message on the project wall <%= @note.project %> 1 New message on the project wall <%= @note.project %>
2 2
3 -<%= url_for(wall_project_url(@note.project, anchor: "note_#{@note.id}")) %> 3 +<%= url_for(project_wall_url(@note.project, anchor: "note_#{@note.id}")) %>
4 4
5 5
6 <%= @note.author_name %> 6 <%= @note.author_name %>
app/views/walls/show.html.haml
@@ -2,9 +2,30 @@ @@ -2,9 +2,30 @@
2 %ul.well-list.notes 2 %ul.well-list.notes
3 .notes-busy.js-notes-busy 3 .notes-busy.js-notes-busy
4 4
5 - .js-main-target-form  
6 - if can? current_user, :write_note, @project 5 - if can? current_user, :write_note, @project
7 - = render "notes/form" 6 + .note-form-holder
  7 + = form_for [@project, @note], remote: true, html: { multipart: true, id: nil, class: "new_note wall-note-form" } do |f|
  8 + = note_target_fields
  9 + .note_text_and_preview
  10 + = f.text_area :note, size: 255, class: 'note_text js-note-text js-gfm-input turn-on'
  11 + .note-form-actions
  12 + .buttons
  13 + = f.submit 'Add Comment', class: "btn comment-btn grouped js-comment-button"
  14 +
  15 + .note-form-option
  16 + = label_tag :notify do
  17 + = check_box_tag :notify, 1, false
  18 + %span.light Notify team via email
  19 +
  20 + .note-form-option
  21 + %a.choose-btn.btn.btn-small.js-choose-note-attachment-button
  22 + %i.icon-paper-clip
  23 + %span Choose File ...
  24 + &nbsp;
  25 + %span.file_name.js-attachment-filename File name...
  26 + = f.file_field :attachment, class: "js-note-attachment-input hide"
  27 +
  28 + .clearfix
8 29
9 :javascript 30 :javascript
10 $(function(){ 31 $(function(){
features/steps/shared/paths.rb
@@ -161,7 +161,7 @@ module SharedPaths @@ -161,7 +161,7 @@ module SharedPaths
161 end 161 end
162 162
163 Given "I visit my project's wall page" do 163 Given "I visit my project's wall page" do
164 - visit wall_project_path(@project) 164 + visit project_wall_path(@project)
165 end 165 end
166 166
167 Given "I visit my project's wiki page" do 167 Given "I visit my project's wiki page" do
spec/features/gitlab_flavored_markdown_spec.rb
@@ -198,7 +198,7 @@ describe &quot;Gitlab Flavored Markdown&quot; do @@ -198,7 +198,7 @@ describe &quot;Gitlab Flavored Markdown&quot; do
198 end 198 end
199 199
200 it "should render in projects#wall", js: true do 200 it "should render in projects#wall", js: true do
201 - visit wall_project_path(project) 201 + visit project_wall_path(project)
202 within ".new_note.js-main-target-form" do 202 within ".new_note.js-main-target-form" do
203 fill_in "note_note", with: "see ##{issue.id}" 203 fill_in "note_note", with: "see ##{issue.id}"
204 click_button "Add Comment" 204 click_button "Add Comment"
spec/features/notes_on_merge_requests_spec.rb
@@ -22,7 +22,7 @@ describe &quot;On a merge request&quot;, js: true do @@ -22,7 +22,7 @@ describe &quot;On a merge request&quot;, js: true do
22 it { within(".js-main-target-form") { should_not have_link("Cancel") } } 22 it { within(".js-main-target-form") { should_not have_link("Cancel") } }
23 23
24 # notifiactions 24 # notifiactions
25 - it { within(".js-main-target-form") { should have_checked_field("Notify team via email") } } 25 + it { within(".js-main-target-form") { should have_unchecked_field("Notify team via email") } }
26 it { within(".js-main-target-form") { should_not have_checked_field("Notify commit author") } } 26 it { within(".js-main-target-form") { should_not have_checked_field("Notify commit author") } }
27 it { within(".js-main-target-form") { should_not have_unchecked_field("Notify commit author") } } 27 it { within(".js-main-target-form") { should_not have_unchecked_field("Notify commit author") } }
28 28
@@ -127,7 +127,7 @@ describe &quot;On a merge request diff&quot;, js: true, focus: true do @@ -127,7 +127,7 @@ describe &quot;On a merge request diff&quot;, js: true, focus: true do
127 it { should have_css(".js-close-discussion-note-form", text: "Cancel") } 127 it { should have_css(".js-close-discussion-note-form", text: "Cancel") }
128 128
129 # notification options 129 # notification options
130 - it { should have_checked_field("Notify team via email") } 130 + it { should have_unchecked_field("Notify team via email") }
131 131
132 it "shouldn't add a second form for same row" do 132 it "shouldn't add a second form for same row" do
133 find("#4735dfc552ad7bf15ca468adc3cad9d05b624490_185_185.line_holder .js-add-diff-note-button").trigger("click") 133 find("#4735dfc552ad7bf15ca468adc3cad9d05b624490_185_185.line_holder .js-add-diff-note-button").trigger("click")
spec/features/notes_on_wall_spec.rb
@@ -2,84 +2,40 @@ require &#39;spec_helper&#39; @@ -2,84 +2,40 @@ require &#39;spec_helper&#39;
2 2
3 describe "On the project wall", js: true do 3 describe "On the project wall", js: true do
4 let!(:project) { create(:project) } 4 let!(:project) { create(:project) }
5 - let!(:commit) { project.repository.commit("bcf03b5de6c33f3869ef70d68cf06e679d1d7f9a") }  
6 5
7 before do 6 before do
8 login_as :user 7 login_as :user
9 project.team << [@user, :master] 8 project.team << [@user, :master]
10 - visit wall_project_path(project) 9 + visit project_wall_path(project)
11 end 10 end
12 11
13 subject { page } 12 subject { page }
14 13
15 describe "the note form" do 14 describe "the note form" do
16 - # main target form creation  
17 - it { should have_css(".js-main-target-form", visible: true, count: 1) }  
18 -  
19 - # button initalization  
20 - it { find(".js-main-target-form input[type=submit]").value.should == "Add Comment" }  
21 - it { within(".js-main-target-form") { should_not have_link("Cancel") } }  
22 -  
23 - # notifiactions  
24 - it { within(".js-main-target-form") { should have_checked_field("Notify team via email") } }  
25 - it { within(".js-main-target-form") { should_not have_checked_field("Notify commit author") } }  
26 - it { within(".js-main-target-form") { should_not have_unchecked_field("Notify commit author") } }  
27 -  
28 - describe "without text" do  
29 - it { within(".js-main-target-form") { should have_css(".js-note-preview-button", visible: false) } }  
30 - end 15 + it { should have_css(".wall-note-form", visible: true, count: 1) }
  16 + it { find(".wall-note-form input[type=submit]").value.should == "Add Comment" }
  17 + it { within(".wall-note-form") { should have_unchecked_field("Notify team via email") } }
31 18
32 describe "with text" do 19 describe "with text" do
33 before do 20 before do
34 - within(".js-main-target-form") do 21 + within(".wall-note-form") do
35 fill_in "note[note]", with: "This is awesome" 22 fill_in "note[note]", with: "This is awesome"
36 end 23 end
37 end 24 end
38 25
39 - it { within(".js-main-target-form") { should_not have_css(".js-comment-button[disabled]") } }  
40 -  
41 - it { within(".js-main-target-form") { should have_css(".js-note-preview-button", visible: true) } }  
42 - end  
43 -  
44 - describe "with preview" do  
45 - before do  
46 - within(".js-main-target-form") do  
47 - fill_in "note[note]", with: "This is awesome"  
48 - find(".js-note-preview-button").trigger("click")  
49 - end  
50 - end  
51 -  
52 - it { within(".js-main-target-form") { should have_css(".js-note-preview", text: "This is awesome", visible: true) } }  
53 -  
54 - it { within(".js-main-target-form") { should have_css(".js-note-preview-button", visible: false) } }  
55 - it { within(".js-main-target-form") { should have_css(".js-note-edit-button", visible: true) } } 26 + it { within(".wall-note-form") { should_not have_css(".js-comment-button[disabled]") } }
56 end 27 end
57 end 28 end
58 29
59 describe "when posting a note" do 30 describe "when posting a note" do
60 before do 31 before do
61 - within(".js-main-target-form") do 32 + within(".wall-note-form") do
62 fill_in "note[note]", with: "This is awsome!" 33 fill_in "note[note]", with: "This is awsome!"
63 - find(".js-note-preview-button").trigger("click")  
64 click_button "Add Comment" 34 click_button "Add Comment"
65 end 35 end
66 end 36 end
67 37
68 - # note added  
69 it { should have_content("This is awsome!") } 38 it { should have_content("This is awsome!") }
70 -  
71 - # reset form  
72 - it { within(".js-main-target-form") { should have_no_field("note[note]", with: "This is awesome!") } }  
73 -  
74 - # return from preview  
75 - it { within(".js-main-target-form") { should have_css(".js-note-preview", visible: false) } }  
76 - it { within(".js-main-target-form") { should have_css(".js-note-text", visible: true) } }  
77 -  
78 -  
79 - it "should be removable" do  
80 - find(".js-note-delete").trigger("click")  
81 -  
82 - should_not have_css(".note")  
83 - end 39 + it { within(".wall-note-form") { should have_no_field("note[note]", with: "This is awesome!") } }
84 end 40 end
85 end 41 end
spec/features/security/project_access_spec.rb
@@ -95,7 +95,7 @@ describe &quot;Application access&quot; do @@ -95,7 +95,7 @@ describe &quot;Application access&quot; do
95 end 95 end
96 96
97 describe "GET /project_code/wall" do 97 describe "GET /project_code/wall" do
98 - subject { wall_project_path(project) } 98 + subject { project_wall_path(project) }
99 99
100 it { should be_allowed_for master } 100 it { should be_allowed_for master }
101 it { should be_allowed_for reporter } 101 it { should be_allowed_for reporter }
spec/mailers/notify_spec.rb
@@ -239,7 +239,7 @@ describe Notify do @@ -239,7 +239,7 @@ describe Notify do
239 end 239 end
240 240
241 describe 'on a project wall' do 241 describe 'on a project wall' do
242 - let(:note_on_the_wall_path) { wall_project_path(project, anchor: "note_#{note.id}") } 242 + let(:note_on_the_wall_path) { project_wall_path(project, anchor: "note_#{note.id}") }
243 243
244 subject { Notify.note_wall_email(recipient.id, note.id) } 244 subject { Notify.note_wall_email(recipient.id, note.id) }
245 245