Merge branch 'notes'

Conflicts:
	Vendorfile
This commit is contained in:
Tom Hughes 2013-04-23 22:40:21 +01:00
commit 0c8ad2f86e
84 changed files with 3162 additions and 107 deletions

View file

@ -29,6 +29,7 @@ gem 'paperclip', '~> 2.0'
gem 'deadlock_retry', '>= 1.2.0'
gem 'i18n-js', '>= 3.0.0.rc2'
gem 'rack-cors'
gem 'jsonify-rails'
# We need ruby-openid 2.2.0 or later for ruby 1.9 support
gem 'ruby-openid', '>= 2.2.0'

View file

@ -67,6 +67,11 @@ GEM
railties (>= 3.0, < 5.0)
thor (>= 0.14, < 2.0)
json (1.7.7)
jsonify (0.3.1)
multi_json (~> 1.0)
jsonify-rails (0.3.2)
actionpack
jsonify (< 0.4.0)
jwt (0.1.8)
multi_json (>= 1.5)
libv8 (3.3.10.4)
@ -183,6 +188,7 @@ DEPENDENCIES
i18n-js (>= 3.0.0.rc2)
iconv (= 0.1)
jquery-rails
jsonify-rails
libxml-ruby (>= 2.0.5)
memcached (>= 1.4.1)
minitest

View file

@ -29,6 +29,13 @@ folder 'vendor/assets' do
folder 'jquery' do
from 'git://github.com/jevin/Autogrow-Textarea.git' do
file 'jquery.autogrowtextarea.js', 'jquery.autogrowtextarea.js'
end
end
folder 'ohauth' do
from 'git://github.com/tmcw/ohauth.git' do
file 'ohauth.js'
file 'sha.js'
end
end
end

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View file

@ -10,6 +10,7 @@
//= require leaflet.pan
//= require leaflet.zoom
//= require i18n/translations
//= require oauth
//= require osm
//= require piwik
//= require map

View file

