diff --git a/Gemfile b/Gemfile index 4a3de515a..9c3cc781d 100644 --- a/Gemfile +++ b/Gemfile @@ -160,7 +160,7 @@ group :development do gem 'guard-rspec', '2.5.3' gem 'rb-fsevent', '0.9.3', :require => false gem 'rb-inotify', '0.9.0', :require => false - + # Preloading environment gem 'guard-spork', '1.5.0' @@ -197,4 +197,5 @@ group :development, :test do # Jasmine (client side application tests (JS)) gem 'jasmine', '1.3.2' + gem 'sinon-rails', '1.4.2.1' end diff --git a/Gemfile.lock b/Gemfile.lock index 4922904e8..952c5910c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -211,7 +211,6 @@ GEM rails multi_json (1.7.2) multipart-post (1.2.0) - mysql2 (0.3.11) nested_form (0.3.2) net-scp (1.1.0) net-ssh (>= 2.6.5) @@ -241,6 +240,7 @@ GEM multi_json (~> 1.3) omniauth-oauth (~> 1.0) orm_adapter (0.4.0) + pg (0.15.1) polyglot (0.3.3) pry (0.9.12) coderay (~> 1.0.5) @@ -357,6 +357,8 @@ GEM rack (~> 1.3, >= 1.3.6) rack-protection (~> 1.2) tilt (~> 1.3, >= 1.3.3) + sinon-rails (1.4.2.1) + railties (>= 3.1) slim (1.3.8) temple (~> 0.6.3) tilt (~> 1.3.3) @@ -441,12 +443,12 @@ DEPENDENCIES messagebus_ruby_api (= 1.0.3) mini_magick (= 3.5) mobile-fu (= 1.1.1) - mysql2 (= 0.3.11) nokogiri (= 1.5.9) omniauth (= 1.1.4) omniauth-facebook (= 1.4.1) omniauth-tumblr (= 1.1) omniauth-twitter (= 0.0.16) + pg (= 0.15.1) rack-cors (= 0.2.7) rack-google-analytics (= 0.11.0) rack-piwik (= 0.2.2) @@ -470,6 +472,7 @@ DEPENDENCIES selenium-webdriver (= 2.32.1) sidekiq (= 2.11.1) sinatra (= 1.3.3) + sinon-rails (= 1.4.2.1) slim (= 1.3.8) spork (= 1.0.0rc3) timecop (= 0.6.1) diff --git a/app/assets/images/icons/marker.png b/app/assets/images/icons/marker.png new file mode 100644 index 000000000..992865329 Binary files /dev/null and b/app/assets/images/icons/marker.png differ diff --git a/app/assets/javascripts/app/views/content_view.js b/app/assets/javascripts/app/views/content_view.js index 68f5f26c8..c869707b7 100644 --- a/app/assets/javascripts/app/views/content_view.js +++ b/app/assets/javascripts/app/views/content_view.js @@ -7,7 +7,8 @@ app.views.Content = app.views.Base.extend({ return _.extend(this.defaultPresenter(), { text : app.helpers.textFormatter(this.model.get("text"), this.model), largePhoto : this.largePhoto(), - smallPhotos : this.smallPhotos() + smallPhotos : this.smallPhotos(), + location: this.location() }); }, @@ -34,12 +35,16 @@ app.views.Content = app.views.Base.extend({ $(evt.currentTarget).hide(); }, + location: function(){ + var address = this.model.get('address')? this.model.get('address') : ''; + return address; + }, + collapseOversized : function() { var collHeight = 200 , elem = this.$(".collapsible") , oembed = elem.find(".oembed") , addHeight = 0; - if($.trim(oembed.html()) != "") { addHeight = oembed.height(); } @@ -100,4 +105,4 @@ app.views.OEmbed = app.views.Base.extend({ insertHTML.attr("src", insertHTML.attr("src") + paramSeparator + "autoplay=1"); this.$el.html(insertHTML); } -}) \ No newline at end of file +}); diff --git a/app/assets/javascripts/app/views/location_stream.js b/app/assets/javascripts/app/views/location_stream.js new file mode 100644 index 000000000..fbf60f580 --- /dev/null +++ b/app/assets/javascripts/app/views/location_stream.js @@ -0,0 +1,3 @@ +app.views.LocationStream = app.views.Content.extend({ + templateName: "status-message-location" +}); diff --git a/app/assets/javascripts/app/views/location_view.js b/app/assets/javascripts/app/views/location_view.js new file mode 100644 index 000000000..a01f2709d --- /dev/null +++ b/app/assets/javascripts/app/views/location_view.js @@ -0,0 +1,25 @@ +app.views.Location = Backbone.View.extend({ + + el: "#location", + + initialize: function(){ + this.render(); + this.getLocation(); + }, + + render: function(){ + $(this.el).append('delete location'); + }, + + getLocation: function(e){ + element = this.el; + + locator = new OSM.Locator(); + locator.getAddress(function(address, latlng){ + $(element).html(''); + $('#location_coords').val(latlng.latitude + "," + latlng.longitude); + $(element).append('delete location'); + }); + }, +}); + diff --git a/app/assets/javascripts/app/views/publisher_view.js b/app/assets/javascripts/app/views/publisher_view.js index 56cd35f91..137bbe381 100644 --- a/app/assets/javascripts/app/views/publisher_view.js +++ b/app/assets/javascripts/app/views/publisher_view.js @@ -23,7 +23,10 @@ app.views.Publisher = Backbone.View.extend(_.extend( "click .post_preview_button" : "createPostPreview", "click .service_icon": "toggleService", "textchange #status_message_fake_text": "handleTextchange", - "click .dropdown .dropdown_list li": "toggleAspect" + "click .dropdown .dropdown_list li": "toggleAspect", + "click #locator" : "showLocation", + "click #hide_location" : "destroyLocation", + "keypress #location_address" : "avoidEnter" }, tooltipSelector: ".service_icon", @@ -79,7 +82,9 @@ app.views.Publisher = Backbone.View.extend(_.extend( }, "aspect_ids" : serializedForm["aspect_ids[]"], "photos" : serializedForm["photos[]"], - "services" : serializedForm["services[]"] + "services" : serializedForm["services[]"], + "location_address" : $("#location_address").val(), + "location_coords" : serializedForm["location[coords]"] }, { url : "/status_messages", success : function() { @@ -94,6 +99,30 @@ app.views.Publisher = Backbone.View.extend(_.extend( // clear state this.clear(); + + // clear location + this.destroyLocation(); + }, + + // creates the location + showLocation: function(){ + if($('#location').length == 0){ + $('#publisher_textarea_wrapper').after('
'); + app.views.location = new app.views.Location(); + } + }, + + // destroys the location + destroyLocation: function(){ + if(app.views.location){ + app.views.location.remove(); + } + }, + + // avoid submitting form when pressing Enter key + avoidEnter: function(evt){ + if(evt.keyCode == 13) + return false; }, createPostPreview : function(evt) { diff --git a/app/assets/javascripts/app/views/stream_post_views.js b/app/assets/javascripts/app/views/stream_post_views.js index 89ddf9927..a8a3d8c52 100644 --- a/app/assets/javascripts/app/views/stream_post_views.js +++ b/app/assets/javascripts/app/views/stream_post_views.js @@ -7,7 +7,8 @@ app.views.StreamPost = app.views.Post.extend({ ".likes" : "likesInfoView", ".comments" : "commentStreamView", ".post-content" : "postContentView", - ".oembed" : "oEmbedView" + ".oembed" : "oEmbedView", + ".status-message-location" : "postLocationStreamView" }, events: { @@ -47,6 +48,10 @@ app.views.StreamPost = app.views.Post.extend({ return new postClass({ model : this.model }) }, + postLocationStreamView : function(){ + return new app.views.LocationStream({ model : this.model}); + }, + removeNsfwShield: function(evt){ if(evt){ evt.preventDefault(); } this.model.set({nsfw : false}) diff --git a/app/assets/javascripts/jasmine-load-all.js b/app/assets/javascripts/jasmine-load-all.js index f7ca3afb4..755afb9a7 100644 --- a/app/assets/javascripts/jasmine-load-all.js +++ b/app/assets/javascripts/jasmine-load-all.js @@ -9,4 +9,5 @@ //= require mobile //= require profile //= require people -//= require photos \ No newline at end of file +//= require photos +//= require sinon diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 5562329d4..2df7a3a00 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -42,3 +42,4 @@ //= require bootstrap-popover //= require bootstrap-dropdown //= require bootstrap-scrollspy-custom +//= require osmlocator diff --git a/app/assets/javascripts/osmlocator.js b/app/assets/javascripts/osmlocator.js new file mode 100644 index 000000000..1ff766294 --- /dev/null +++ b/app/assets/javascripts/osmlocator.js @@ -0,0 +1,23 @@ +OSM = {}; + +OSM.Locator = function(){ + + var geolocalize = function(callback){ + navigator.geolocation.getCurrentPosition(function(position) { + lat=position.coords.latitude; + lon=position.coords.longitude; + var display_name =$.getJSON("http://nominatim.openstreetmap.org/reverse?format=json&lat="+lat+"&lon="+lon+"&addressdetails=3", function(data){ + return callback(data.display_name, position.coords); + }); + },errorGettingPosition); + }; + + function errorGettingPosition(err) { + $("#location").remove(); + }; + + return { + getAddress: geolocalize + } + +} diff --git a/app/assets/stylesheets/application.css.sass b/app/assets/stylesheets/application.css.sass index c0f9c516a..bda28d483 100644 --- a/app/assets/stylesheets/application.css.sass +++ b/app/assets/stylesheets/application.css.sass @@ -573,9 +573,22 @@ ul.as-selections & > .likes, & > .comments :margin-right 15px +.status-message-location + .near-from + :font-size smaller + :color #aaa + :width 100% + :float left + .address + :font-size 11px + :color #bbb + .stream_element .post-content .reshare :border-left 2px solid #ddd +.stream_element.loaded .media .bd .feedback + :clear both + form.new_comment input :display none @@ -697,6 +710,8 @@ form p.checkbox_select :height 100% :width 100% :cursor pointer + img + :margin-right 20px #publisher :z-index 1 @@ -876,6 +891,8 @@ form p.checkbox_select :position absolute :bottom 0 :right 35px + :width 430px + :left 5px :padding 0 li @@ -3120,3 +3137,46 @@ body :bottom 3px solid #3f8fba !important :background :color #e8f7ff + +#publisher-images + #locator + :bottom 1px !important + :display inline-block + :margin 0 + :position absolute !important + :right 30px + :cursor pointer + img + :padding-top 2px + @include opacity(0.4) + &:hover + :color #666 + :cursor pointer + img + @include opacity(0.8) + .btn + :height 19px + :width 19px + +#location + :border 1px solid #999 + :height 20px + #location_address + :border none + :color #aaa + :height 10px + :width 430px + :float left + a#hide_location + :position absolute + :right 22px + :filter alpha(opacity=30) + :-moz-opacity 0.3 + :-khtml-opacity 0.3 + :opacity 0.3 + :z-index 5 + a#hide_location:hover + @include opacity(0) + :-khtml-opacity 1 + :opacity 1 + :cursor pointer diff --git a/app/assets/templates/status-message-location_tpl.jst.hbs b/app/assets/templates/status-message-location_tpl.jst.hbs new file mode 100644 index 000000000..c6c7ea7ec --- /dev/null +++ b/app/assets/templates/status-message-location_tpl.jst.hbs @@ -0,0 +1,5 @@ +{{#if location}} +
+ {{ t "publisher.near_from" location=location}} +
+{{/if}} diff --git a/app/assets/templates/stream-element_tpl.jst.hbs b/app/assets/templates/stream-element_tpl.jst.hbs index 5a4df8288..be0587f72 100644 --- a/app/assets/templates/stream-element_tpl.jst.hbs +++ b/app/assets/templates/stream-element_tpl.jst.hbs @@ -55,6 +55,7 @@ {{/if}}
+
diff --git a/app/controllers/status_messages_controller.rb b/app/controllers/status_messages_controller.rb index 3b8a4d120..119442805 100644 --- a/app/controllers/status_messages_controller.rb +++ b/app/controllers/status_messages_controller.rb @@ -46,6 +46,7 @@ class StatusMessagesController < ApplicationController services = [*params[:services]].compact @status_message = current_user.build_post(:status_message, params[:status_message]) + @status_message.build_location(:address => params[:location_address], :coordinates => params[:location_coords]) if params[:location_address].present? @status_message.attach_photos_by_ids(params[:photos]) if @status_message.save diff --git a/app/models/location.rb b/app/models/location.rb new file mode 100644 index 000000000..b3896c32e --- /dev/null +++ b/app/models/location.rb @@ -0,0 +1,12 @@ +class Location < ActiveRecord::Base + + before_validation :split_coords, :on => :create + + attr_accessor :coordinates + + belongs_to :status_message + + def split_coords + coordinates.present? ? (self.lat, self.lng = coordinates.split(',')) : false + end +end diff --git a/app/models/post.rb b/app/models/post.rb index 847f7ddda..3926a0abf 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -66,6 +66,10 @@ class Post < ActiveRecord::Base def mentioned_people; []; end def photos; []; end + #prevents error when trying to access @post.address in a post different than Reshare and StatusMessage types; + #check PostPresenter + def address + end def self.excluding_blocks(user) people = user.blocks.map{|b| b.person_id} diff --git a/app/models/reshare.rb b/app/models/reshare.rb index e5bdba9b9..1815badb4 100644 --- a/app/models/reshare.rb +++ b/app/models/reshare.rb @@ -79,6 +79,10 @@ class Reshare < Post current end + def address + absolute_root.location.try(:address) + end + private def after_parse diff --git a/app/models/status_message.rb b/app/models/status_message.rb index 949db615e..c46c3e3d3 100644 --- a/app/models/status_message.rb +++ b/app/models/status_message.rb @@ -18,6 +18,8 @@ class StatusMessage < Post has_many :photos, :dependent => :destroy, :foreign_key => :status_message_guid, :primary_key => :guid + has_one :location + # a StatusMessage is federated before its photos are so presence_of_content() fails erroneously if no text is present # therefore, we put the validation in a before_destory callback instead of a validation before_destroy :presence_of_content @@ -164,6 +166,10 @@ class StatusMessage < Post self.oembed_url = urls.find{ |url| !TRUSTED_OEMBED_PROVIDERS.find(url).nil? } end + def address + location.try(:address) + end + protected def presence_of_content unless text_and_photos_blank? diff --git a/app/presenters/post_presenter.rb b/app/presenters/post_presenter.rb index 895ff9a4f..03d05f559 100644 --- a/app/presenters/post_presenter.rb +++ b/app/presenters/post_presenter.rb @@ -33,6 +33,7 @@ class PostPresenter :title => title, :next_post => next_post_path, :previous_post => previous_post_path, + :address => @post.address, :interactions => { :likes => [user_like].compact, diff --git a/app/views/shared/_publisher.html.haml b/app/views/shared/_publisher.html.haml index f2150dfca..f892b4e44 100644 --- a/app/views/shared/_publisher.html.haml +++ b/app/views/shared/_publisher.html.haml @@ -26,8 +26,12 @@ = status.text_area :fake_text, :rows => 2, :value => h(publisher_formatted_text), :tabindex => 1, :placeholder => "#{t('contacts.index.start_a_conversation')}..." = status.hidden_field :text, :value => h(publisher_hidden_text), :class => 'clear_on_submit' - #file-upload{:title => t('.upload_photos')} - = image_tag 'icons/camera.png', :alt => t('.upload_photos').titleize + #publisher-images + #locator.btn{:title => t('.get_location')} + = image_tag 'icons/marker.png', :alt => t('.get_location').titleize, :class => 'publisher_image' + #file-upload.btn{:title => t('.upload_photos')} + = image_tag 'icons/camera.png', :alt => t('.upload_photos').titleize, :class => 'publisher_image' + = hidden_field :location, :coords - if publisher_public = hidden_field_tag 'aspect_ids[]', "public" diff --git a/config/locales/diaspora/en.yml b/config/locales/diaspora/en.yml index 92984d995..6b98b10b2 100644 --- a/config/locales/diaspora/en.yml +++ b/config/locales/diaspora/en.yml @@ -794,6 +794,7 @@ en: make_public: "make public" all: "all" upload_photos: "Upload photos" + get_location: "Get your location" all_contacts: "all contacts" share_with: "share with" whats_on_your_mind: "What's on your mind?" diff --git a/config/locales/javascript/javascript.en.yml b/config/locales/javascript/javascript.en.yml index 44e6f0e42..4c2507c2f 100644 --- a/config/locales/javascript/javascript.en.yml +++ b/config/locales/javascript/javascript.en.yml @@ -41,6 +41,7 @@ en: at_least_one_aspect: "You must publish to at least one aspect" limited: "Limited - your post will only be seen by people you are sharing with" public: "Public - your post will be visible to everyone and found by search engines" + near_from: "Near from: <%= location %>" infinite_scroll: no_more: "No more posts." no_more_contacts: "No more contacts." diff --git a/db/migrate/20120405170105_create_locations.rb b/db/migrate/20120405170105_create_locations.rb new file mode 100644 index 000000000..f46d76860 --- /dev/null +++ b/db/migrate/20120405170105_create_locations.rb @@ -0,0 +1,12 @@ +class CreateLocations < ActiveRecord::Migration + def change + create_table :locations do |t| + t.string :address + t.string :lat + t.string :lng + t.integer :status_message_id + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 9b1ff820f..79e696a65 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -151,6 +151,15 @@ ActiveRecord::Schema.define(:version => 20130404211624) do add_index "likes", ["target_id", "author_id", "target_type"], :name => "index_likes_on_target_id_and_author_id_and_target_type", :unique => true add_index "likes", ["target_id"], :name => "index_likes_on_post_id" + create_table "locations", :force => true do |t| + t.string "address" + t.string "lat" + t.string "lng" + t.integer "status_message_id" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + create_table "mentions", :force => true do |t| t.integer "post_id", :null => false t.integer "person_id", :null => false diff --git a/spec/javascripts/app/views/location_view_spec.js b/spec/javascripts/app/views/location_view_spec.js new file mode 100644 index 000000000..68e5ac59b --- /dev/null +++ b/spec/javascripts/app/views/location_view_spec.js @@ -0,0 +1,17 @@ +describe("app.views.Location", function(){ + beforeEach(function(){ + OSM = {}; + OSM.Locator = function(){return { getAddress:function(){}}}; + + this.view = new app.views.Location(); + }); + + describe("When it gets instantiated", function(){ + it("creates #location_address", function(){ + + expect($("#location_address")).toBeTruthy(); + expect($("#location_coords")).toBeTruthy(); + expect($("#hide_location")).toBeTruthy(); + }) + }); +}); diff --git a/spec/javascripts/app/views/publisher_view_spec.js b/spec/javascripts/app/views/publisher_view_spec.js index e40c66466..5c9b0866a 100644 --- a/spec/javascripts/app/views/publisher_view_spec.js +++ b/spec/javascripts/app/views/publisher_view_spec.js @@ -292,4 +292,66 @@ describe("app.views.Publisher", function() { }); }); }); + + context("locator", function() { + beforeEach(function() { + // should be jasmine helper + loginAs({name: "alice", avatar : {small : "http://avatar.com/photo.jpg"}}); + + spec.loadFixture("aspects_index"); + this.view = new app.views.Publisher(); + }); + + describe('#showLocation', function(){ + it("Show location", function(){ + + // inserts location to the DOM; it is the location's view element + setFixtures('
'); + + // creates a fake Locator + OSM = {}; + OSM.Locator = function(){return { getAddress:function(){}}}; + + // validates there is not location + expect($("#location").length).toBe(0); + + // this should create a new location + this.view.showLocation(); + + // validates there is one location created + expect($("#location").length).toBe(1); + }) + }); + + describe('#destroyLocation', function(){ + it("Destroy location if exists", function(){ + + // inserts location to the DOM; it is the location's view element + setFixtures('
'); + + // creates a new Location view with the #location element + app.views.Location = new Backbone.View({el:"#location"}); + + // creates the mock + app.views.location = sinon.mock(app.views.Location).object; + + // calls the destroy function and test the expected result + this.view.destroyLocation(); + expect($("#location").length).toBe(0); + }) + }); + + describe('#avoidEnter', function(){ + it("Avoid submitting the form when pressing enter", function(){ + // simulates the event object + evt = {}; + evt.keyCode = 13; + + // should return false in order to avoid the form submition + expect(this.view.avoidEnter(evt)).toBeFalsy(); + }) + }); + }); + }); + diff --git a/spec/javascripts/osmlocator-spec.js b/spec/javascripts/osmlocator-spec.js new file mode 100644 index 000000000..5cd22fa69 --- /dev/null +++ b/spec/javascripts/osmlocator-spec.js @@ -0,0 +1,24 @@ +describe("Locator", function(){ + navigator.geolocation['getCurrentPosition'] = function(myCallback){ + lat = 1; + lon = 2; + position = { coords: { latitude: lat, longitude: lon} } + return myCallback(position); + }; + + $.getJSON = function(url, myCallback){ + if(url == "http://nominatim.openstreetmap.org/reverse?format=json&lat=1&lon=2&addressdetails=3") + { + return myCallback({ display_name: 'locator address' }) + } + } + + var osmlocator = new OSM.Locator(); + + it("should return address, latitude, and longitude using getAddress method", function(){ + osmlocator.getAddress(function(display_name, coordinates){ + expect(display_name, 'locator address') + expect(coordinates, { latitude: 1, longitude: 2 }) + }) + }); +}); diff --git a/spec/models/location_spec.rb b/spec/models/location_spec.rb new file mode 100644 index 000000000..f3c45d654 --- /dev/null +++ b/spec/models/location_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +describe Location do + describe 'before validation' do + it 'should create new location when it has coordinates' do + location = Location.new(coordinates:'1,2') + location.save.should be true + end + + it 'should not create new location when it does not have coordinates' do + location = Location.new() + location.save.should be false + end + end +end