Website, database, and API for Falling Fruit
HTML JavaScript Ruby R CoffeeScript CSS Other
Latest commit b4be335 Feb 27, 2017 @ezwelty ezwelty committed on GitHub Merge pull request #21 from falling-fruit/allow-filtering-by-2-or-mor…

Allow filtering by two or more types in locations.json
Failed to load latest commit information.
api Allow filtering by two or more types in locations.json Feb 28, 2017
app Cleanup code readability Feb 24, 2017
config Add Turkish name field Jan 13, 2017
db Add Turkish name field Jan 13, 2017
doc initial import of basically blank rails app Oct 24, 2012
lib Deployed attribute normalizer gem. Added remove_blanks rake task. Mar 11, 2015
log initial import of basically blank rails app Oct 24, 2012
public Include Colorado ELP Bronze logo file Sep 28, 2016
script Add nodemon script Oct 9, 2016
spec Deployed attribute normalizer gem. Added remove_blanks rake task. Mar 11, 2015
util Add common name table export for human translators Nov 29, 2016
vendor initial import of basically blank rails app Oct 24, 2012
.gitignore Ignore db .dump files Feb 8, 2017
.phraseapp.yml.dist Switch to PhraseApp for translations. Jan 12, 2017
.rspec Fix a style issue in notice Mar 8, 2013
.ruby-gemset Commit to 1.9.3, update libv8 and therubyracer to work Oct 9, 2016
.ruby-version Commit to 1.9.3, update libv8 and therubyracer to work Oct 9, 2016
Gemfile Commit to 1.9.3, update libv8 and therubyracer to work Oct 9, 2016
Gemfile.lock Commit to 1.9.3, update libv8 and therubyracer to work Oct 9, 2016
Makefile Replace stray hard tabs with soft tabs (except Makefiles) Nov 29, 2016 Install PostGIS extension Feb 8, 2017
Rakefile Add reverse geocode rake task Nov 27, 2016 initial import of basically blank rails app Oct 24, 2012
locales Symlink in case this is needed for some reason Apr 22, 2016

Falling Fruit Web

This is a Rails 3 web application for Falling Fruit, live at The PostgreSQL + PostGIS database is accessed over a RESTful JSON API served up at

Who is responsible?

Falling Fruit co-founders Caleb Phillips and Ethan Welty. More info at

This code is licensed under the GNU General Public License version 3. All of Falling Fruit's data, if not otherwise noted, is licensed under a Creative Commons Attribution Non-Commercial Share-Alike License.

