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 @@
  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 53 $('.trigger-submit').on 'change', ->
54 54 $(@).parents('form').submit()
55 55  
  56 + $("abbr.timeago").timeago()
  57 +
56 58 # Flash
57 59 if (flash = $(".flash-container")).length > 0
58 60 flash.click -> $(@).fadeOut()
... ...
app/assets/javascripts/wall.js.coffee
... ... @@ -30,24 +30,13 @@
30 30 Wall.note_ids.push(note.id)
31 31 Wall.renderNote(note)
32 32 Wall.scrollDown()
  33 + $("abbr.timeago").timeago()
33 34  
34 35 complete: ->
35 36 $('.js-notes-busy').removeClass("loading")
36 37 beforeSend: ->
37 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 40 initRefresh: ->
52 41 setInterval("Wall.refresh()", 10000)
53 42  
... ... @@ -59,14 +48,9 @@
59 48 $('body').scrollTop(notes.height())
60 49  
61 50 initForm: ->
62   - form = $('.new_note')
  51 + form = $('.wall-note-form')
63 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 54 form.on 'ajax:success', ->
71 55 Wall.refresh()
72 56 form.find(".js-note-text").val("").trigger("input")
... ... @@ -83,3 +67,17 @@
83 67 form.find(".js-attachment-filename").text(filename)
84 68  
85 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 1 .wall-page {
2   - .new_note {
  2 + .wall-note-form {
3 3 @extend .span12;
4 4  
5 5 margin: 0;
... ... @@ -23,7 +23,14 @@
23 23 }
24 24  
25 25 .wall-file {
  26 + margin-left: 8px;
  27 + background: #EEE;
  28 + }
  29 +
  30 + abbr {
26 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 11 #{event.note_target_type} ##{truncate event.note_target_id}
12 12  
13 13 - elsif event.wall_note?
14   - = link_to 'wall', wall_project_path(event.project)
  14 + = link_to 'wall', project_wall_path(event.project)
15 15 - else
16 16 %strong (deleted)
17 17 at
... ...
app/views/notify/note_wall_email.html.haml
1 1 %p
2 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 5 = render 'note_message'
... ...
app/views/notify/note_wall_email.text.erb
1 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 6 <%= @note.author_name %>
... ...
app/views/walls/show.html.haml
... ... @@ -2,9 +2,30 @@
2 2 %ul.well-list.notes
3 3 .notes-busy.js-notes-busy
4 4  
5   - .js-main-target-form
6 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 30 :javascript
10 31 $(function(){
... ...
features/steps/shared/paths.rb
... ... @@ -161,7 +161,7 @@ module SharedPaths
161 161 end
162 162  
163 163 Given "I visit my project's wall page" do
164   - visit wall_project_path(@project)
  164 + visit project_wall_path(@project)
165 165 end
166 166  
167 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 198 end
199 199  
200 200 it "should render in projects#wall", js: true do
201   - visit wall_project_path(project)
  201 + visit project_wall_path(project)
202 202 within ".new_note.js-main-target-form" do
203 203 fill_in "note_note", with: "see ##{issue.id}"
204 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 22 it { within(".js-main-target-form") { should_not have_link("Cancel") } }
23 23  
24 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 26 it { within(".js-main-target-form") { should_not have_checked_field("Notify commit author") } }
27 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 127 it { should have_css(".js-close-discussion-note-form", text: "Cancel") }
128 128  
129 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 132 it "shouldn't add a second form for same row" do
133 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 2  
3 3 describe "On the project wall", js: true do
4 4 let!(:project) { create(:project) }
5   - let!(:commit) { project.repository.commit("bcf03b5de6c33f3869ef70d68cf06e679d1d7f9a") }
6 5  
7 6 before do
8 7 login_as :user
9 8 project.team << [@user, :master]
10   - visit wall_project_path(project)
  9 + visit project_wall_path(project)
11 10 end
12 11  
13 12 subject { page }
14 13  
15 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 19 describe "with text" do
33 20 before do
34   - within(".js-main-target-form") do
  21 + within(".wall-note-form") do
35 22 fill_in "note[note]", with: "This is awesome"
36 23 end
37 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 27 end
57 28 end
58 29  
59 30 describe "when posting a note" do
60 31 before do
61   - within(".js-main-target-form") do
  32 + within(".wall-note-form") do
62 33 fill_in "note[note]", with: "This is awsome!"
63   - find(".js-note-preview-button").trigger("click")
64 34 click_button "Add Comment"
65 35 end
66 36 end
67 37  
68   - # note added
69 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 40 end
85 41 end
... ...
spec/features/security/project_access_spec.rb
... ... @@ -95,7 +95,7 @@ describe &quot;Application access&quot; do
95 95 end
96 96  
97 97 describe "GET /project_code/wall" do
98   - subject { wall_project_path(project) }
  98 + subject { project_wall_path(project) }
99 99  
100 100 it { should be_allowed_for master }
101 101 it { should be_allowed_for reporter }
... ...
spec/mailers/notify_spec.rb
... ... @@ -239,7 +239,7 @@ describe Notify do
239 239 end
240 240  
241 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 244 subject { Notify.note_wall_email(recipient.id, note.id) }
245 245  
... ...