@ -43,7 +43,27 @@ $(document).ready(function () {
});
var centre = bbox.getCenter();
updatelinks(centre.lng, centre.lat, 16, null, params.minlon, params.minlat, params.maxlon, params.maxlat);
updatelinks(centre.lon, centre.lat, 16, null, params.minlon, params.minlat, params.maxlon, params.maxlat);
} else if (params.type == "note") {
var object = {type: params.type, id: params.id};
map.setView([params.lat, params.lon], 16);
L.marker([params.lat, params.lon], { icon: getUserIcon() }).addTo(map);
var bbox = map.getBounds();
$("#loading").hide();
$("#browse_map .geolink").show();
$("a[data-editor=remote]").click(function () {
return remoteEditHandler(bbox);
});
updatelinks(params.lon, params.lat, 16, null,
bbox.getWestLng(), bbox.getSouthLat(),
bbox.getEastLng(), bbox.getNorthLat(),
object);
} else {
$("#object_larger_map").hide();
$("#object_edit").hide();

View file

@ -2,6 +2,7 @@
//= require index/browse
//= require index/export
//= require index/key
//= require index/notes
$(document).ready(function () {
var permalinks = $("#permalink").detach().html();

View file

@ -0,0 +1,281 @@
//= require templates/notes/show
//= require templates/notes/new
$(document).ready(function () {
var params = OSM.mapParams();
var noteIcons = {
"new": L.icon({
iconUrl: "<%= image_path 'new_note_marker.png' %>",
iconSize: [22, 22],
iconAnchor: [11, 11]
}),
"open": L.icon({
iconUrl: "<%= image_path 'open_note_marker.png' %>",
iconSize: [22, 22],
iconAnchor: [11, 11]
}),
"closed": L.icon({
iconUrl: "<%= image_path 'closed_note_marker.png' %>",
iconSize: [22, 22],
iconAnchor: [11, 11]
})
};
var noteLayer = new L.LayerGroup();
var notes = {};
var newNote;
map.on("layeradd", function (e) {
if (e.layer == noteLayer) {
loadNotes();
map.on("moveend", loadNotes);
}
});
map.on("layerremove", function (e) {
if (e.layer == noteLayer) {
map.off("moveend", loadNotes);
noteLayer.clearLayers();
notes = {};
}
});
map.on("popupopen", function (e) {
$(e.popup._container).find(".comment").focus();
});
map.on("popupclose", function (e) {
if (newNote && e.popup == newNote._popup) {
$(newNote).oneTime(10, "removenote", function () {
map.removeLayer(newNote);
newNote = null;
});
}
});
if (OSM.STATUS != 'api_offline' && OSM.STATUS != 'database_offline') {
map.layersControl.addOverlay(noteLayer, I18n.t("browse.start_rjs.notes_layer_name"));
if (params.notes) map.addLayer(noteLayer);
if (params.note) {
$.ajax({
url: "/api/" + OSM.API_VERSION + "/notes/" + params.note + ".json",
success: function (feature) {
var marker = updateMarker(notes[feature.properties.id], feature);
notes[feature.properties.id] = marker;
map.addLayer(noteLayer);
marker.openPopup();
}
});
}
}
function updateMarker(marker, feature) {
if (marker)
{
marker.setIcon(noteIcons[feature.properties.status]);
marker._popup.setContent(createPopupContent(marker, feature.properties));
}
else
{
marker = L.marker(feature.geometry.coordinates.reverse(), {
icon: noteIcons[feature.properties.status],
opacity: 0.7
});
marker.addTo(noteLayer).bindPopup(
createPopupContent(marker, feature.properties),
popupOptions()
);
}
return marker;
}
var noteLoader;
function loadNotes() {
var bounds = map.getBounds();
var size = bounds.getSize();
if (size <= OSM.MAX_NOTE_REQUEST_AREA) {
var url = "/api/" + OSM.API_VERSION + "/notes.json?bbox=" + bounds.toBBOX();
if (noteLoader) noteLoader.abort();
noteLoader = $.ajax({
url: url,
success: function (json) {
var oldNotes = notes;
notes = {};
json.features.forEach(function (feature) {
var marker = oldNotes[feature.properties.id];
delete oldNotes[feature.properties.id];
notes[feature.properties.id] = updateMarker(marker, feature);
});
for (id in oldNotes) {
noteLayer.removeLayer(oldNotes[id]);
}
noteLoader = null;
}
});
}
};
function popupOptions() {
var mapSize = map.getSize();
return {
minWidth: 320,
maxWidth: mapSize.y * 1 / 3,
maxHeight: mapSize.y * 2 / 3,
offset: new L.Point(0, -3),
autoPanPadding: new L.Point(60, 40)
};
}
function createPopupContent(marker, properties) {
var content = $(JST["templates/notes/show"]({ note: properties }));
content.find("textarea").on("input", function (e) {
var form = e.target.form;
if ($(e.target).val() == "") {
$(form.close).val(I18n.t("javascripts.notes.show.resolve"));
$(form.comment).prop("disabled", true);
} else {
$(form.close).val(I18n.t("javascripts.notes.show.comment_and_resolve"));
$(form.comment).prop("disabled", false);
}
});
content.find("input[type=submit]").on("click", function (e) {
e.preventDefault();
var data = $(e.target).data();
updateNote(marker, e.target.form, data.method, data.url);
});
return content[0];
}
function createNote(marker, form, url) {
var location = marker.getLatLng();
$(form).find("input[type=submit]").prop("disabled", true);
$.ajax({
url: url,
type: "POST",
oauth: true,
data: {
lat: location.lat,
lon: location.lng,
text: $(form.text).val()
},
success: function (feature) {
notes[feature.properties.id] = updateMarker(marker, feature);
newNote = null;
$("#createnoteanchor").removeClass("disabled").addClass("geolink");
}
});
}
function updateNote(marker, form, method, url) {
$(form).find("input[type=submit]").prop("disabled", true);
$.ajax({
url: url,
type: method,
oauth: true,
data: {
text: $(form.text).val()
},
success: function (feature) {
if (feature.properties.status == "hidden") {
noteLayer.removeLayer(marker);
delete notes[feature.properties.id];
} else {
var popupContent = createPopupContent(marker, feature.properties);
marker.setIcon(noteIcons[feature.properties.status]);
marker._popup.setContent(popupContent);
}
}
});
}
$("#createnoteanchor").click(function (e) {
e.preventDefault();
if ($(e.target).hasClass("disabled")) return;
$(e.target).removeClass("geolink").addClass("disabled");
map.addLayer(noteLayer);
var mapSize = map.getSize();
var markerPosition;
if (mapSize.y > 800)
{
markerPosition = [mapSize.x / 2, mapSize.y / 2];
}
else if (mapSize.y > 400)
{
markerPosition = [mapSize.x / 2, 400];
}
else
{
markerPosition = [mapSize.x / 2, mapSize.y];
}
newNote = L.marker(map.containerPointToLatLng(markerPosition), {
icon: noteIcons["new"],
opacity: 0.7,
draggable: true
});
var popupContent = $(JST["templates/notes/new"]({ create_url: $(e.target).attr("href") }));
popupContent.find("textarea").on("input", function (e) {
var form = e.target.form;
if ($(e.target).val() == "") {
$(form.add).prop("disabled", true);
} else {
$(form.add).prop("disabled", false);
}
});
popupContent.find("input[type=submit]").on("click", function (e) {
e.preventDefault();
createNote(newNote, e.target.form, $(e.target).data("url"));
});
newNote.addTo(noteLayer).bindPopup(popupContent[0], popupOptions()).openPopup();
newNote.on("remove", function (e) {
$("#createnoteanchor").removeClass("disabled").addClass("geolink");
});
newNote.on("dragstart", function (e) {
$(newNote).stopTime("removenote");
});
newNote.on("dragend", function (e) {
e.target.openPopup();
});
});
});

View file

@ -0,0 +1,25 @@
//= require sha
//= require ohauth
$.ajaxPrefilter(function(options, jqxhr) {
if (options.oauth && OSM.oauth_token) {
var ohauth = window.ohauth;
var url = options.url.replace(/\?$/, "");
var params = {
oauth_consumer_key: OSM.oauth_consumer_key,
oauth_token: OSM.oauth_token,
oauth_signature_method: "HMAC-SHA1",
oauth_timestamp: ohauth.timestamp(),
oauth_nonce: ohauth.nonce()
};
params.oauth_signature = ohauth.signature(
OSM.oauth_consumer_secret,
OSM.oauth_token_secret,
ohauth.baseString(options.type, url, $.extend({}, params, jqxhr.data))
);
options.headers = options.headers || {};
options.headers.Authorization = "OAuth " + ohauth.authHeader(params);
}
});

View file

@ -1,13 +1,14 @@
OSM = {
<% if defined?(PIWIK_LOCATION) and defined?(PIWIK_SITE) %>
PIWIK_LOCATION: <%= PIWIK_LOCATION.to_json %>,
PIWIK_SITE: <%= PIWIK_SITE.to_json %>,
PIWIK_LOCATION: <%= PIWIK_LOCATION.to_json %>,
PIWIK_SITE: <%= PIWIK_SITE.to_json %>,
<% end %>
MAX_REQUEST_AREA: <%= MAX_REQUEST_AREA.to_json %>,
SERVER_URL: <%= SERVER_URL.to_json %>,
API_VERSION: <%= API_VERSION.to_json %>,
STATUS: <%= STATUS.to_json %>,
MAX_REQUEST_AREA: <%= MAX_REQUEST_AREA.to_json %>,
SERVER_URL: <%= SERVER_URL.to_json %>,
API_VERSION: <%= API_VERSION.to_json %>,
STATUS: <%= STATUS.to_json %>,
MAX_NOTE_REQUEST_AREA: <%= MAX_NOTE_REQUEST_AREA.to_json %>,
apiUrl: function (object) {
var url = "/api/" + OSM.API_VERSION + "/" + object.type + "/" + object.id;
@ -110,6 +111,12 @@ OSM = {
mapParams.lat = (mapParams.minlat + mapParams.maxlat) / 2;
}
mapParams.notes = params.notes == "yes";
if (params.note) {
mapParams.note = parseInt(params.note);
}
var scale = parseFloat(params.scale);
if (scale > 0) {
mapParams.zoom = Math.log(360.0 / (scale * 512.0)) / Math.log(2.0);

View file

@ -0,0 +1,12 @@
<div class="note">
<p><%- I18n.t('javascripts.notes.new.intro') %></p>
<form action="#">
<input type="hidden" name="lon">
<input type="hidden" name="lat">
<textarea class="comment" name="text" cols="40" rows="10"></textarea>
<br/>
<div class="buttons">
<input type="submit" name="add" value="<%- I18n.t('javascripts.notes.new.add') %>" data-url="<%- create_url %>" disabled="1">
</div>
</form>
</div>

View file

@ -0,0 +1,34 @@
<div class="note">
<h2><a href="/?note=<%- note.id %>"><%- I18n.t('javascripts.notes.show.title', { id: note.id }) %></a></h2>
<% if (note.comments.some(function (comment) { return !comment.user })) { %>
<small class="warning"><%- I18n.t('javascripts.notes.show.anonymous_warning') %></small>
<% } %>
<% note.comments.forEach(function (comment) { %>
<div>
<small class="deemphasize">
<% if (comment.user) { %>
<%= I18n.t('javascripts.notes.show.' + comment.action + '_by', {
user: comment.user, user_url: comment.user_url,
time: I18n.l("time.formats.long", comment.date)
}) %>
<% } else { %>
<%- I18n.t('javascripts.notes.show.' + comment.action + '_by_anonymous', {
time: I18n.l("time.formats.long", comment.date)
}) %>
<% } %>
</small>
<div class="comment_body"><%= comment.html %></div>
</div>
<% }) %>
<% if (note.status == "open") { %>
<form action="#">
<textarea class="comment" name="text" cols="40" rows="5"></textarea>
<br/>
<div class="buttons">
<input type="submit" name="hide" value="<%- I18n.t('javascripts.notes.show.hide') %>" class="hide_unless_moderator" data-method="DELETE" data-url="<%- note.url %>">
<input type="submit" name="close" value="<%- I18n.t('javascripts.notes.show.resolve') %>" class="hide_unless_logged_in" data-method="POST" data-url="<%- note.close_url %>">
<input type="submit" name="comment" value="<%- I18n.t('javascripts.notes.show.comment') %>" data-method="POST" data-url="<%- note.comment_url %>" disabled="1">
</div>
</form>
<% } %>
</div>

View file

@ -511,6 +511,7 @@ table {
#greeting {
float: right;
padding-top: 3px;
margin-right: 5px;
}
.greeting-bar-unread {
@ -537,6 +538,12 @@ table {
background:#fff;
}
.leaflet-control-attribution a.disabled {
color: #99c9dc;
cursor: default;
text-decoration: none;
}
.site-index .leaflet-top,
.site-export .leaflet-top {
top: 10px !important;
@ -550,6 +557,12 @@ table {
left: 10px !important;
}
.leaflet-popup-scrolled {
padding-right: 20px;
border-bottom: 0px !important;
border-top: 0px !important;
}
/* Rules for edit menu */
.menuicon {
@ -922,12 +935,20 @@ ul.results-list li { border-bottom: 1px solid #ccc; }
&:first-child {
margin-top: 0;
}
&.warning {
color: #ff7070;
font-weight: bold;
}
h4, p {
margin-bottom: 5px;
}
p, ul, .bbox {
margin-left: 33.3333%;
}
ul p {
margin-left: 0;
margin-bottom: 0;
}
h4 {
width: 33.3333%;
float: left;
@ -1488,6 +1509,9 @@ ul.secondary-actions {
border-left: 0;
padding-left: 0;
}
&:last-child {
margin-right: 0px;
}
}
}
@ -1677,3 +1701,53 @@ a.button.submit {
}
}
}
/* Rules for the user notes list */
.note_list {
tr.creator {
background-color: #eeeeee;
}
td {
padding: 3px;
}
p {
margin-bottom: 0px;
}
}
/* Rules for the notes interface */
.note {
h2 {
margin-bottom: 10px;
}
.warning {
display: block;
background-color: #ff7070;
padding: 4px 6px;
margin-bottom: 10px;
}
.comment_body {
margin-top: 4px;
margin-bottom: 4px;
p {
margin-top: 0px;
margin-bottom: 0px;
}
}
.comment {
width: 100%;
}
.buttons {
margin-top: 5px;
text-align: right;
}
}

View file

@ -55,6 +55,10 @@ class ApplicationController < ActionController::Base
end
end
def require_oauth
@oauth = @user.access_token(OAUTH_KEY) if @user and defined? OAUTH_KEY
end
##
# requires the user to be logged in by the token or HTTP methods, or have an
# OAuth token with the right capability. this method is a bit of a pain to call
@ -112,6 +116,9 @@ class ApplicationController < ActionController::Base
def require_allow_write_gpx
require_capability(:allow_write_gpx)
end
def require_allow_write_notes
require_capability(:allow_write_notes)
end
##
# require that the user is a moderator, or fill out a helpful error message

View file

@ -76,4 +76,13 @@ class BrowseController < ApplicationController
rescue ActiveRecord::RecordNotFound
render :action => "not_found", :status => :not_found
end
def note
@type = "note"
@note = Note.find(params[:id])
@next = Note.find(:first, :order => "id ASC", :conditions => [ "status != 'hidden' AND id > :id", { :id => @note.id }] )
@prev = Note.find(:first, :order => "id DESC", :conditions => [ "status != 'hidden' AND id < :id", { :id => @note.id }] )
rescue ActiveRecord::RecordNotFound
render :action => "not_found", :status => :not_found
end
end

View file

@ -0,0 +1,328 @@
class NotesController < ApplicationController
layout 'site', :only => [:mine]
before_filter :check_api_readable
before_filter :authorize_web, :only => [:mine]
before_filter :setup_user_auth, :only => [:create, :comment]
before_filter :authorize, :only => [:close, :destroy]
before_filter :require_moderator, :only => [:destroy]
before_filter :check_api_writable, :only => [:create, :comment, :close, :destroy]
before_filter :require_allow_write_notes, :only => [:create, :comment, :close, :destroy]
before_filter :set_locale, :only => [:mine]
after_filter :compress_output
around_filter :api_call_handle_error, :api_call_timeout
##
# Return a list of notes in a given area
def index
# Figure out the bbox - we prefer a bbox argument but also
# support the old, deprecated, method with four arguments
if params[:bbox]
bbox = BoundingBox.from_bbox_params(params)
else
raise OSM::APIBadUserInput.new("No l was given") unless params[:l]
raise OSM::APIBadUserInput.new("No r was given") unless params[:r]
raise OSM::APIBadUserInput.new("No b was given") unless params[:b]
raise OSM::APIBadUserInput.new("No t was given") unless params[:t]
bbox = BoundingBox.from_lrbt_params(params)
end
# Get any conditions that need to be applied
notes = closed_condition(Note.scoped)
# Check that the boundaries are valid
bbox.check_boundaries
# Check the the bounding box is not too big
bbox.check_size(MAX_NOTE_REQUEST_AREA)
# Find the notes we want to return
@notes = notes.bbox(bbox).order("updated_at DESC").limit(result_limit).preload(:comments)
# Render the result
respond_to do |format|
format.rss
format.xml
format.json
format.gpx
end
end
##
# Create a new note
def create
# Check the arguments are sane
raise OSM::APIBadUserInput.new("No lat was given") unless params[:lat]
raise OSM::APIBadUserInput.new("No lon was given") unless params[:lon]
raise OSM::APIBadUserInput.new("No text was given") if params[:text].blank?
# Extract the arguments
lon = params[:lon].to_f
lat = params[:lat].to_f
comment = params[:text]
# Include in a transaction to ensure that there is always a note_comment for every note
Note.transaction do
# Create the note
@note = Note.create(:lat => lat, :lon => lon)
raise OSM::APIBadUserInput.new("The note is outside this world") unless @note.in_world?
# Save the note
@note.save!
# Add a comment to the note
add_comment(@note, comment, "opened")
end
# Return a copy of the new note
respond_to do |format|
format.xml { render :action => :show }
format.json { render :action => :show }
end
end
##
# Add a comment to an existing note
def comment
# Check the arguments are sane
raise OSM::APIBadUserInput.new("No id was given") unless params[:id]
raise OSM::APIBadUserInput.new("No text was given") if params[:text].blank?
# Extract the arguments
id = params[:id].to_i
comment = params[:text]
# Find the note and check it is valid
@note = Note.find(id)
raise OSM::APINotFoundError unless @note
raise OSM::APIAlreadyDeletedError.new("note", @note.id) unless @note.visible?
raise OSM::APINoteAlreadyClosedError.new(@note) if @note.closed?
# Add a comment to the note
Note.transaction do
add_comment(@note, comment, "commented")
end
# Return a copy of the updated note
respond_to do |format|
format.xml { render :action => :show }
format.json { render :action => :show }
end
end
##
# Close a note
def close
# Check the arguments are sane
raise OSM::APIBadUserInput.new("No id was given") unless params[:id]
# Extract the arguments
id = params[:id].to_i
comment = params[:text]
# Find the note and check it is valid
@note = Note.find_by_id(id)
raise OSM::APINotFoundError unless @note
raise OSM::APIAlreadyDeletedError.new("note", @note.id) unless @note.visible?
raise OSM::APINoteAlreadyClosedError.new(@note) if @note.closed?
# Close the note and add a comment
Note.transaction do
@note.close
add_comment(@note, comment, "closed")
end
# Return a copy of the updated note
respond_to do |format|
format.xml { render :action => :show }
format.json { render :action => :show }
end
end
##
# Get a feed of recent notes and comments
def feed
# Get any conditions that need to be applied
notes = closed_condition(Note.scoped)
# Process any bbox
if params[:bbox]
bbox = BoundingBox.from_bbox_params(params)
bbox.check_boundaries
bbox.check_size(MAX_NOTE_REQUEST_AREA)
notes = notes.bbox(bbox)
end
# Find the comments we want to return
@comments = NoteComment.where(:note_id => notes).order("created_at DESC").limit(result_limit).preload(:note)
# Render the result
respond_to do |format|
format.rss
end
end
##
# Read a note
def show
# Check the arguments are sane
raise OSM::APIBadUserInput.new("No id was given") unless params[:id]
# Find the note and check it is valid
@note = Note.find(params[:id])
raise OSM::APINotFoundError unless @note
raise OSM::APIAlreadyDeletedError.new("note", @note.id) unless @note.visible?
# Render the result
respond_to do |format|
format.xml
format.rss
format.json
format.gpx
end
end
##
# Delete (hide) a note
def destroy
# Check the arguments are sane
raise OSM::APIBadUserInput.new("No id was given") unless params[:id]
# Extract the arguments
id = params[:id].to_i
comment = params[:text]
# Find the note and check it is valid
@note = Note.find(id)
raise OSM::APINotFoundError unless @note
raise OSM::APIAlreadyDeletedError.new("note", @note.id) unless @note.visible?
# Mark the note as hidden
Note.transaction do
@note.status = "hidden"
@note.save
add_comment(@note, comment, "hidden")
end
# Return a copy of the updated note
respond_to do |format|
format.xml { render :action => :show }
format.json { render :action => :show }
end
end
##
# Return a list of notes matching a given string
def search
# Check the arguments are sane
raise OSM::APIBadUserInput.new("No query string was given") unless params[:q]
# Get any conditions that need to be applied
@notes = closed_condition(Note.scoped)
@notes = @notes.joins(:comments).where("note_comments.body ~ ?", params[:q])
# Find the notes we want to return
@notes = @notes.order("updated_at DESC").limit(result_limit).preload(:comments)
# Render the result
respond_to do |format|
format.rss { render :action => :index }
format.xml { render :action => :index }
format.json { render :action => :index }
format.gpx { render :action => :index }
end
end
##
# Display a list of notes by a specified user
def mine
if params[:display_name]
if @this_user = User.active.find_by_display_name(params[:display_name])
@title = t 'note.mine.title', :user => @this_user.display_name
@heading = t 'note.mine.heading', :user => @this_user.display_name
@description = t 'note.mine.subheading', :user => render_to_string(:partial => "user", :object => @this_user)
@page = (params[:page] || 1).to_i
@page_size = 10
@notes = @this_user.notes.order("updated_at DESC, id").uniq.offset((@page - 1) * @page_size).limit(@page_size).preload(:comments => :author)
else
@title = t 'user.no_such_user.title'
@not_found_user = params[:display_name]
render :template => 'user/no_such_user', :status => :not_found
end
end
end
private
#------------------------------------------------------------
# utility functions below.
#------------------------------------------------------------
##
# Render an OK response
def render_ok
if params[:format] == "js"
render :text => "osbResponse();", :content_type => "text/javascript"
else
render :text => "ok " + @note.id.to_s + "\n", :content_type => "text/plain" if @note
render :text => "ok\n", :content_type => "text/plain" unless @note
end
end
##
# Get the maximum number of results to return
def result_limit
if params[:limit] and params[:limit].to_i > 0 and params[:limit].to_i < 10000
params[:limit].to_i
else
100
end
end
##
# Generate a condition to choose which bugs we want based
# on their status and the user's request parameters
def closed_condition(notes)
if params[:closed]
closed_since = params[:closed].to_i
else
closed_since = 7
end
if closed_since < 0
notes = notes.where("status != 'hidden'")
elsif closed_since > 0
notes = notes.where("(status = 'open' OR (status = 'closed' AND closed_at > '#{Time.now - closed_since.days}'))")
else
notes = notes.where("status = 'open'")
end
return notes
end
##
# Add a comment to a note
def add_comment(note, text, event)
attributes = { :visible => true, :event => event, :body => text }
if @user
attributes[:author_id] = @user.id
else
attributes[:author_ip] = request.remote_ip
end
comment = note.comments.create(attributes, :without_protection => true)
note.comments.map { |c| c.author }.uniq.each do |user|
if user and user != @user
Notifier.note_comment_notification(comment, user).deliver
end
end
end
end

View file

@ -5,6 +5,7 @@ class SiteController < ApplicationController
before_filter :authorize_web
before_filter :set_locale
before_filter :require_user, :only => [:edit]
before_filter :require_oauth, :only => [:index]
def index
unless STATUS == :database_readonly or STATUS == :database_offline

View file

@ -26,6 +26,7 @@ module ApplicationHelper
css << ".hide_if_user_#{@user.id} { display: none !important }" if @user;
css << ".show_if_user_#{@user.id} { display: inline !important }" if @user;
css << ".hide_unless_administrator { display: none !important }" unless @user and @user.administrator?;
css << ".hide_unless_moderator { display: none !important }" unless @user and @user.moderator?;
return content_tag(:style, css, :type => "text/css")
end
@ -95,4 +96,8 @@ module ApplicationHelper
I18n.t("html.dir")
end
end
def friendly_date(date)
content_tag(:span, time_ago_in_words(date), :title => l(date, :format => :friendly))
end
end

View file

@ -22,22 +22,6 @@ module GeocoderHelper
end
def describe_location(lat, lon, zoom = nil, language = nil)
zoom = zoom || 14
language = language || request.user_preferred_languages.join(',')
url = "http://nominatim.openstreetmap.org/reverse?lat=#{lat}&lon=#{lon}&zoom=#{zoom}&accept-language=#{language}"
begin
response = OSM::Timer.timeout(4) do
REXML::Document.new(Net::HTTP.get(URI.parse(url)))
end
rescue Exception
response = nil
end
if response and result = response.get_text("reversegeocode/result")
result.to_s
else
"#{number_with_precision(lat, :precision => 3)}, #{number_with_precision(lon, :precision => 3)}"
end
Nominatim.describe_location(lat, lon, zoom, language)
end
end

View file

@ -0,0 +1,17 @@
module NoteHelper
def note_event(at, by)
if by.nil?
I18n.t("browse.note.at_html", :when => friendly_date(at)).html_safe
else
I18n.t("browse.note.at_by_html", :when => friendly_date(at), :user => note_author(by)).html_safe
end
end
def note_author(author, link_options = {})
if author.nil?
""
else
link_to h(author.display_name), link_options.merge({:controller => "user", :action => "view", :display_name => author.display_name})
end
end
end

View file

@ -16,7 +16,8 @@ class ClientApplication < ActiveRecord::Base
attr_accessible :name, :url, :support_url, :callback_url,
:allow_read_prefs, :allow_write_prefs,
:allow_write_diary, :allow_write_api,
:allow_read_gpx, :allow_write_gpx
:allow_read_gpx, :allow_write_gpx,
:allow_write_notes
before_validation :generate_keys, :on => :create
@ -87,7 +88,8 @@ protected
# have to say up-front what permissions they want and when users sign up they
# can agree or not agree to each of them.
PERMISSIONS = [:allow_read_prefs, :allow_write_prefs, :allow_write_diary,
:allow_write_api, :allow_read_gpx, :allow_write_gpx ]
:allow_write_api, :allow_read_gpx, :allow_write_gpx,
:allow_write_notes]
def generate_keys
self.key = OAuth::Helper.generate_key(40)[0,40]

76
app/models/note.rb Normal file
View file

@ -0,0 +1,76 @@
class Note < ActiveRecord::Base
include GeoRecord
has_many :comments, :class_name => "NoteComment",
:foreign_key => :note_id,
:order => :created_at,
:conditions => { :visible => true }
validates_presence_of :id, :on => :update
validates_uniqueness_of :id
validates_numericality_of :latitude, :only_integer => true
validates_numericality_of :longitude, :only_integer => true
validates_presence_of :closed_at if :status == "closed"
validates_inclusion_of :status, :in => ["open", "closed", "hidden"]
validate :validate_position
attr_accessible :lat, :lon
after_initialize :set_defaults
# Sanity check the latitude and longitude and add an error if it's broken
def validate_position
errors.add(:base, "Note is not in the world") unless in_world?
end
# Close a note
def close
self.status = "closed"
self.closed_at = Time.now.getutc
self.save
end
# Return a flattened version of the comments for a note
def flatten_comment(separator_char, upto_timestamp = :nil)
resp = ""
comment_no = 1
self.comments.each do |comment|
next if upto_timestamp != :nil and comment.created_at > upto_timestamp
resp += (comment_no == 1 ? "" : separator_char)
resp += comment.body if comment.body
resp += " [ "
resp += comment.author.display_name if comment.author
resp += " " + comment.created_at.to_s + " ]"
comment_no += 1
end
return resp
end
# Check if a note is visible
def visible?
status != "hidden"
end
# Check if a note is closed
def closed?
not closed_at.nil?
end
# Return the author object, derived from the first comment
def author
self.comments.first.author
end
# Return the author IP address, derived from the first comment
def author_ip
self.comments.first.author_ip
end
private
# Fill in default values for new notes
def set_defaults
self.status = "open" unless self.attribute_present?(:status)
end
end

View file

@ -0,0 +1,17 @@
class NoteComment < ActiveRecord::Base
belongs_to :note, :foreign_key => :note_id, :touch => true
belongs_to :author, :class_name => "User", :foreign_key => :author_id
validates_presence_of :id, :on => :update
validates_uniqueness_of :id
validates_presence_of :note_id
validates_associated :note
validates_presence_of :visible
validates_associated :author
validates_inclusion_of :event, :in => [ "opened", "closed", "reopened", "commented", "hidden" ]
# Return the comment text
def body
RichText.new("text", read_attribute(:body))
end
end

View file

@ -6,7 +6,7 @@ class Notifier < ActionMailer::Base
def signup_confirm(user, token)
@locale = user.preferred_language_from(I18n.available_locales)
# If we are passed an email address verification token, create
# the confirumation URL for account activation.
#
@ -19,7 +19,7 @@ class Notifier < ActionMailer::Base
:display_name => user.display_name,
:confirm_string => token.token)
end
mail :to => user.email,
:subject => I18n.t('notifier.signup_confirm.subject', :locale => @locale)
end
@ -67,7 +67,7 @@ class Notifier < ActionMailer::Base
mail :to => trace.user.email,
:subject => I18n.t('notifier.gpx_notification.failure.subject', :locale => @locale)
end
def message_notification(message)
@locale = message.recipient.preferred_language_from(I18n.available_locales)
@to_user = message.recipient.display_name
@ -123,6 +123,28 @@ class Notifier < ActionMailer::Base
:subject => I18n.t('notifier.friend_notification.subject', :user => friend.befriender.display_name, :locale => @locale)
end
def note_comment_notification(comment, recipient)
@locale = recipient.preferred_language_from(I18n.available_locales)
@noteurl = browse_note_url(comment.note, :host => SERVER_URL)
@place = Nominatim.describe_location(comment.note.lat, comment.note.lon, 14, @locale)
@comment = comment.body
@owner = recipient == comment.note.author
if comment.author
@commenter = comment.author.display_name
else
@commenter = I18n.t("notifier.note_comment_notification.anonymous")
end
if @owner
subject = I18n.t('notifier.note_comment_notification.subject_own', :commenter => @commenter)
else
subject = I18n.t('notifier.note_comment_notification.subject_other', :commenter => @commenter)
end
mail :to => recipient.email, :subject => subject
end
private
def from_address(name, type, id, digest)

View file

@ -12,6 +12,8 @@ class User < ActiveRecord::Base
has_many :tokens, :class_name => "UserToken"
has_many :preferences, :class_name => "UserPreference"
has_many :changesets, :order => 'created_at DESC'
has_many :note_comments, :foreign_key => :author_id
has_many :notes, :through => :note_comments
has_many :client_applications
has_many :oauth_tokens, :class_name => "OauthToken", :order => "authorized_at desc", :include => [:client_application]

View file

@ -2,7 +2,7 @@
</iframe>
<div id="browse_map" class='clearfix content_map'>
<% if map.instance_of? Changeset or (map.instance_of? Node and map.version > 1) or map.visible %>
<% if map.instance_of? Changeset or (map.instance_of? Node and map.version > 1) or map.visible? %>
<% content_for :head do %>
<%= javascript_include_tag "browse" %>
@ -19,6 +19,13 @@
:maxlon => bbox.max_lon,
:maxlat => bbox.max_lat
}
elsif map.instance_of? Note
data = {
:type => "note",
:id => map.id,
:lon => map.lon,
:lat => map.lat
}
else
data = {
:type => map.class.name.downcase,
@ -33,10 +40,17 @@
<ul class='secondary-actions clearfix'>
<li>
<%= link_to t("browse.map.larger.area"),
root_path(:box => "yes"),
:id => "area_larger_map",
:class => "geolink bbox" %>
<% if map.instance_of? Note -%>
<%= link_to t("browse.map.larger.area"),
root_path(:notes => "yes"),
:id => "area_larger_map",
:class => "geolink bbox" %>
<% else -%>
<%= link_to t("browse.map.larger.area"),
root_path(:box => "yes"),
:id => "area_larger_map",
:class => "geolink bbox" %>
<% end -%>
</li>
<li>
<%= link_to h(t("browse.map.edit.area")) + content_tag(:span, "▼", :class => "menuicon"),

View file

@ -0,0 +1,67 @@
<% content_for :head do %>
<%= stylesheet_link_tag 'browse' %>
<% end %>
<% content_for :heading do %>
<h2>
<%= image_tag "#{@note.status}_note_marker.png", :alt => @note.status %>
<%= t "browse.note.#{@note.status}_title", :note_name => @note.id %>
</h2>
<% end %>
<%= render :partial => "navigation" %>
<%= render :partial => "map", :object => @note %>
<div class='column-1'>
<% if @note.comments.find { |comment| comment.author.nil? } -%>
<div class='browse-section common warning'>
<%= t "javascripts.notes.show.anonymous_warning" %>
</div>
<% end -%>
<div class='browse-section common'>
<div>
<h4><%= t "browse.note.opened" %></h4>
<p><%= note_event(@note.created_at, @note.author) %></p>
</div>
<% if @note.status == "closed" %>
<div>
<h4><%= t "browse.note.closed" %></h4>
<p><%= note_event(@note.closed_at, @note.comments.last.author) %></p>
</div>
<% elsif @note.comments.length > 1 %>
<div>
<h4><%= t "browse.note.last_modified" %></h4>
<p><%= note_event(@note.updated_at, @note.comments.last.author) %></p>
</div>
<% end %>
<div>
<h4><%= t "browse.note.description" %></h4>
<p><%= h(@note.comments.first.body) %></p>
</div>
<div>
<h4><%= t "browse.node_details.coordinates" %></h4>
<p><div class="geo"><%= link_to ("<span class='latitude'>#{number_with_delimiter(@note.lat)}</span>, <span class='longitude'>#{number_with_delimiter(@note.lon)}</span>".html_safe), {:controller => 'site', :action => 'index', :lat => h(@note.lat), :lon => h(@note.lon), :zoom => "18"} %></div></p>
</div>
</div>
<% if @note.comments.length > 1 %>
<div class='browse-section clearfix'>
<h4><%= t "browse.note.comments" %></h4>
<ul>
<% @note.comments[1..-1].each do |comment| %>
<li>
<%= comment.body.to_html %>
<small class="deemphasize"><%= note_event(comment.created_at, comment.author) %></small>
</li>
<% end %>
</ul>
</div>
<% end %>
</div>

View file

@ -2,8 +2,6 @@
<a href="<%= url_for :controller => 'site', :action => 'index', :lat => location.latitude, :lon => location.longitude, :zoom => 14 %>">
<abbr class="geo" title="<%= number_with_precision(location.latitude, :precision => 4) %>; <%= number_with_precision(location.longitude, :precision => 4) %>">
<% cache(:controller => 'diary_entry', :action => 'view', :display_name => location.user.display_name, :id => location.id, :part => "location") do %>
<%= describe_location location.latitude, location.longitude, 14, location.language_code %>
<% end %>
</abbr>
</a>

View file

@ -25,16 +25,19 @@
I18n.defaultLocale = "<%= I18n.default_locale %>";
I18n.locale = "<%= I18n.locale %>";
I18n.fallbacks = true;
<% if @user and !@user.home_lon.nil? and !@user.home_lat.nil? %>
<% if @user and !@user.home_lon.nil? and !@user.home_lat.nil? -%>
OSM.home = <%= { :lat => @user.home_lat, :lon => @user.home_lon }.to_json.html_safe %>;
<% end %>
<% if session[:location] %>
<% end -%>
<% if session[:location] -%>
OSM.location = <%= session[:location].to_json.html_safe %>;
<% end %>
<% end -%>
OSM.preferred_editor = <%= preferred_editor.to_json.html_safe %>;
<% if @oauth -%>
OSM.oauth_token = "<%= @oauth.token %>";
OSM.oauth_token_secret = "<%= @oauth.secret %>";
OSM.oauth_consumer_key = "<%= @oauth.client_application.key %>";
OSM.oauth_consumer_secret = "<%= @oauth.client_application.secret %>";
<% end -%>
</script>
<title><%= t 'layouts.project_name.title' %><%= ' | '+ @title if @title %></title>
</head>

View file

@ -0,0 +1,12 @@
<div>
<% description.comments.each do |comment| -%>
<div class="note-comment" style="margin-top: 5px">
<% if comment.author.nil? -%>
<div class="note-comment-description" style="font-size: smaller; color: #999999"><%= t "note.description.#{comment.event}_at", :when => friendly_date(comment.created_at) %></div>
<% else -%>
<div class="note-comment-description" style="font-size: smaller; color: #999999"><%= t "note.description.#{comment.event}_at_by", :when => friendly_date(comment.created_at), :user => note_author(comment.author, :only_path => false) %></div>
<% end -%>
<div class="note-comment-text"><%= comment.body %></div>
</div>
<% end -%>
</div>

View file

@ -0,0 +1,18 @@
xml.wpt("lon" => note.lon, "lat" => note.lat) do
xml.desc do
xml.cdata! render(:partial => "description", :object => note, :formats => [ :html ])
end
xml.extension do
if note.status = "open"
xml.closed "0"
else
xml.closed "1"
end
xml.id note.id
xml.url note_url(note, :format => params[:format])
xml.comment_url comment_note_url(note, :format => params[:format])
xml.close_url close_note_url(note, :format => params[:format])
end
end

View file

@ -0,0 +1,33 @@
json.type "Feature"
json.geometry do
json.type "Point"
json.coordinates [ note.lon, note.lat ]
end
json.properties do
json.id note.id
json.url note_url(note, :format => params[:format])
json.comment_url comment_note_url(note, :format => params[:format])
json.close_url close_note_url(note, :format => params[:format])
json.date_created note.created_at
json.status note.status
json.closed_at note.closed_at if note.status == "closed"
json.comments(note.comments) do |comment|
json.date comment.created_at
if comment.author
json.uid comment.author.id
json.user comment.author.display_name
json.user_url user_url(:display_name => comment.author.display_name)
end
json.action comment.event
if comment.body
json.text comment.body.to_text
json.html comment.body.to_html
end
end
end

View file

@ -0,0 +1,24 @@
xml.item do
location = describe_location(note.lat, note.lon, 14, locale)
if note.status == "closed"
xml.title t('note.rss.closed', :place => location)
elsif note.comments.length > 1
xml.title t('note.rss.comment', :place => location)
else
xml.title t('note.rss.new', :place => location)
end
xml.link browse_note_url(note)
xml.guid note_url(note)
xml.description render(:partial => "description", :object => note, :formats => [ :html ])
if note.author
xml.author note.author_display_name
end
xml.pubDate note.updated_at.to_s(:rfc822)
xml.geo :lat, note.lat
xml.geo :long, note.lon
xml.georss :point, "#{note.lat} #{note.lon}"
end

View file

@ -0,0 +1,31 @@
xml.note("lon" => note.lon, "lat" => note.lat) do
xml.id note.id
xml.url note_url(note, :format => params[:format])
xml.comment_url comment_note_url(note, :format => params[:format])
xml.close_url close_note_url(note, :format => params[:format])
xml.date_created note.created_at
xml.status note.status
if note.status == "closed"
xml.date_closed note.closed_at
end
xml.comments do
note.comments.each do |comment|
xml.comment do
xml.date comment.created_at
if comment.author
xml.uid comment.author.id
xml.user comment.author.display_name
xml.user_url user_url(:display_name => comment.author.display_name)
end
if comment.body
xml.text comment.body.to_text
xml.html comment.body.to_html
end
end
end
end
end

View file

@ -0,0 +1,17 @@
<p>
<% if @page > 1 %>
<%= link_to t('changeset.changeset_paging_nav.previous'), params.merge({ :page => @page - 1 }) %>
<% else %>
<%= t('changeset.changeset_paging_nav.previous') %>
<% end %>
| <%= t('changeset.changeset_paging_nav.showing_page', :page => @page) %> |
<% if @notes.size < @page_size %>
<%= t('changeset.changeset_paging_nav.next') %>
<% else %>
<%= link_to t('changeset.changeset_paging_nav.next'), params.merge({ :page => @page + 1 }) %>
<% end %>
</p>

View file

@ -0,0 +1 @@
<%= link_to user.display_name, :controller => "user", :action => "view", :display_name => user.display_name %>

View file

@ -0,0 +1,52 @@
xml.instruct!
xml.rss("version" => "2.0",
"xmlns:geo" => "http://www.w3.org/2003/01/geo/wgs84_pos#",
"xmlns:georss" => "http://www.georss.org/georss") do
xml.channel do
xml.title t('note.rss.title')
xml.description t('note.rss.description_area', :min_lat => @min_lat, :min_lon => @min_lon, :max_lat => @max_lat, :max_lon => @max_lon )
xml.link url_for(:controller => "site", :action => "index", :only_path => false)
@comments.each do |comment|
location = describe_location(comment.note.lat, comment.note.lon, 14, locale)
xml.item do
if comment.event == "closed"
xml.title t('note.rss.closed', :place => location)
elsif comment.event == "commented"
xml.title t('note.rss.comment', :place => location)
elsif comment.event == "opened"
xml.title t('note.rss.new', :place => location)
else
xml.title "unknown event"
end
xml.link url_for(:controller => "browse", :action => "note", :id => comment.note.id, :only_path => false)
xml.guid url_for(:controller => "browse", :action => "note", :id => comment.note.id, :only_path => false)
description_text = ""
if comment.event == "commented" and not comment.nil?
description_text += "<b>Comment:</b><br>"
description_text += comment.body.to_html
description_text += "<br>"
end
description_text += "<b>Full note:</b><br>"
description_text += comment.note.flatten_comment("<br>", comment.created_at)
xml.description description_text
if comment.author
xml.author comment.author.display_name
end
xml.pubDate comment.created_at.to_s(:rfc822)
xml.geo :lat, comment.note.lat
xml.geo :long, comment.note.lon
xml.georss :point, "#{comment.note.lat} #{comment.note.lon}"
end
end
end
end

View file

@ -0,0 +1,7 @@
xml.instruct!
xml.gpx("version" => "1.1",
"xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance",
"xsi:schemaLocation" => "http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd") do
xml << render(:partial => "note", :collection => @notes)
end

View file

@ -0,0 +1,5 @@
json.type "FeatureCollection"
json.features(@notes) do |note|
json.ingest! render(:partial => "note", :object => note)
end

View file

@ -0,0 +1,13 @@
xml.instruct!
xml.rss("version" => "2.0",
"xmlns:geo" => "http://www.w3.org/2003/01/geo/wgs84_pos#",
"xmlns:georss" => "http://www.georss.org/georss") do
xml.channel do
xml.title t('note.rss.title')
xml.description t('note.rss.description_area', :min_lat => @min_lat, :min_lon => @min_lon, :max_lat => @max_lat, :max_lon => @max_lon )
xml.link url_for(:controller => "site", :action => "index", :only_path => false)
xml << render(:partial => "note", :collection => @notes)
end
end

View file

@ -0,0 +1,3 @@
xml.instruct!
xml << render(:partial => "note", :collection => @notes)

View file

@ -0,0 +1,35 @@
<% content_for :heading do %>
<h2><%= @heading %></h2>
<p><%= raw @description %></p>
<% end %>
<%= render :partial => 'notes_paging_nav' %>
<table class="note_list">
<tr>
<th></th>
<th><%= t'note.mine.id' %></th>
<th><%= t'note.mine.creator' %></th>
<th><%= t'note.mine.description' %></th>
<th><%= t'note.mine.created_at' %></th>
<th><%= t'note.mine.last_changed' %></th>
</tr>
<% @notes.each do |note| -%>
<tr<% if note.author != @user2 %> class="creator"<% end %>>
<td>
<% if note.status == "closed" %>
<%= image_tag("closed_note_marker.png", :alt => 'closed') %>
<% else %>
<%= image_tag("open_note_marker.png", :alt => 'open') %>
<% end %>
</td>
<td><%= link_to note.id.to_s, :controller => "browse", :action => "note", :id => note.id %></td>
<td><%= note_author(note.author) %></td>
<td><%= note.comments.first.body.to_html %></td>
<td><%= t 'note.mine.ago_html', :when => friendly_date(note.created_at) %></td>
<td><%= t 'note.mine.ago_html', :when => friendly_date(note.updated_at) %></td>
</tr>
<% end -%>
</table>
<%= render :partial => 'notes_paging_nav' %>

View file

@ -0,0 +1,7 @@
xml.instruct!
xml.gpx("version" => "1.1",
"xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance",
"xsi:schemaLocation" => "http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd") do
xml << render(:partial => "note", :object => @note)
end

View file

@ -0,0 +1 @@
json.ingest! render(:partial => "note", :object => @note)

View file

@ -0,0 +1,13 @@
xml.instruct!
xml.rss("version" => "2.0",
"xmlns:geo" => "http://www.w3.org/2003/01/geo/wgs84_pos#",
"xmlns:georss" => "http://www.georss.org/georss") do
xml.channel do
xml.title t('note.rss.title')
xml.description t('note.rss.description_item', :id => @note.id)
xml.link url_for(:controller => "site", :action => "index", :only_path => false)
xml << render(:partial => "note", :object => @note)
end
end

View file

@ -0,0 +1,3 @@
xml.instruct!
xml << render(:partial => "note", :object => @note)

View file

@ -0,0 +1,13 @@
<p><%= t 'notifier.note_comment_notification.greeting' %></p>
<% if @owner %>
<p><%= t 'notifier.note_comment_notification.your_note', :commenter => @commenter, :place => @place %></p>
<% else %>
<p><%= t 'notifier.note_comment_notification.commented_note', :commenter => @commenter, :place => @place %></p>
<% end %>
==
<%= @comment.to_html %>
==
<p><%= raw t 'notifier.note_comment_notification.details', :url => link_to(@noteurl, @noteurl) %></p>

View file

@ -0,0 +1,13 @@
<%= t 'notifier.note_comment_notification.greeting' %>
<% if @owner %>
<%= t 'notifier.note_comment_notification.your_note', :commenter => @commenter, :place => @place %>
<% else %>
<%= t 'notifier.note_comment_notification.commented_note', :commenter => @commenter, :place => @place %>
<% end %>
==
<%= @comment.to_text %>
==
<%= t 'notifier.note_comment_notification.details', :url => @noteurl %>

View file

@ -19,8 +19,16 @@
<div id="map">
<div id="permalink">
<a href="/" id="permalinkanchor" class="geolink llz layers object"><%= t 'site.index.permalink' %></a>
<a href="/" id="shortlinkanchor"><%= t 'site.index.shortlink' %></a>
<ul class="secondary-actions">
<li><a href="/" id="permalinkanchor" class="geolink llz layers object"><%= t 'site.index.permalink' %></a></li>
<li><a href="/" id="shortlinkanchor"><%= t 'site.index.shortlink' %></a></li>
<li><%= link_to t("site.index.createnote"), notes_url(:format => :json),
:id => "createnoteanchor",
:data => { :minzoom => 12 },
:title => "javascripts.site.createnote_tooltip",
:class => "geolink"
%></li>
</ul>
</div>
</div>
@ -38,3 +46,4 @@
</tr>
</table>
</div>

View file

@ -10,6 +10,9 @@
<%= link_to t('user.view.my edits'), :controller => 'changeset', :action => 'list', :display_name => @user.display_name %>
<span class='count-number'><%= number_with_delimiter(@user.changesets.size) %></span>
</li>
<li>
<%= link_to t('user.view.my notes'), :controller => 'notes', :action=> 'mine' %>
</li>
<li>
<%= link_to t('user.view.my traces'), :controller => 'trace', :action=>'mine' %>
<span class='count-number'><%= number_with_delimiter(@user.traces.size) %></span>
@ -49,6 +52,9 @@
<%= link_to t('user.view.edits'), :controller => 'changeset', :action => 'list', :display_name => @this_user.display_name %>
<span class='count-number'><%= number_with_delimiter(@this_user.changesets.size) %></span>
</li>
<li>
<%= link_to t('user.view.notes'), :controller => 'notes', :action=> 'mine' %>
</li>
<li>
<%= link_to t('user.view.traces'), :controller => 'trace', :action => 'list', :display_name => @this_user.display_name %>
<span class='count-number'><%= number_with_delimiter(@this_user.traces.size) %></span>

View file

@ -27,6 +27,8 @@ defaults: &defaults
max_number_of_nodes: 50000
# Maximum number of nodes that can be in a way (checked on save)
max_number_of_way_nodes: 2000
# The maximum area you're allowed to request notes from, in square degrees
max_note_request_area: 25
# Zoom level to use for postcode results from the geocoder
postcode_zoom: 15
# Zoom level to use for geonames results from the geocoder
@ -74,6 +76,8 @@ defaults: &defaults
default_editor: "potlatch2"
# OAuth consumer key for Potlatch 2
#potlatch2_key: ""
# OAuth consumer key for the web site
#oauth_key: ""
# Whether to require users to view the CTs before continuing to edit...
require_terms_seen: false
# Whether to require users to agree to the CTs before editing

View file

@ -23,6 +23,8 @@
translations:
- file: "app/assets/javascripts/i18n/translations.js"
only:
- "*.date"
- "*.time"
- "*.browse.start_rjs.*"
- "*.export.start_rjs.*"
- "*.javascripts.*"

View file

@ -0,0 +1,32 @@
#
# Make :formats work when rendering one partial from another
#
# Taken from https://github.com/rails/rails/pull/6626
#
module ActionView
class AbstractRenderer #:nodoc:
def prepend_formats(formats)
formats = Array(formats)
return if formats.empty?
@lookup_context.formats = formats | @lookup_context.formats
end
end
class PartialRenderer
def setup_with_formats(context, options, block)
prepend_formats(options[:formats])
setup_without_formats(context, options, block)
end
alias_method_chain :setup, :formats
end
class TemplateRenderer
def render_with_formats(context, options)
prepend_formats(options[:formats])
render_without_formats(context, options)
end
alias_method_chain :render, :formats
end
end

View file

@ -4,3 +4,4 @@
# Mime::Type.register "text/richtext", :rtf
# Mime::Type.register_alias "text/html", :iphone
Mime::Type.register "application/x-amf", :amf
Mime::Type.register "application/gpx+xml", :gpx

View file

@ -953,6 +953,22 @@ de:
history_disabled_tooltip: Reinzoomen um Änderungen für diesen Bereich anzuzeigen
history_tooltip: Änderungen für diesen Bereich anzeigen
history_zoom_alert: Du musst näher heranzoomen, um die Chronik zu sehen
osb:
Fixed Error: Behobener Fehler
Unresolved Error: Offener Fehler
Description: Beschreibung
Comment: Kommentar
Has been fixed: Der Fehler wurde bereits behoben. Es kann jedoch bis zu einigen Tagen dauern, bis die Kartenansicht aktualisiert wird.
Comment/Close: Kommentieren/Schließen
Nickname: Benutzername
Add comment: Kommentar hinzufügen
Mark as fixed: Als behoben markieren
Cancel: Abbrechen
Create OpenStreetBug: OpenStreetBug melden
Create bug: Bug anlegen
Bug description: Fehlerbeschreibung
Create: Anlegeeen
Permalink: Permalink
layouts:
community: Gemeinschaft
community_blogs: Blogs

View file

@ -121,6 +121,8 @@ en:
next_relation_tooltip: "Next relation"
prev_changeset_tooltip: "Previous changeset"
next_changeset_tooltip: "Next changeset"
prev_note_tooltip: "Previous note"
next_note_tooltip: "Next note"
changeset_details:
created_at: "Created at:"
closed_at: "Closed at:"
@ -157,11 +159,13 @@ en:
node: "View node on larger map"
way: "View way on larger map"
relation: "View relation on larger map"
note: "View note on larger map"
edit:
area: "Edit area"
node: "Edit node"
way: "Edit way"
relation: "Edit relation"
note: "Edit note"
node_details:
coordinates: "Coordinates:"
part_of: "Part of:"
@ -221,6 +225,7 @@ en:
download_xml: "Download XML"
view_history: "View history"
start_rjs:
notes_layer_name: "Browse Notes"
data_layer_name: "Browse Map Data"
data_frame_title: "Data"
zoom_or_select: "Zoom in or select an area of the map to view"
@ -280,6 +285,16 @@ en:
download_xml: "Download XML"
view_history: "View history"
edit: "Edit way"
note:
open_title: "Unresolved issue: %{note_name}"
closed_title: "Resolved issue: %{note_name}"
opened: "Opened:"
last_modified: "Last modified:"
closed: "Closed:"
at_html: "%{when} ago"
at_by_html: "%{when} ago by %{user}"
description: "Description:"
comments: "Comments:"
changeset:
changeset_paging_nav:
showing_page: "Page %{page}"
@ -1192,6 +1207,14 @@ en:
greeting: "Hi,"
hopefully_you: "Someone (possibly you) has asked for the password to be reset on this email address's openstreetmap.org account."
click_the_link: "If this is you, please click the link below to reset your password."
note_comment_notification:
anonymous: An anonymous user
subject_own: "[OpenStreetMap] %{commenter} has commented on one of your notes"
subject_other: "[OpenStreetMap] %{commenter} has commented on a note you are interested in"
greeting: "Hi,"
your_note: "%{commenter} has left a comment on one of your map notes near %{place}."
commented_note: "%{commenter} has left a comment on a map note you have commented on. The note is near %{place}."
details: "More details about the note can be found at %{url}."
message:
inbox:
title: "Inbox"
@ -1266,6 +1289,7 @@ en:
js_2: "OpenStreetMap uses JavaScript for its slippy map."
permalink: Permalink
shortlink: Shortlink
createnote: Add a note
license:
copyright: "Copyright OpenStreetMap and contributors, under an open license"
license_url: "http://openstreetmap.org/copyright"
@ -1676,6 +1700,7 @@ en:
new diary entry: new diary entry
my edits: my edits
my traces: my traces
my notes: my map notes
my settings: my settings
my comments: my comments
oauth settings: oauth settings
@ -1685,6 +1710,7 @@ en:
diary: diary
edits: edits
traces: traces
notes: map notes
remove as friend: unfriend
add as friend: add friend
mapper since: "Mapper since:"
@ -1955,6 +1981,33 @@ en:
back: "View all blocks"
revoker: "Revoker:"
needs_view: "The user needs to log in before this block will be cleared."
note:
description:
opened_at: "Created %{when} ago"
opened_at_by: "Created %{when} ago by %{user}"
commented_at: "Updated %{when} ago"
commented_at_by: "Updated %{when} ago by %{user}"
closed_at: "Resolved %{when} ago"
closed_at_by: "Resolved %{when} ago by %{user}"
reopened_at: "Reactivated %{when} ago"
reopened_at_by: "Reactivated %{when} ago by %{user}"
rss:
title: "OpenStreetMap Notes"
description_area: "A list of notes, reported, commented on or closed in your area [(%{min_lat}|%{min_lon}) -- (%{max_lat}|%{max_lon})]"
description_item: "An rss feed for note %{id}"
closed: "closed note (near %{place})"
new: "new note (near %{place})"
comment: "new comment (near %{place})"
mine:
title: "Notes submitted or commented on by %{user}"
heading: "%{user}'s notes"
subheading: "Notes submitted or commented on by %{user}"
id: "Id"
creator: "Creator"
description: "Description"
created_at: "Created at"
last_changed: "Last changed"
ago_html: "%{when} ago"
javascripts:
map:
base:
@ -1971,6 +2024,28 @@ en:
history_tooltip: View edits for this area
history_disabled_tooltip: Zoom in to view edits for this area
history_zoom_alert: You must zoom in to view edits for this area
createnote_tooltip: Add a note to the map
createnote_disabled_tooltip: Zoom in to add a note to the map
createnote_zoom_alert: You must zoom in to add a note to the map
notes:
new:
intro: "In order to improve the map the information you enter is shown to other mappers, so please be as descriptive and precise as possible when moving the marker to the correct position and entering your note below."
add: Add Note
show:
title: Note %{id}
anonymous_warning: This note includes comments from anonymous users which should be independently verified.
opened_by: "created by <a href='%{user_url}'>%{user}</a> at %{time}"
opened_by_anonymous: "created by anonymous at %{time}"
commented_by: "comment from <a href='%{user_url}'>%{user}</a> at %{time}"
commented_by_anonymous: "comment from anonymous at %{time}"
closed_by: "resolved by <a href='%{user_url}'>%{user}</a> at %{time}"
closed_by_anonymous: "resolved by anonymous at %{time}"
reopened_by: "reactivated by <a href='%{user_url}'>%{user}</a> at %{time}"
reopened_by_anonymous: "reactivated by anonymous at %{time}"
hide: Hide
resolve: Resolve
comment_and_resolve: Comment & Resolve
comment: Comment
redaction:
edit:
description: "Description"

View file

@ -6,6 +6,7 @@
OpenLayers/Map.js
OpenLayers/Control/ArgParser.js
OpenLayers/Control/Attribution.js
OpenLayers/Control/DragFeature.js
OpenLayers/Control/DrawFeature.js
OpenLayers/Control/LayerSwitcher.js
OpenLayers/Control/Navigation.js
@ -25,12 +26,16 @@ OpenLayers/Tile.js
OpenLayers/Tile/Image.js
OpenLayers/Feature/Vector.js
OpenLayers/Strategy/Fixed.js
OpenLayers/Strategy/BBOX.js
OpenLayers/Protocol/HTTP.js
OpenLayers/Format/QueryStringFilter.js
OpenLayers/Format/GeoJSON.js
OpenLayers/Format/OSM.js
OpenLayers/Geometry/Point.js
OpenLayers/Geometry/LinearRing.js
OpenLayers/Handler/Point.js
OpenLayers/Handler/RegularPolygon.js
OpenLayers/Protocol/HTTP.js
OpenLayers/Renderer.js
OpenLayers/Renderer/Canvas.js
OpenLayers/Renderer/SVG.js

View file

@ -75,11 +75,31 @@ OpenStreetMap::Application.routes.draw do
match 'api/0.6/gpx/:id/data' => 'trace#api_data', :via => :get
# AMF (ActionScript) API
match 'api/0.6/amf/read' => 'amf#amf_read', :via => :post
match 'api/0.6/amf/write' => 'amf#amf_write', :via => :post
match 'api/0.6/swf/trackpoints' => 'swf#trackpoints', :via => :get
# Map notes API
scope "api/0.6" do
resources :notes, :except => [ :new, :edit, :update ], :constraints => { :id => /\d+/ }, :defaults => { :format => "xml" } do
collection do
get 'search'
get 'feed', :defaults => { :format => "rss" }
end
member do
post 'comment'
post 'close'
end
end
match 'notes/addPOIexec' => 'notes#create', :via => :post
match 'notes/closePOIexec' => 'notes#close', :via => :post
match 'notes/editPOIexec' => 'notes#comment', :via => :post
match 'notes/getGPX' => 'notes#index', :via => :get, :format => "gpx"
match 'notes/getRSSfeed' => 'notes#feed', :via => :get, :format => "rss"
end
# Data browsing
match '/browse/start' => 'browse#start', :via => :get
match '/browse/way/:id' => 'browse#way', :via => :get, :id => /\d+/
@ -89,8 +109,10 @@ OpenStreetMap::Application.routes.draw do
match '/browse/relation/:id' => 'browse#relation', :via => :get, :id => /\d+/
match '/browse/relation/:id/history' => 'browse#relation_history', :via => :get, :id => /\d+/
match '/browse/changeset/:id' => 'browse#changeset', :via => :get, :as => :changeset, :id => /\d+/
match '/browse/note/:id' => 'browse#note', :via => :get, :id => /\d+/, :as => "browse_note"
match '/user/:display_name/edits' => 'changeset#list', :via => :get
match '/user/:display_name/edits/feed' => 'changeset#feed', :via => :get, :format => :atom
match '/user/:display_name/notes' => 'notes#mine', :via => :get
match '/browse/friends' => 'changeset#list', :via => :get, :friends => true, :as => "friend_changesets"
match '/browse/nearby' => 'changeset#list', :via => :get, :nearby => true, :as => "nearby_changesets"
match '/browse/changesets' => 'changeset#list', :via => :get

View file

@ -0,0 +1,33 @@
require 'migrate'
class AddMapBugTables < ActiveRecord::Migration
def self.up
create_enumeration :map_bug_status_enum, ["open", "closed", "hidden"]
create_table :map_bugs do |t|
t.column :id, :bigint, :null => false
t.integer :latitude, :null => false
t.integer :longitude, :null => false
t.column :tile, :bigint, :null => false
t.datetime :last_changed, :null => false
t.datetime :date_created, :null => false
t.string :nearby_place
t.string :text
t.column :status, :map_bug_status_enum, :null => false
end
add_index :map_bugs, [:tile, :status], :name => "map_bugs_tile_idx"
add_index :map_bugs, [:last_changed], :name => "map_bugs_changed_idx"
add_index :map_bugs, [:date_created], :name => "map_bugs_created_idx"
end
def self.down
remove_index :map_bugs, :name => "map_bugs_tile_idx"
remove_index :map_bugs, :name => "map_bugs_changed_idx"
remove_index :map_bugs, :name => "map_bugs_created_idx"
drop_table :map_bugs
drop_enumeration :map_bug_status_enum
end
end

View file

@ -0,0 +1,34 @@
require 'migrate'
class RefactorMapBugTables < ActiveRecord::Migration
def self.up
create_table :map_bug_comment do |t|
t.column :id, :bigint, :null => false
t.column :bug_id, :bigint, :null => false
t.boolean :visible, :null => false
t.datetime :date_created, :null => false
t.string :commenter_name
t.string :commenter_ip
t.column :commenter_id, :bigint
t.string :comment
end
remove_column :map_bugs, :text
add_index :map_bug_comment, [:bug_id], :name => "map_bug_comment_id_idx"
add_foreign_key :map_bug_comment, [:bug_id], :map_bugs, [:id]
add_foreign_key :map_bug_comment, [:commenter_id], :users, [:id]
end
def self.down
remove_foreign_key :map_bug_comment, [:commenter_id]
remove_foreign_key :map_bug_comment, [:bug_id]
remove_index :map_bugs, :name => "map_bug_comment_id_idx"
add_column :map_bugs, :text, :string
drop_table :map_bug_comment
end
end

View file

@ -0,0 +1,11 @@
require 'migrate'
class ChangeMapBugCommentType < ActiveRecord::Migration
def self.up
change_column :map_bug_comment, :comment, :text
end
def self.down
change_column :map_bug_comment, :comment, :string
end
end

View file

@ -0,0 +1,11 @@
require 'migrate'
class AddDateClosed < ActiveRecord::Migration
def self.up
add_column :map_bugs, :date_closed, :timestamp
end
def self.down
remove_column :map_bugs, :date_closed
end
end

View file

@ -0,0 +1,15 @@
require 'migrate'
class AddMapBugCommentEvent < ActiveRecord::Migration
def self.up
create_enumeration :map_bug_event_enum, ["opened", "closed", "reopened", "commented", "hidden"]
add_column :map_bug_comment, :event, :map_bug_event_enum
end
def self.down
remove_column :map_bug_comment, :event
drop_enumeration :map_bug_event_enum
end
end

View file

@ -0,0 +1,25 @@
class CleanupBugTables < ActiveRecord::Migration
def self.up
rename_column :map_bugs, :date_created, :created_at
rename_column :map_bugs, :last_changed, :updated_at
rename_column :map_bugs, :date_closed, :closed_at
rename_column :map_bug_comment, :date_created, :created_at
rename_column :map_bug_comment, :commenter_name, :author_name
rename_column :map_bug_comment, :commenter_ip, :author_ip
rename_column :map_bug_comment, :commenter_id, :author_id
rename_column :map_bug_comment, :comment, :body
end
def self.down
rename_column :map_bug_comment, :body, :comment
rename_column :map_bug_comment, :author_id, :commenter_id
rename_column :map_bug_comment, :author_ip, :commenter_ip
rename_column :map_bug_comment, :author_name, :commenter_name
rename_column :map_bug_comment, :created_at, :date_created
rename_column :map_bugs, :closed_at, :date_closed
rename_column :map_bugs, :updated_at, :last_changed
rename_column :map_bugs, :created_at, :date_created
end
end

View file

@ -0,0 +1,51 @@
require 'migrate'
class RenameBugsToNotes < ActiveRecord::Migration
def self.up
rename_enumeration "map_bug_status_enum", "note_status_enum"
rename_enumeration "map_bug_event_enum", "note_event_enum"
rename_table :map_bugs, :notes
rename_index :notes, "map_bugs_pkey", "notes_pkey"
rename_index :notes, "map_bugs_changed_idx", "notes_updated_at_idx"
rename_index :notes, "map_bugs_created_idx", "notes_created_at_idx"
rename_index :notes, "map_bugs_tile_idx", "notes_tile_status_idx"
remove_foreign_key :map_bug_comment, [:bug_id], :map_bugs, [:id]
rename_column :map_bug_comment, :author_id, :commenter_id
remove_foreign_key :map_bug_comment, [:commenter_id], :users, [:id]
rename_column :map_bug_comment, :commenter_id, :author_id
rename_table :map_bug_comment, :note_comments
rename_column :note_comments, :bug_id, :note_id
rename_index :note_comments, "map_bug_comment_pkey", "note_comments_pkey"
rename_index :note_comments, "map_bug_comment_id_idx", "note_comments_note_id_idx"
add_foreign_key :note_comments, [:note_id], :notes, [:id]
add_foreign_key :note_comments, [:author_id], :users, [:id]
end
def self.down
remove_foreign_key :note_comments, [:author_id], :users, [:id]
remove_foreign_key :note_comments, [:note_id], :notes, [:id]
rename_index :note_comments, "note_comments_note_id_idx", "map_bug_comment_id_idx"
rename_index :notes, "note_comments_pkey", "map_bug_comment_pkey"
rename_column :note_comments, :note_id, :bug_id
rename_table :note_comments, :map_bug_comment
rename_column :map_bug_comment, :author_id, :commenter_id
add_foreign_key :map_bug_comment, [:commenter_id], :users, [:id]
rename_column :map_bug_comment, :commenter_id, :author_id
add_foreign_key :map_bug_comment, [:bug_id], :notes, [:id]
rename_index :notes, "notes_tile_status_idx", "map_bugs_tile_idx"
rename_index :notes, "notes_created_at_idx", "map_bugs_created_idx"
rename_index :notes, "notes_updated_at_idx", "map_bugs_changed_idx"
rename_index :notes, "notes_pkey", "map_bugs_pkey"
rename_table :notes, :map_bugs
rename_enumeration "note_event_enum", "map_bug_event_enum"
rename_enumeration "note_status_enum", "map_bug_status_enum"
end
end

View file

@ -0,0 +1,9 @@
class DropNearbyPlaceFromNotes < ActiveRecord::Migration
def up
remove_column :notes, :nearby_place
end
def down
add_column :notes, :nearby_place, :string
end
end

View file

@ -0,0 +1,9 @@
class RemoveAuthorNameFromNoteComment < ActiveRecord::Migration
def up
remove_column :note_comments, :author_name
end
def down
add_column :note_comments, :author_name, :string
end
end

View file

@ -0,0 +1,9 @@
class ChangeNoteAddressToInet < ActiveRecord::Migration
def up
execute "ALTER TABLE note_comments ALTER COLUMN author_ip TYPE inet USING CAST(author_ip AS inet)"
end
def down
change_column :note_comments, :author_ip, :string
end
end

View file

@ -0,0 +1,11 @@
class AddWriteNotesPermission < ActiveRecord::Migration
def up
add_column :oauth_tokens, :allow_write_notes, :boolean, :null => false, :default => false
add_column :client_applications, :allow_write_notes, :boolean, :null => false, :default => false
end
def down
remove_column :client_applications, :allow_write_notes
remove_column :oauth_tokens, :allow_write_notes
end
end

View file

@ -61,6 +61,30 @@ CREATE TYPE gpx_visibility_enum AS ENUM (
);
--
-- Name: note_event_enum; Type: TYPE; Schema: public; Owner: -
--
CREATE TYPE note_event_enum AS ENUM (
'opened',
'closed',
'reopened',
'commented',
'hidden'
);
--
-- Name: note_status_enum; Type: TYPE; Schema: public; Owner: -
--
CREATE TYPE note_status_enum AS ENUM (
'open',
'closed',
'hidden'
);
--
-- Name: nwr_enum; Type: TYPE; Schema: public; Owner: -
--
@ -101,7 +125,7 @@ CREATE TYPE user_status_enum AS ENUM (
CREATE FUNCTION maptile_for_point(bigint, bigint, integer) RETURNS integer
LANGUAGE c STRICT
AS '/srv/www/master.osm.compton.nu/db/functions/libpgosm.so', 'maptile_for_point';
AS '/srv/www/notes.osm.compton.nu/db/functions/libpgosm.so', 'maptile_for_point';
--
@ -110,7 +134,7 @@ CREATE FUNCTION maptile_for_point(bigint, bigint, integer) RETURNS integer
CREATE FUNCTION tile_for_point(integer, integer) RETURNS bigint
LANGUAGE c STRICT
AS '/srv/www/master.osm.compton.nu/db/functions/libpgosm.so', 'tile_for_point';
AS '/srv/www/notes.osm.compton.nu/db/functions/libpgosm.so', 'tile_for_point';
--
@ -119,7 +143,7 @@ CREATE FUNCTION tile_for_point(integer, integer) RETURNS bigint
CREATE FUNCTION xid_to_int4(xid) RETURNS integer
LANGUAGE c IMMUTABLE STRICT
AS '/srv/www/master.osm.compton.nu/db/functions/libpgosm.so', 'xid_to_int4';
AS '/srv/www/notes.osm.compton.nu/db/functions/libpgosm.so', 'xid_to_int4';
SET default_tablespace = '';
@ -218,14 +242,15 @@ CREATE TABLE client_applications (
key character varying(50),
secret character varying(50),
user_id integer,
created_at timestamp without time zone,
updated_at timestamp without time zone,
created_at timestamp without time zone NOT NULL,
updated_at timestamp without time zone NOT NULL,
allow_read_prefs boolean DEFAULT false NOT NULL,
allow_write_prefs boolean DEFAULT false NOT NULL,
allow_write_diary boolean DEFAULT false NOT NULL,
allow_write_api boolean DEFAULT false NOT NULL,
allow_read_gpx boolean DEFAULT false NOT NULL,
allow_write_gpx boolean DEFAULT false NOT NULL
allow_write_gpx boolean DEFAULT false NOT NULL,
allow_write_notes boolean DEFAULT false NOT NULL
);
@ -700,6 +725,76 @@ CREATE TABLE nodes (
);
--
-- Name: note_comments; Type: TABLE; Schema: public; Owner: -; Tablespace:
--
CREATE TABLE note_comments (
id integer NOT NULL,
note_id bigint NOT NULL,
visible boolean NOT NULL,
created_at timestamp without time zone NOT NULL,
author_ip inet,
author_id bigint,
body text,
event note_event_enum
);
--
-- Name: note_comments_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE note_comments_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: note_comments_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE note_comments_id_seq OWNED BY note_comments.id;
--
-- Name: notes; Type: TABLE; Schema: public; Owner: -; Tablespace:
--
CREATE TABLE notes (
id integer NOT NULL,
latitude integer NOT NULL,
longitude integer NOT NULL,
tile bigint NOT NULL,
updated_at timestamp without time zone NOT NULL,
created_at timestamp without time zone NOT NULL,
status note_status_enum NOT NULL,
closed_at timestamp without time zone
);
--
-- Name: notes_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE notes_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: notes_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE notes_id_seq OWNED BY notes.id;
--
-- Name: oauth_nonces; Type: TABLE; Schema: public; Owner: -; Tablespace:
--
@ -708,8 +803,8 @@ CREATE TABLE oauth_nonces (
id integer NOT NULL,
nonce character varying(255),
"timestamp" integer,
created_at timestamp without time zone,
updated_at timestamp without time zone
created_at timestamp without time zone NOT NULL,
updated_at timestamp without time zone NOT NULL
);
@ -745,8 +840,8 @@ CREATE TABLE oauth_tokens (
secret character varying(50),
authorized_at timestamp without time zone,
invalidated_at timestamp without time zone,
created_at timestamp without time zone,
updated_at timestamp without time zone,
created_at timestamp without time zone NOT NULL,
updated_at timestamp without time zone NOT NULL,
allow_read_prefs boolean DEFAULT false NOT NULL,
allow_write_prefs boolean DEFAULT false NOT NULL,
allow_write_diary boolean DEFAULT false NOT NULL,
@ -756,7 +851,8 @@ CREATE TABLE oauth_tokens (
callback_url character varying(255),
verifier character varying(20),
scope character varying(255),
valid_to timestamp without time zone
valid_to timestamp without time zone,
allow_write_notes boolean DEFAULT false NOT NULL
);
@ -874,8 +970,8 @@ CREATE TABLE user_blocks (
ends_at timestamp without time zone NOT NULL,
needs_view boolean DEFAULT false NOT NULL,
revoker_id bigint,
created_at timestamp without time zone,
updated_at timestamp without time zone,
created_at timestamp without time zone NOT NULL,
updated_at timestamp without time zone NOT NULL,
reason_format format_enum DEFAULT 'html'::format_enum NOT NULL
);
@ -917,8 +1013,8 @@ CREATE TABLE user_preferences (
CREATE TABLE user_roles (
id integer NOT NULL,
user_id bigint NOT NULL,
created_at timestamp without time zone,
updated_at timestamp without time zone,
created_at timestamp without time zone NOT NULL,
updated_at timestamp without time zone NOT NULL,
role user_role_enum NOT NULL,
granter_id bigint NOT NULL
);
@ -1000,9 +1096,9 @@ CREATE TABLE users (
status user_status_enum DEFAULT 'pending'::user_status_enum NOT NULL,
terms_agreed timestamp without time zone,
consider_pd boolean DEFAULT false NOT NULL,
openid_url character varying(255),
preferred_editor character varying(255),
terms_seen boolean DEFAULT false NOT NULL,
openid_url character varying(255),
description_format format_enum DEFAULT 'html'::format_enum NOT NULL,
image_fingerprint character varying(255),
changesets_count integer DEFAULT 0 NOT NULL,
@ -1160,6 +1256,20 @@ ALTER TABLE ONLY gpx_files ALTER COLUMN id SET DEFAULT nextval('gpx_files_id_seq
ALTER TABLE ONLY messages ALTER COLUMN id SET DEFAULT nextval('messages_id_seq'::regclass);
--
-- Name: id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY note_comments ALTER COLUMN id SET DEFAULT nextval('note_comments_id_seq'::regclass);
--
-- Name: id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY notes ALTER COLUMN id SET DEFAULT nextval('notes_id_seq'::regclass);
--
-- Name: id; Type: DEFAULT; Schema: public; Owner: -
--
@ -1377,6 +1487,22 @@ ALTER TABLE ONLY nodes
ADD CONSTRAINT nodes_pkey PRIMARY KEY (node_id, version);
--
-- Name: note_comments_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace:
--
ALTER TABLE ONLY note_comments
ADD CONSTRAINT note_comments_pkey PRIMARY KEY (id);
--
-- Name: notes_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace:
--
ALTER TABLE ONLY notes
ADD CONSTRAINT notes_pkey PRIMARY KEY (id);
--
-- Name: oauth_nonces_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace:
--
@ -1727,6 +1853,34 @@ CREATE INDEX nodes_tile_idx ON nodes USING btree (tile);
CREATE INDEX nodes_timestamp_idx ON nodes USING btree ("timestamp");
--
-- Name: note_comments_note_id_idx; Type: INDEX; Schema: public; Owner: -; Tablespace:
--
CREATE INDEX note_comments_note_id_idx ON note_comments USING btree (note_id);
--
-- Name: notes_created_at_idx; Type: INDEX; Schema: public; Owner: -; Tablespace:
--
CREATE INDEX notes_created_at_idx ON notes USING btree (created_at);
--
-- Name: notes_tile_status_idx; Type: INDEX; Schema: public; Owner: -; Tablespace:
--
CREATE INDEX notes_tile_status_idx ON notes USING btree (tile, status);
--
-- Name: notes_updated_at_idx; Type: INDEX; Schema: public; Owner: -; Tablespace:
--
CREATE INDEX notes_updated_at_idx ON notes USING btree (updated_at);
--
-- Name: points_gpxid_idx; Type: INDEX; Schema: public; Owner: -; Tablespace:
--
@ -2061,6 +2215,22 @@ ALTER TABLE ONLY nodes
ADD CONSTRAINT nodes_redaction_id_fkey FOREIGN KEY (redaction_id) REFERENCES redactions(id);
--
-- Name: note_comments_author_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY note_comments
ADD CONSTRAINT note_comments_author_id_fkey FOREIGN KEY (author_id) REFERENCES users(id);
--
-- Name: note_comments_note_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY note_comments
ADD CONSTRAINT note_comments_note_id_fkey FOREIGN KEY (note_id) REFERENCES notes(id);
--
-- Name: oauth_tokens_client_application_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@ -2245,6 +2415,10 @@ INSERT INTO schema_migrations (version) VALUES ('20101114011429');
INSERT INTO schema_migrations (version) VALUES ('20110322001319');
INSERT INTO schema_migrations (version) VALUES ('20110508145337');
INSERT INTO schema_migrations (version) VALUES ('20110521142405');
INSERT INTO schema_migrations (version) VALUES ('20110925112722');
INSERT INTO schema_migrations (version) VALUES ('20111116184519');
@ -2273,6 +2447,14 @@ INSERT INTO schema_migrations (version) VALUES ('20121005195010');
INSERT INTO schema_migrations (version) VALUES ('20121012044047');
INSERT INTO schema_migrations (version) VALUES ('20121119165817');
INSERT INTO schema_migrations (version) VALUES ('20121202155309');
INSERT INTO schema_migrations (version) VALUES ('20121203124841');
INSERT INTO schema_migrations (version) VALUES ('20130328184137');
INSERT INTO schema_migrations (version) VALUES ('21');
INSERT INTO schema_migrations (version) VALUES ('22');
@ -2343,6 +2525,16 @@ INSERT INTO schema_migrations (version) VALUES ('51');
INSERT INTO schema_migrations (version) VALUES ('52');
INSERT INTO schema_migrations (version) VALUES ('53');
INSERT INTO schema_migrations (version) VALUES ('54');
INSERT INTO schema_migrations (version) VALUES ('55');
INSERT INTO schema_migrations (version) VALUES ('56');
INSERT INTO schema_migrations (version) VALUES ('57');
INSERT INTO schema_migrations (version) VALUES ('6');
INSERT INTO schema_migrations (version) VALUES ('7');

View file

@ -37,6 +37,13 @@ public
from_bbox_array(bbox_array)
end
def self.from_lrbt_params(params)
if params[:l] and params[:b] and params[:t] and params[:t]
bbox_array = [params[:l], params[:b], params[:r], params[:t]]
end
from_bbox_array(bbox_array)
end
def expand!(bbox, margin = 0)
update!(bbox) unless complete?
# only try to expand the bbox if there is a value for every coordinate
@ -71,10 +78,10 @@ public
self
end
def check_size
def check_size(max_area = MAX_REQUEST_AREA)
# check the bbox isn't too large
if area > MAX_REQUEST_AREA
raise OSM::APIBadBoundingBox.new("The maximum bbox size is " + MAX_REQUEST_AREA.to_s +
if area > max_area
raise OSM::APIBadBoundingBox.new("The maximum bbox size is " + max_area.to_s +
", and your request was too large. Either request a smaller area, or use planet.osm")
end
self

View file

@ -45,4 +45,3 @@ private
180/Math::PI * Math.log(Math.tan(Math::PI/4+a*(Math::PI/180)/2))
end
end

View file

@ -87,18 +87,23 @@ module ActiveRecord
@enumerations ||= Hash.new
end
def create_enumeration (enumeration_name, values)
def create_enumeration(enumeration_name, values)
enumerations[enumeration_name] = values
execute "create type #{enumeration_name} as enum ('#{values.join '\',\''}')"
execute "CREATE TYPE #{enumeration_name} AS ENUM ('#{values.join '\',\''}')"
end
def drop_enumeration (enumeration_name)
execute "drop type #{enumeration_name}"
def drop_enumeration(enumeration_name)
execute "DROP TYPE #{enumeration_name}"
enumerations.delete(enumeration_name)
end
def rename_enumeration(old_name, new_name)
execute "ALTER TYPE #{quote_table_name(old_name)} RENAME TO #{quote_table_name(new_name)}"
end
def alter_primary_key(table_name, new_columns)
execute "alter table #{table_name} drop constraint #{table_name}_pkey; alter table #{table_name} add primary key (#{new_columns.join(',')})"
execute "ALTER TABLE #{table_name} DROP CONSTRAINT #{table_name}_pkey"
execute "ALTER TABLE #{table_name} ADD PRIMARY KEY (#{new_columns.join(',')})"
end
def interval_constant(interval)
@ -125,6 +130,10 @@ module ActiveRecord
execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} USING #{index_method} (#{quoted_column_names})"
end
def rename_index(table_name, old_name, new_name)
execute "ALTER INDEX #{quote_table_name(old_name)} RENAME TO #{quote_table_name(new_name)}"
end
end
end
end

26
lib/nominatim.rb Normal file
View file

@ -0,0 +1,26 @@
module Nominatim
extend ActionView::Helpers::NumberHelper
def self.describe_location(lat, lon, zoom = nil, language = nil)
zoom = zoom || 14
language = language || request.user_preferred_languages.join(',')
Rails.cache.fetch "/nominatim/location/#{lat}/#{lon}/#{zoom}/#{language}" do
url = "http://nominatim.openstreetmap.org/reverse?lat=#{lat}&lon=#{lon}&zoom=#{zoom}&accept-language=#{language}"
begin
response = OSM::Timer.timeout(4) do
REXML::Document.new(Net::HTTP.get(URI.parse(url)))
end
rescue Exception
response = nil
end
if response and result = response.get_text("reversegeocode/result")
result.to_s
else
"#{number_with_precision(lat, :precision => 3)}, #{number_with_precision(lon, :precision => 3)}"
end
end
end
end

View file

@ -281,6 +281,23 @@ module OSM
end
end
# Raised when the note provided is already closed
class APINoteAlreadyClosedError < APIError
def initialize(note)
@note = note
end
attr_reader :note
def status
:conflict
end
def to_s
"The note #{@note.id} was closed at #{@note.closed_at}"
end
end
# Helper methods for going to/from mercator and lat/lng.
class Mercator
include Math

107
test/fixtures/note_comments.yml vendored Normal file
View file

@ -0,0 +1,107 @@
t1:
id: 1
note_id: 1
visible: true
created_at: 2007-01-01 00:00:00
author_ip: '192.168.1.1'
body: 'This is the initial description of the note 1'
t2:
id: 2
note_id: 2
visible: true
created_at: 2007-01-01 00:00:00
author_ip: '192.168.1.1'
body: 'This is the initial description of the note 2'
t3:
id: 3
note_id: 2
visible: true
created_at: 2007-02-01 00:00:00
author_ip: '192.168.1.1'
body: 'This is an additional comment for note 2'
t4:
id: 4
note_id: 3
visible: true
created_at: 2007-01-01 00:00:00
author_ip: '192.168.1.1'
body: 'This is the initial comment for note 3'
t5:
id: 5
note_id: 4
visible: true
created_at: 2007-01-01 00:00:00
author_ip: '192.168.1.1'
body: 'Spam for note 4'
t6:
id: 6
note_id: 5
visible: true
created_at: 2007-01-01 00:00:00
author_ip: '192.168.1.1'
body: 'Valid comment for note 5'
t7:
id: 7
note_id: 5
visible: false
created_at: 2007-02-01 00:00:00
author_ip: '192.168.1.1'
body: 'Spam for note 5'
t8:
id: 8
note_id: 5
visible: true
created_at: 2007-02-01 00:00:00
author_ip: '192.168.1.1'
body: 'Another valid comment for note 5'
t9:
id: 9
note_id: 6
visible: true
created_at: 2007-01-01 00:00:00
event: opened
author_id: 1
body: 'This is a note with from a logged-in user'
t10:
id: 10
note_id: 6
visible: true
created_at: 2007-02-01 00:00:00
event: commented
author_id: 4
body: 'A comment from another logged-in user'
t11:
id: 11
note_id: 7
visible: true
event: opened
created_at: 2007-01-01 00:00:00
author_ip: '192.168.1.1'
body: 'Initial note description'
t12:
id: 12
note_id: 7
visible: true
event: commented
created_at: 2007-02-01 00:00:00
author_ip: '192.168.1.1'
body: 'A comment description'
t13:
id: 13
note_id: 7
visible: true
event: closed
created_at: 2007-03-01 00:00:00
author_id: 4

67
test/fixtures/notes.yml vendored Normal file
View file

@ -0,0 +1,67 @@
# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
<% SCALE = 10000000 unless defined?(SCALE) %>
open_note:
id: 1
latitude: <%= 1*SCALE %>
longitude: <%= 1*SCALE %>
status: open
tile: <%= QuadTile.tile_for_point(1,1) %>
created_at: 2007-01-01 00:00:00
updated_at: 2007-01-01 00:00:00
open_note_with_comment:
id: 2
latitude: <%= 1.1*SCALE %>
longitude: <%= 1.1*SCALE %>
status: open
tile: <%= QuadTile.tile_for_point(1.1,1.1) %>
created_at: 2007-01-01 00:00:00
updated_at: 2007-02-01 00:00:00
closed_note_with_comment:
id: 3
latitude: <%= 1.2*SCALE %>
longitude: <%= 1.2*SCALE %>
status: closed
tile: <%= QuadTile.tile_for_point(1.2,1.2) %>
created_at: 2007-01-01 00:00:00
updated_at: 2007-03-01 00:00:00
closed_at: 2007-03-01 00:00:00
hidden_note_with_comment:
id: 4
latitude: <%= 1.3*SCALE %>
longitude: <%= 1.3*SCALE %>
status: hidden
tile: <%= QuadTile.tile_for_point(1.3,1.3) %>
created_at: 2007-01-01 00:00:00
updated_at: 2007-03-01 00:00:00
note_with_hidden_comment:
id: 5
latitude: <%= 1.4*SCALE %>
longitude: <%= 1.4*SCALE %>
status: open
tile: <%= QuadTile.tile_for_point(1.4,1.4) %>
created_at: 2007-01-01 00:00:00
updated_at: 2007-03-01 00:00:00
note_with_comments_by_users:
id: 6
latitude: <%= 1.5*SCALE %>
longitude: <%= 1.5*SCALE %>
status: open
tile: <%= QuadTile.tile_for_point(1.5,1.5) %>
created_at: 2007-01-01 00:00:00
updated_at: 2007-03-01 00:00:00
note_closed_by_user:
id: 7
latitude: <%= 1.6*SCALE %>
longitude: <%= 1.6*SCALE %>
status: closed
tile: <%= QuadTile.tile_for_point(1.6,1.6) %>
created_at: 2007-01-01 00:00:00
updated_at: 2007-03-01 00:00:00
closed_at: 2007-03-01 00:00:00

View file

@ -39,6 +39,10 @@ class BrowseControllerTest < ActionController::TestCase
{ :path => "/browse/changeset/1", :method => :get },
{ :controller => "browse", :action => "changeset", :id => "1" }
)
assert_routing(
{ :path => "/browse/note/1", :method => :get },
{ :controller => "browse", :action => "note", :id => "1" }
)
end
def test_start

View file

@ -0,0 +1,523 @@
require File.dirname(__FILE__) + '/../test_helper'
class NotesControllerTest < ActionController::TestCase
fixtures :users, :notes, :note_comments
##
# test all routes which lead to this controller
def test_routes
assert_routing(
{ :path => "/api/0.6/notes", :method => :post },
{ :controller => "notes", :action => "create", :format => "xml" }
)
assert_routing(
{ :path => "/api/0.6/notes/1", :method => :get },
{ :controller => "notes", :action => "show", :id => "1", :format => "xml" }
)
assert_recognizes(
{ :controller => "notes", :action => "show", :id => "1", :format => "xml" },
{ :path => "/api/0.6/notes/1.xml", :method => :get }
)
assert_routing(
{ :path => "/api/0.6/notes/1.rss", :method => :get },
{ :controller => "notes", :action => "show", :id => "1", :format => "rss" }
)
assert_routing(
{ :path => "/api/0.6/notes/1.json", :method => :get },
{ :controller => "notes", :action => "show", :id => "1", :format => "json" }
)
assert_routing(
{ :path => "/api/0.6/notes/1.gpx", :method => :get },
{ :controller => "notes", :action => "show", :id => "1", :format => "gpx" }
)
assert_routing(
{ :path => "/api/0.6/notes/1/comment", :method => :post },
{ :controller => "notes", :action => "comment", :id => "1", :format => "xml" }
)
assert_routing(
{ :path => "/api/0.6/notes/1/close", :method => :post },
{ :controller => "notes", :action => "close", :id => "1", :format => "xml" }
)
assert_routing(
{ :path => "/api/0.6/notes/1", :method => :delete },
{ :controller => "notes", :action => "destroy", :id => "1", :format => "xml" }
)
assert_routing(
{ :path => "/api/0.6/notes", :method => :get },
{ :controller => "notes", :action => "index", :format => "xml" }
)
assert_recognizes(
{ :controller => "notes", :action => "index", :format => "xml" },
{ :path => "/api/0.6/notes.xml", :method => :get }
)
assert_routing(
{ :path => "/api/0.6/notes.rss", :method => :get },
{ :controller => "notes", :action => "index", :format => "rss" }
)
assert_routing(
{ :path => "/api/0.6/notes.json", :method => :get },
{ :controller => "notes", :action => "index", :format => "json" }
)
assert_routing(
{ :path => "/api/0.6/notes.gpx", :method => :get },
{ :controller => "notes", :action => "index", :format => "gpx" }
)
assert_routing(
{ :path => "/api/0.6/notes/search", :method => :get },
{ :controller => "notes", :action => "search", :format => "xml" }
)
assert_recognizes(
{ :controller => "notes", :action => "search", :format => "xml" },
{ :path => "/api/0.6/notes/search.xml", :method => :get }
)
assert_routing(
{ :path => "/api/0.6/notes/search.rss", :method => :get },
{ :controller => "notes", :action => "search", :format => "rss" }
)
assert_routing(
{ :path => "/api/0.6/notes/search.json", :method => :get },
{ :controller => "notes", :action => "search", :format => "json" }
)
assert_routing(
{ :path => "/api/0.6/notes/search.gpx", :method => :get },
{ :controller => "notes", :action => "search", :format => "gpx" }
)
assert_routing(
{ :path => "/api/0.6/notes/feed", :method => :get },
{ :controller => "notes", :action => "feed", :format => "rss" }
)
assert_recognizes(
{ :controller => "notes", :action => "create" },
{ :path => "/api/0.6/notes/addPOIexec", :method => :post }
)
assert_recognizes(
{ :controller => "notes", :action => "close" },
{ :path => "/api/0.6/notes/closePOIexec", :method => :post }
)
assert_recognizes(
{ :controller => "notes", :action => "comment" },
{ :path => "/api/0.6/notes/editPOIexec", :method => :post }
)
assert_recognizes(
{ :controller => "notes", :action => "index", :format => "gpx" },
{ :path => "/api/0.6/notes/getGPX", :method => :get }
)
assert_recognizes(
{ :controller => "notes", :action => "feed", :format => "rss" },
{ :path => "/api/0.6/notes/getRSSfeed", :method => :get }
)
assert_routing(
{ :path => "/user/username/notes", :method => :get },
{ :controller => "notes", :action => "mine", :display_name => "username" }
)
end
def test_note_create_success
assert_difference('Note.count') do
assert_difference('NoteComment.count') do
post :create, {:lat => -1.0, :lon => -1.0, :text => "This is a comment", :format => "json"}
end
end
assert_response :success
js = ActiveSupport::JSON.decode(@response.body)
assert_not_nil js
assert_equal "Feature", js["type"]
assert_equal "Point", js["geometry"]["type"]
assert_equal [-1.0, -1.0], js["geometry"]["coordinates"]
assert_equal "open", js["properties"]["status"]
assert_equal 1, js["properties"]["comments"].count
assert_equal "opened", js["properties"]["comments"].last["action"]
assert_equal "This is a comment", js["properties"]["comments"].last["text"]
assert_nil js["properties"]["comments"].last["user"]
id = js["properties"]["id"]
get :show, {:id => id, :format => "json"}
assert_response :success
js = ActiveSupport::JSON.decode(@response.body)
assert_not_nil js
assert_equal "Feature", js["type"]
assert_equal "Point", js["geometry"]["type"]
assert_equal [-1.0, -1.0], js["geometry"]["coordinates"]
assert_equal id, js["properties"]["id"]
assert_equal "open", js["properties"]["status"]
assert_equal 1, js["properties"]["comments"].count
assert_equal "opened", js["properties"]["comments"].last["action"]
assert_equal "This is a comment", js["properties"]["comments"].last["text"]
assert_nil js["properties"]["comments"].last["user"]
end
def test_note_create_fail
assert_no_difference('Note.count') do
assert_no_difference('NoteComment.count') do
post :create, {:lon => -1.0, :text => "This is a comment"}
end
end
assert_response :bad_request
assert_no_difference('Note.count') do
assert_no_difference('NoteComment.count') do
post :create, {:lat => -1.0, :text => "This is a comment"}
end
end
assert_response :bad_request
assert_no_difference('Note.count') do
assert_no_difference('NoteComment.count') do
post :create, {:lat => -1.0, :lon => -1.0}
end
end
assert_response :bad_request
assert_no_difference('Note.count') do
assert_no_difference('NoteComment.count') do
post :create, {:lat => -1.0, :lon => -1.0, :text => ""}
end
end
assert_response :bad_request
assert_no_difference('Note.count') do
assert_no_difference('NoteComment.count') do
post :create, {:lat => -100.0, :lon => -1.0, :text => "This is a comment"}
end
end
assert_response :bad_request
assert_no_difference('Note.count') do
assert_no_difference('NoteComment.count') do
post :create, {:lat => -1.0, :lon => -200.0, :text => "This is a comment"}
end
end
assert_response :bad_request
end
def test_note_comment_create_success
assert_difference('NoteComment.count') do
post :comment, {:id => notes(:open_note_with_comment).id, :text => "This is an additional comment", :format => "json"}
end
assert_response :success
js = ActiveSupport::JSON.decode(@response.body)
assert_not_nil js
assert_equal "Feature", js["type"]
assert_equal notes(:open_note_with_comment).id, js["properties"]["id"]
assert_equal "open", js["properties"]["status"]
assert_equal 3, js["properties"]["comments"].count
assert_equal "commented", js["properties"]["comments"].last["action"]
assert_equal "This is an additional comment", js["properties"]["comments"].last["text"]
assert_nil js["properties"]["comments"].last["user"]
get :show, {:id => notes(:open_note_with_comment).id, :format => "json"}
assert_response :success
js = ActiveSupport::JSON.decode(@response.body)
assert_not_nil js
assert_equal "Feature", js["type"]
assert_equal notes(:open_note_with_comment).id, js["properties"]["id"]
assert_equal "open", js["properties"]["status"]
assert_equal 3, js["properties"]["comments"].count
assert_equal "commented", js["properties"]["comments"].last["action"]
assert_equal "This is an additional comment", js["properties"]["comments"].last["text"]
assert_nil js["properties"]["comments"].last["user"]
end
def test_note_comment_create_fail
assert_no_difference('NoteComment.count') do
post :comment, {:text => "This is an additional comment"}
end
assert_response :bad_request
assert_no_difference('NoteComment.count') do
post :comment, {:id => notes(:open_note_with_comment).id}
end
assert_response :bad_request
assert_no_difference('NoteComment.count') do
post :comment, {:id => notes(:open_note_with_comment).id, :text => ""}
end
assert_response :bad_request
assert_no_difference('NoteComment.count') do
post :comment, {:id => 12345, :text => "This is an additional comment"}
end
assert_response :not_found
assert_no_difference('NoteComment.count') do
post :comment, {:id => notes(:hidden_note_with_comment).id, :text => "This is an additional comment"}
end
assert_response :gone
assert_no_difference('NoteComment.count') do
post :comment, {:id => notes(:closed_note_with_comment).id, :text => "This is an additional comment"}
end
assert_response :conflict
end
def test_note_close_success
post :close, {:id => notes(:open_note_with_comment).id, :text => "This is a close comment", :format => "json"}
assert_response :unauthorized
basic_authorization(users(:public_user).email, "test")
post :close, {:id => notes(:open_note_with_comment).id, :text => "This is a close comment", :format => "json"}
assert_response :success
js = ActiveSupport::JSON.decode(@response.body)
assert_not_nil js
assert_equal "Feature", js["type"]
assert_equal notes(:open_note_with_comment).id, js["properties"]["id"]
assert_equal "closed", js["properties"]["status"]
assert_equal 3, js["properties"]["comments"].count
assert_equal "closed", js["properties"]["comments"].last["action"]
assert_equal "This is a close comment", js["properties"]["comments"].last["text"]
assert_equal "test2", js["properties"]["comments"].last["user"]
get :show, {:id => notes(:open_note_with_comment).id, :format => "json"}
assert_response :success
js = ActiveSupport::JSON.decode(@response.body)
assert_not_nil js
assert_equal "Feature", js["type"]
assert_equal notes(:open_note_with_comment).id, js["properties"]["id"]
assert_equal "closed", js["properties"]["status"]
assert_equal 3, js["properties"]["comments"].count
assert_equal "closed", js["properties"]["comments"].last["action"]
assert_equal "This is a close comment", js["properties"]["comments"].last["text"]
assert_equal "test2", js["properties"]["comments"].last["user"]
end
def test_note_close_fail
post :close
assert_response :unauthorized
basic_authorization(users(:public_user).email, "test")
post :close
assert_response :bad_request
post :close, {:id => 12345}
assert_response :not_found
post :close, {:id => notes(:hidden_note_with_comment).id}
assert_response :gone
post :close, {:id => notes(:closed_note_with_comment).id}
assert_response :conflict
end
def test_note_read_success
get :show, {:id => notes(:open_note).id, :format => "xml"}
assert_response :success
assert_equal "application/xml", @response.content_type
get :show, {:id => notes(:open_note).id, :format => "rss"}
assert_response :success
assert_equal "application/rss+xml", @response.content_type
get :show, {:id => notes(:open_note).id, :format => "json"}
assert_response :success
assert_equal "application/json", @response.content_type
get :show, {:id => notes(:open_note).id, :format => "gpx"}
assert_response :success
assert_equal "application/gpx+xml", @response.content_type
end
def test_note_read_hidden_comment
get :show, {:id => notes(:note_with_hidden_comment).id, :format => "json"}
assert_response :success
js = ActiveSupport::JSON.decode(@response.body)
assert_not_nil js
assert_equal notes(:note_with_hidden_comment).id, js["properties"]["id"]
assert_equal 2, js["properties"]["comments"].count
assert_equal "Valid comment for note 5", js["properties"]["comments"][0]["text"]
assert_equal "Another valid comment for note 5", js["properties"]["comments"][1]["text"]
end
def test_note_read_fail
get :show, {:id => 12345}
assert_response :not_found
get :show, {:id => notes(:hidden_note_with_comment).id}
assert_response :gone
end
def test_note_delete_success
delete :destroy, {:id => notes(:open_note_with_comment).id, :text => "This is a hide comment", :format => "json"}
assert_response :unauthorized
basic_authorization(users(:public_user).email, "test")
delete :destroy, {:id => notes(:open_note_with_comment).id, :text => "This is a hide comment", :format => "json"}
assert_response :forbidden
basic_authorization(users(:moderator_user).email, "test")
delete :destroy, {:id => notes(:open_note_with_comment).id, :text => "This is a hide comment", :format => "json"}
assert_response :success
js = ActiveSupport::JSON.decode(@response.body)
assert_not_nil js
assert_equal "Feature", js["type"]
assert_equal notes(:open_note_with_comment).id, js["properties"]["id"]
assert_equal "hidden", js["properties"]["status"]
assert_equal 3, js["properties"]["comments"].count
assert_equal "hidden", js["properties"]["comments"].last["action"]
assert_equal "This is a hide comment", js["properties"]["comments"].last["text"]
assert_equal "moderator", js["properties"]["comments"].last["user"]
get :show, {:id => notes(:open_note_with_comment).id, :format => 'json'}
assert_response :gone
end
def test_note_delete_fail
delete :destroy, {:id => 12345, :format => "json"}
assert_response :unauthorized
basic_authorization(users(:public_user).email, "test")
delete :destroy, {:id => 12345, :format => "json"}
assert_response :forbidden
basic_authorization(users(:moderator_user).email, "test")
delete :destroy, {:id => 12345, :format => "json"}
assert_response :not_found
delete :destroy, {:id => notes(:hidden_note_with_comment).id, :format => "json"}
assert_response :gone
end
def test_get_notes_success
# get :index, {:bbox => '1,1,1.2,1.2'}
# assert_response :success
# assert_equal "text/javascript", @response.content_type
get :index, {:bbox => '1,1,1.2,1.2', :format => 'rss'}
assert_response :success
assert_equal "application/rss+xml", @response.content_type
get :index, {:bbox => '1,1,1.2,1.2', :format => 'json'}
assert_response :success
assert_equal "application/json", @response.content_type
get :index, {:bbox => '1,1,1.2,1.2', :format => 'xml'}
assert_response :success
assert_equal "application/xml", @response.content_type
get :index, {:bbox => '1,1,1.2,1.2', :format => 'gpx'}
assert_response :success
assert_equal "application/gpx+xml", @response.content_type
end
def test_get_notes_large_area
# get :index, {:bbox => '-2.5,-2.5,2.5,2.5'}
# assert_response :success
# get :index, {:l => '-2.5', :b => '-2.5', :r => '2.5', :t => '2.5'}
# assert_response :success
get :index, {:bbox => '-10,-10,12,12'}
assert_response :bad_request
get :index, {:l => '-10', :b => '-10', :r => '12', :t => '12'}
assert_response :bad_request
end
def test_get_notes_closed
get :index, {:bbox => '1,1,1.7,1.7', :closed => '7', :format => 'json'}
assert_response :success
assert_equal "application/json", @response.content_type
js = ActiveSupport::JSON.decode(@response.body)
assert_not_nil js
assert_equal "FeatureCollection", js["type"]
assert_equal 4, js["features"].count
get :index, {:bbox => '1,1,1.7,1.7', :closed => '0', :format => 'json'}
assert_response :success
assert_equal "application/json", @response.content_type
js = ActiveSupport::JSON.decode(@response.body)
assert_not_nil js
assert_equal "FeatureCollection", js["type"]
assert_equal 4, js["features"].count
get :index, {:bbox => '1,1,1.7,1.7', :closed => '-1', :format => 'json'}
assert_response :success
assert_equal "application/json", @response.content_type
js = ActiveSupport::JSON.decode(@response.body)
assert_not_nil js
assert_equal "FeatureCollection", js["type"]
assert_equal 6, js["features"].count
end
def test_get_notes_bad_params
get :index, {:bbox => '-2.5,-2.5,2.5'}
assert_response :bad_request
get :index, {:bbox => '-2.5,-2.5,2.5,2.5,2.5'}
assert_response :bad_request
get :index, {:b => '-2.5', :r => '2.5', :t => '2.5'}
assert_response :bad_request
get :index, {:l => '-2.5', :r => '2.5', :t => '2.5'}
assert_response :bad_request
get :index, {:l => '-2.5', :b => '-2.5', :t => '2.5'}
assert_response :bad_request
get :index, {:l => '-2.5', :b => '-2.5', :r => '2.5'}
assert_response :bad_request
end
def test_search_success
get :search, {:q => 'note 1', :format => 'xml'}
assert_response :success
assert_equal "application/xml", @response.content_type
get :search, {:q => 'note 1', :format => 'json'}
assert_response :success
assert_equal "application/json", @response.content_type
get :search, {:q => 'note 1', :format => 'rss'}
assert_response :success
assert_equal "application/rss+xml", @response.content_type
get :search, {:q => 'note 1', :format => 'gpx'}
assert_response :success
assert_equal "application/gpx+xml", @response.content_type
end
def test_search_bad_params
get :search
assert_response :bad_request
end
def test_rss_success
get :feed, {:format => "rss"}
assert_response :success
assert_equal "application/rss+xml", @response.content_type
get :feed, {:bbox => "1,1,1.2,1.2", :format => "rss"}
assert_response :success
assert_equal "application/rss+xml", @response.content_type
end
def test_rss_fail
get :feed, {:bbox => "1,1,1.2"}
assert_response :bad_request
get :feed, {:bbox => "1,1,1.2,1.2,1.2"}
assert_response :bad_request
end
def test_user_notes_success
get :mine, {:display_name => "test"}
assert_response :success
get :mine, {:display_name => "pulibc_test2"}
assert_response :success
get :mine, {:display_name => "non-existent"}
assert_response :not_found
end
end

86
vendor/assets/ohauth/ohauth.js vendored Normal file
View file

@ -0,0 +1,86 @@
(function(context) {
var ohauth = {};
ohauth.qsString = function(obj) {
return Object.keys(obj).sort().map(function(key) {
return encodeURIComponent(key) + '=' +
encodeURIComponent(obj[key]);
}).join('&');
};
ohauth.sha = sha1();
ohauth.stringQs = function(str) {
return str.split('&').reduce(function(obj, pair){
var parts = pair.split('=');
obj[parts[0]] = (null === parts[1]) ?
'' : decodeURIComponent(parts[1]);
return obj;
}, {});
};
ohauth.rawxhr = function(method, url, data, headers, callback) {
var xhr = new XMLHttpRequest(), twoHundred = /^20\d$/;
xhr.onreadystatechange = function() {
if (4 == xhr.readyState && 0 !== xhr.status) {
if (twoHundred.test(xhr.status)) callback(null, xhr);
else return callback(xhr, null);
}
};
xhr.onerror = function(e) { return callback(e, null); };
xhr.open(method, url, true);
for (var h in headers) xhr.setRequestHeader(h, headers[h]);
xhr.send(data);
};
ohauth.xhr = function(method, url, auth, data, options, callback) {
var headers = (options && options.header) || {
'Content-Type': 'application/x-www-form-urlencoded'
};
headers.Authorization = 'OAuth ' + ohauth.authHeader(auth);
ohauth.rawxhr(method, url, auth, data, headers, callback);
};
ohauth.nonce = function() {
for (var o = ''; o.length < 6;) {
o += '0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz'[Math.floor(Math.random() * 61)];
}
return o;
};
ohauth.authHeader = function(obj) {
return Object.keys(obj).sort().map(function(key) {
return encodeURIComponent(key) + '="' + encodeURIComponent(obj[key]) + '"';
}).join(', ');
};
ohauth.timestamp = function() { return ~~((+new Date()) / 1000); };
ohauth.percentEncode = function(s) {
return encodeURIComponent(s)
.replace(/\!/g, '%21').replace(/\'/g, '%27')
.replace(/\*/g, '%2A').replace(/\(/g, '%28').replace(/\)/g, '%29');
};
ohauth.baseString = function(method, url, params) {
if (params.oauth_signature) delete params.oauth_signature;
return [
method,
ohauth.percentEncode(url),
ohauth.percentEncode(ohauth.qsString(params))].join('&');
};
ohauth.signature = function(oauth_secret, token_secret, baseString) {
return ohauth.sha.b64_hmac_sha1(
ohauth.percentEncode(oauth_secret) + '&' +
ohauth.percentEncode(token_secret),
baseString);
};
context.ohauth = ohauth;
// export for npm/browserify compatibility
if (typeof module !== 'undefined') module.exports = ohauth;
})(this);

191
vendor/assets/ohauth/sha.js vendored Normal file
View file

@ -0,0 +1,191 @@
/*
* A JavaScript implementation of the Secure Hash Algorithm, SHA-1, as defined
* in FIPS PUB 180-1
* Version 2.1a Copyright Paul Johnston 2000 - 2002.
* Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
* Distributed under the BSD License
* See http://pajhome.org.uk/crypt/md5 for details.
*/
function sha1() {
/*
* Configurable variables. You may need to tweak these to be compatible with
* the server-side, but the defaults work in most cases.
*/
var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */
var b64pad = "="; /* base-64 pad character. "=" for strict RFC compliance */
var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */
/*
* These are the functions you'll usually want to call
* They take string arguments and return either hex or base-64 encoded strings
*/
function hex_sha1(s){return binb2hex(core_sha1(str2binb(s),s.length * chrsz));}
function b64_sha1(s){return binb2b64(core_sha1(str2binb(s),s.length * chrsz));}
function str_sha1(s){return binb2str(core_sha1(str2binb(s),s.length * chrsz));}
function hex_hmac_sha1(key, data){ return binb2hex(core_hmac_sha1(key, data));}
function b64_hmac_sha1(key, data){ return binb2b64(core_hmac_sha1(key, data));}
function str_hmac_sha1(key, data){ return binb2str(core_hmac_sha1(key, data));}
/*
* Perform a simple self-test to see if the VM is working
*/
function sha1_vm_test() {
return hex_sha1("abc") == "a9993e364706816aba3e25717850c26c9cd0d89d";
}
/*
* Calculate the SHA-1 of an array of big-endian words, and a bit length
*/
function core_sha1(x, len) {
/* append padding */
x[len >> 5] |= 0x80 << (24 - len % 32);
x[((len + 64 >> 9) << 4) + 15] = len;
var w = Array(80);
var a = 1732584193;
var b = -271733879;
var c = -1732584194;
var d = 271733878;
var e = -1009589776;
for(var i = 0; i < x.length; i += 16) {
var olda = a;
var oldb = b;
var oldc = c;
var oldd = d;
var olde = e;
for(var j = 0; j < 80; j++) {
if(j < 16) w[j] = x[i + j];
else w[j] = rol(w[j-3] ^ w[j-8] ^ w[j-14] ^ w[j-16], 1);
var t = safe_add(safe_add(rol(a, 5), sha1_ft(j, b, c, d)),
safe_add(safe_add(e, w[j]), sha1_kt(j)));
e = d;
d = c;
c = rol(b, 30);
b = a;
a = t;
}
a = safe_add(a, olda);
b = safe_add(b, oldb);
c = safe_add(c, oldc);
d = safe_add(d, oldd);
e = safe_add(e, olde);
}
return Array(a, b, c, d, e);
}
/*
* Perform the appropriate triplet combination function for the current
* iteration
*/
function sha1_ft(t, b, c, d) {
if(t < 20) return (b & c) | ((~b) & d);
if(t < 40) return b ^ c ^ d;
if(t < 60) return (b & c) | (b & d) | (c & d);
return b ^ c ^ d;
}
/*
* Determine the appropriate additive constant for the current iteration
*/
function sha1_kt(t) {
return (t < 20) ? 1518500249 : (t < 40) ? 1859775393 :
(t < 60) ? -1894007588 : -899497514;
}
/*
* Calculate the HMAC-SHA1 of a key and some data
*/
function core_hmac_sha1(key, data) {
var bkey = str2binb(key);
if(bkey.length > 16) bkey = core_sha1(bkey, key.length * chrsz);
var ipad = Array(16), opad = Array(16);
for(var i = 0; i < 16; i++) {
ipad[i] = bkey[i] ^ 0x36363636;
opad[i] = bkey[i] ^ 0x5C5C5C5C;
}
var hash = core_sha1(ipad.concat(str2binb(data)), 512 + data.length * chrsz);
return core_sha1(opad.concat(hash), 512 + 160);
}
/*
* Add integers, wrapping at 2^32. This uses 16-bit operations internally
* to work around bugs in some JS interpreters.
*/
function safe_add(x, y) {
var lsw = (x & 0xFFFF) + (y & 0xFFFF);
var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
return (msw << 16) | (lsw & 0xFFFF);
}
/*
* Bitwise rotate a 32-bit number to the left.
*/
function rol(num, cnt) {
return (num << cnt) | (num >>> (32 - cnt));
}
/*
* Convert an 8-bit or 16-bit string to an array of big-endian words
* In 8-bit function, characters >255 have their hi-byte silently ignored.
*/
function str2binb(str) {
var bin = Array();
var mask = (1 << chrsz) - 1;
for(var i = 0; i < str.length * chrsz; i += chrsz)
bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (32 - chrsz - i%32);
return bin;
}
/*
* Convert an array of big-endian words to a string
*/
function binb2str(bin) {
var str = "";
var mask = (1 << chrsz) - 1;
for(var i = 0; i < bin.length * 32; i += chrsz)
str += String.fromCharCode((bin[i>>5] >>> (32 - chrsz - i%32)) & mask);
return str;
}
/*
* Convert an array of big-endian words to a hex string.
*/
function binb2hex(binarray) {
var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
var str = "";
for(var i = 0; i < binarray.length * 4; i++) {
str += hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8+4)) & 0xF) +
hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8 )) & 0xF);
}
return str;
}
/*
* Convert an array of big-endian words to a base-64 string
*/
function binb2b64(binarray) {
var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
var str = "";
for(var i = 0; i < binarray.length * 4; i += 3) {
var triplet = (((binarray[i >> 2] >> 8 * (3 - i %4)) & 0xFF) << 16)
| (((binarray[i+1 >> 2] >> 8 * (3 - (i+1)%4)) & 0xFF) << 8 )
| ((binarray[i+2 >> 2] >> 8 * (3 - (i+2)%4)) & 0xFF);
for(var j = 0; j < 4; j++) {
if(i * 8 + j * 6 > binarray.length * 32) str += b64pad;
else str += tab.charAt((triplet >> 6*(3-j)) & 0x3F);
}
}
return str;
}
return { b64_hmac_sha1: b64_hmac_sha1 };
}

File diff suppressed because one or more lines are too long