diff --git a/Gemfile b/Gemfile
index 4a3de515ade..9c3cc781d23 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 4922904e8c6..952c5910c22 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 00000000000..99286532988
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 68f5f26c8ef..c869707b72e 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 00000000000..fbf60f58022
--- /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 00000000000..a01f2709d70
--- /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('
');
+ },
+
+ 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('
');
+ });
+ },
+});
+
diff --git a/app/assets/javascripts/app/views/publisher_view.js b/app/assets/javascripts/app/views/publisher_view.js
index 56cd35f9181..137bbe381a3 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 89ddf9927d9..a8a3d8c52cb 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 f7ca3afb48d..755afb9a72c 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 5562329d4a2..2df7a3a006d 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 00000000000..1ff7662946c
--- /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 c0f9c516a11..bda28d483ac 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 00000000000..c6c7ea7ecf8
--- /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 5a4df8288ff..be0587f7201 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 3b8a4d120ab..11944280536 100644
--- a/app/controllers/status_messages_controller.rb
+++ b/app/controllers/status_messages_controller.rb
@@ -46,6 +46,7 @@ def create
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 00000000000..b3896c32e43
--- /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 847f7ddda52..3926a0abfbc 100644
--- a/app/models/post.rb
+++ b/app/models/post.rb
@@ -66,6 +66,10 @@ def raw_message; ""; end
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 e5bdba9b9ef..1815badb479 100644
--- a/app/models/reshare.rb
+++ b/app/models/reshare.rb
@@ -79,6 +79,10 @@ def absolute_root
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 949db615e54..c46c3e3d364 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 @@ def contains_oembed_url_in_text?
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 895ff9a4f8b..03d05f559e8 100644
--- a/app/presenters/post_presenter.rb
+++ b/app/presenters/post_presenter.rb
@@ -33,6 +33,7 @@ def as_json(options={})
: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 f2150dfca87..f892b4e44f7 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 92984d99553..6b98b10b2cb 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 44e6f0e42e5..4c2507c2f77 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 00000000000..f46d768602a
--- /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 9b1ff820f0b..79e696a656b 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -151,6 +151,15 @@
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 00000000000..68e5ac59bf8
--- /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 e40c664662b..5c9b0866a0c 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 00000000000..5cd22fa690e
--- /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 00000000000..f3c45d65448
--- /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