Both licenses require attribution and that derivative works retain the same license. We reserve the right to prohibit use on a project-by-project basis. Please contact us ( if you have any questions.

How can I help?

If you want to help with development, feel free to fork the project. If you have something to submit upstream, send a pull request from your fork. Cool? Cool!

Build instructions (website)

Install Ruby and dependencies

  • Ruby Version Manager (rvm): installation instructions
  • Ruby (1.9.3):

    rvm install 1.9.3
    rvm use 1.9.3
  • PostgreSQL (9.5.4): installation instructions

    brew install postgresql
  • PostGIS (2.2.2): installation instructions

    brew install postgis
  • Bundler: installation instructions

    gem install bundler
  • Install project gems:

    bundle install
  • Initialize configuration files:

    cp config/database.yml.dist config/database.yml
    cp config/s3.yml.dist config/s3.yml
    cp config/initializers/secret_token.rb.example config/initializers/secret_token.rb

    Edit config/database.yml with your desired development database name, username, and password. Since all files (photos) are stored on Amazon S3 servers, you'll need to add Amazon S3 credentials to your config/s3.yml file. Contact us ( for a key.

Prepare database

  • Initialize and start your postgres database:

    initdb -D /usr/local/var/postgres/
    pg_ctl -D /usr/local/var/postgres/ -l logfile start
  • Create a Falling Fruit database and superuser:

    psql postgres
    CREATE DATABASE fallingfruit_new_db;
    GRANT ALL ON DATABASE fallingfruit_new_db TO fallingfruit_user;

    The database, username, and password should match your settings for the development database in config/database.yml.

  • Add the PostGIS extension to the database:

    psql fallingfruit_new_db
  • Load the database schema:

    If you have a dump of the production database, it can be copied to your local environment. These commands, although slow, have been found to work reliably:

    # remote
    pg_dump --no-owner fallingfruit_new_db > fallingfruit.latest.dump
    # local
    psql -d fallingfruit_new_db < fallingfruit.latest.dump
    pg_restore --clean --no-owner -d fallingfruit_new_db ~/desktop/fallingfruit.latest.sql

    Otherwise, load the database schema:

    bundle exec rake db:schema:load

    Or if updating an existing database with the latest schema, run migrations instead:

    bundle exec rake db:migrate

    If you're proceeding from an empty database, you'll need to add a couple required functions:

    psql fallingfruit_new_db
    /* Function: utmzone(geometry)
    DROP FUNCTION utmzone(geometry);
    Usage: SELECT ST_Transform(the_geom, utmzone(ST_Centroid(the_geom))) FROM sometable; */
    CREATE OR REPLACE FUNCTION utmzone(geometry)
    RETURNS integer AS
    geomgeog geometry;
    zone int;
    pref int;
    geomgeog:= ST_Transform($1,4326);
    IF (ST_Y(geomgeog))>0 THEN
    END IF;
    RETURN zone+pref;
    COST 100;
    /* Function: ST_Buffer_Meters(geometry, double precision)
    DROP FUNCTION ST_Buffer_Meters(geometry, double precision);
    Usage: SELECT ST_Buffer_Meters(the_geom, num_meters) FROM sometable; */
    CREATE OR REPLACE FUNCTION ST_Buffer_Meters(geometry, double precision)
    RETURNS geometry AS
    orig_srid int;
    utm_srid int;
    orig_srid:= ST_SRID($1);
    utm_srid:= utmzone(ST_Centroid($1));
    RETURN ST_transform(ST_Buffer(ST_transform($1, utm_srid), $2), orig_srid);
    COST 100;
  • To start the thin web server, run:

    bundle exec thin start

    and visit localhost:3000/users/sign_up to register an account.

  • Finally, force-confirm your account (so that you can sign in) and make yourself an admin (so that you have access to all site features):

    psql fallingfruit_new_db
    UPDATE users SET confirmed_at='2013-01-01 00:00:00' WHERE id='1';
    UPDATE users SET roles_mask='3' WHERE id='1';

    Don't go too far, you'll now need to get the API working!

Build instructions (API)

Install Node and dependencies

  • Node Version Manager (nvm): installation instructions
  • Node (0.12):

    nvm install 0.12
    nvm use 0.12
  • Node packages:

    cd api
    npm install

Prepare API

  • Create an API key:

    Calls to the API will require an api_key parameter that matches an entry in the api_keys database table. You can create one from the rails console.

    rails console
    ApiKey.create(api_key: 'AKDJGHSD')

    Then set the api_key variable in /app/assets/javascripts/mapcommon.js. The key 'AKDJGHSD' is set by default on localhost since it is also the testing key for the live version of the API.

  • Start the API:

    make start

    You can test the API by visiting localhost:3100/api/0.2/types.json?api_key=AKDJGHSD. The page should return [] until you create a new type at localhost:3000/types/new.

    The API is currently (poorly) documented here.

API Versioning

Since we have multiple versions of the mobile app in the wild, using different versions of the API, more care is needed with respect to branching and versioning the API than with other Falling Fruit code. As of 23 September 2016, we are running two versions of the API in parallel:

  • v0.1 (api-release-0.1 branch) - A Rails-based API existing entirely within app/controller/api. All versions of the mobile app (v0.1 & 0.2) use this version of the API.
  • v0.2 (api-release-0.2 branch) - The first version of the NodeJS-based API. The current website uses this version of the API.
  • v0.3 (under construction in master branch) - The next release of the API, which will be NodeJS-based and both the mobile app and website should use it.

API v0.1 will need to persist for the foreseeable future (unless/until we decide to force upgrade all v0.1 and v0.2 installs of the mobile app). API v0.2 can presumably be removed once v0.3 is released since only the website uses it. Starting with API v0.3, we will need to run parallel versions of the API to allow backwards compatibility.


For translators

Translations of the website interface are managed via the PhraseApp project Falling Fruit (web). To contribute, contact us ( and we'll add you as a translator to the project. Species common names are machine-translated and stored directly in the database.

For developers

Install the PhraseApp CLI:

brew tap phrase/brewed
brew install phraseapp
cp .phraseapp.yml.dist .phraseapp.yml

Edit .phraseapp.yml, and replace YOUR_ACCESS_TOKEN with your PhraseApp access token.

Adding a new translation is easy!

Step 1: Add the new translation key on PhraseApp.

Browse to the Falling Fruit (web) project, select the default locale (English/en), and add a new translation key. If the same word or phrase appears often, add it as glossary.<key name> to avoid making many keys with identical or derived (pluralized, capitalized, etc) values.

Step 2: Update your translation files.

Provided you've setup the PhraseApp CLI (instructions above), run:

phraseapp pull

This will update the translation files in config/locales/*.yml.

Step 3: Replace the string in your template with the translation key.

<!-- Instead of adding text to the markup: -->

<!-- Evoke the translation key value with translate() -->
<span><%= translate("") %></span>

This script, run in the console, can be used to mass-translate keys using Google Translate. For usage, follow the instructions on this page. NOTE: Requires the old PhraseApp translation editor.

var GET_TRANSLATIONS = "Machine translate";
var APPLY_TRANSLATIONS = "Apply translations";
var UNVERIFY_TRANSLATIONS = "Unverify translations (after save)";

function suggestion_button() {
  suggestBtn.attr('value', GET_TRANSLATIONS);'click', function() {
    var editors = $('.translation-editor');
    var emptyEditors = editors.filter(function () {
      return $(this).find('textarea')[0].value == "";
    var limit = prompt("Number of empty keys to translate (default: all)", emptyEditors.length);
    emptyEditors = emptyEditors.slice(0, parseInt(limit, 10));
    emptyEditors.each(function () {
    suggestBtn.attr('value', APPLY_TRANSLATIONS);'click', function () {
      emptyEditors.each(function () {
      suggestBtn.attr('value', UNVERIFY_TRANSLATIONS);'click', function () {
        emptyEditors.each(function () {

var bar = $('#translation-options-bar');
var submitBtn = bar.find('.btn-primary').first();
submitBtn.before("<input id='suggestBtn' class='btn btn-primary'></input>");
var suggestBtn = $('input#suggestBtn').last();