<commit_before>var db = require('./connection');
var async = require('async');

var defaultCallback = function (functionName) {
  return function (err, data){
    if (err) throw functionName + ' : ' + err;
    console.log(functionName, 'successfull! ', data);
  };
};

module.exports = {

  addRoom: function (roomName, callback) {
    callback = callback || defaultCallback('addRoom');
    db.hgetall('rooms', function(err, data){
      if (err) throw 'addRoom error: ' + err;
      if (!data[roomName]){
        db.HSET('rooms', roomName, 1, function(err, data){callback(err, data);});
      } else {
        console.log('room exists');
        db.HINCRBY('rooms', roomName, 1, function(err, data){callback(err, data);});
      }
    });
  },

  deleteRooms: function (roomName, callback){
    callback = callback || defaultCallback('deleteRooms');
    async.waterfall([function (callback) {
      db.HDEL('rooms', roomName, function(err, data){
        callback(null, data);
      });
    }, function (data, callback) {
      db.DEL(roomName+'_KILLS', function(err, data){
        callback(null, data);
      });
    }, function (data, callback) {
      db.DEL(roomName+'_DEATHS', function(err, data){
        callback(null, data);
      });
    }], callback);
  },

  joinRoom: function (roomName, playerID, joinCallback) {
    var that = this;
    joinCallback = joinCallback || defaultCallback('joinRoom');

    async.waterfall([function (callback){
      console.log('drop0');
      console.log(arguments);
      that.addRoom(roomName, callback);
    },
    function(resultData, callback) {
      console.log('drop1');
      console.log(arguments);
      db.HSET(roomName+'_KILLS', playerID, 0, function(err, data){
        callback(null, data);
      });
    },
    function(resultData, callback) {
      console.log('drop2');
      console.log(arguments);
      db.HSET(roomName+'_DEATHS', playerID, 0, function(err, data){
        callback(null, data);
      });
    }], joinCallback);
  },

  leaveRoom: function (roomName, playerID, leaveCallback) {
    leaveCallback = leaveCallback || defaultCallback('leaveRoom');
    var that = this;

    async.waterfall([function (callback){
      db.HDEL(roomName+'_KILLS', playerID, function(err, data){
        callback(null, data);
      });
    },
    function(resultData, callback) {
      db.HDEL(roomName+'_DEATHS', playerID, function(err, data){
        callback(null, data);
      });
    },
    function(resultData, callback) {
      db.HINCRBY('rooms', roomName, -1, function(err, playersInRoom){
        callback(null, playersInRoom);
      });
    },
    // need to remove the old rooms from the _KILLS and _DEATHS sections
    function(playersInRoom, callback) {
      if (playersInRoom < 1){
        that.deleteRooms(roomName);
      } else {
        callback(null, playersInRoom);
      }
    }], leaveCallback);
  },

  incKillCount: function (roomName, playerID) {
    db.HINCRBY(roomName+'_KILLS', playerID, 1, defaultCallback('incKillCount'));
  },

  incDeathCount: function (roomName, playerID) {
    db.HINCRBY(roomName+'_DEATHS', playerID, 1, defaultCallback('incKillCount'));
  },

};
<commit_msg>updated db connections, cleaned out some logs<commit_after>var db = require('./connection');
var async = require('async');

var defaultCallback = function (functionName) {
  return function (err, data){
    if (err) throw functionName + ' : ' + err;
    console.log(functionName, 'successfull! ', data);
  };
};

module.exports = {

  addRoom: function (roomName, callback) {
    callback = callback || defaultCallback('addRoom');
    db.hgetall('rooms', function(err, data){
      if (err) throw 'addRoom error: ' + err;
      if (!data || !data[roomName]){
        console.log('room DNE');
        db.HSET('rooms', roomName, 1, function(err, data){
          callback(err, data);
        });
      } else {
        console.log('room exists');
        db.HINCRBY('rooms', roomName, 1, function(err, data){
          callback(err, data);
        });
      }
    });
  },

  deleteRooms: function (roomName, callback){
    callback = callback || defaultCallback('deleteRooms');
    async.waterfall([function (callback) {
      db.HDEL('rooms', roomName, function(err, data){
        callback(null, data);
      });
    }, function (data, callback) {
      db.DEL(roomName+'_KILLS', function(err, data){
        callback(null, data);
      });
    }, function (data, callback) {
      db.DEL(roomName+'_DEATHS', function(err, data){
        callback(null, data);
      });
    }], callback);
  },

  joinRoom: function (roomName, playerID, joinCallback) {
    var that = this;
    joinCallback = joinCallback || defaultCallback('joinRoom');

    async.waterfall([function (callback){
      that.addRoom(roomName, callback);
    },
    function(resultData, callback) {
      db.HSET(roomName+'_KILLS', playerID, 0, function(err, data){
        callback(null, data);
      });
    },
    function(resultData, callback) {
      db.HSET(roomName+'_DEATHS', playerID, 0, function(err, data){
        callback(null, data);
      });
    }], joinCallback);
  },

  leaveRoom: function (roomName, playerID, leaveCallback) {
    leaveCallback = leaveCallback || defaultCallback('leaveRoom');
    var that = this;

    async.waterfall([function (callback){
      db.HDEL(roomName+'_KILLS', playerID, function(err, data){
        callback(null, data);
      });
    },
    function(resultData, callback) {
      db.HDEL(roomName+'_DEATHS', playerID, function(err, data){
        callback(null, data);
      });
    },
    function(resultData, callback) {
      db.HINCRBY('rooms', roomName, -1, function(err, playersInRoom){
        callback(null, playersInRoom);
      });
    },
    // need to remove the room's _KILLS and _DEATHS hashes
    function(playersInRoom, callback) {
      if (playersInRoom < 1){
        that.deleteRooms(roomName);
      } else {
        callback(null, playersInRoom);
      }
    }], leaveCallback);
  },

  incKillCount: function (roomName, playerID) {
    db.HINCRBY(roomName+'_KILLS', playerID, 1, defaultCallback('incKillCount'));
  },

  incDeathCount: function (roomName, playerID) {
    db.HINCRBY(roomName+'_DEATHS', playerID, 1, defaultCallback('incKillCount'));
  },

};
<|endoftext|>


<commit_before>'use strict';

var elasticsearch = require('elasticsearch');
var Q = require('q');

var cip = require('../lib/cip-methods.js');
var cip_categories = require('../lib/cip-categories.js');
var asset_mapping = require('../lib/asset-mapping.js');

var client = new elasticsearch.Client({requestTimeout: 30 * 60 * 1000 });

var sync_all = false;

// Aliases of specific catalogs that are to be synced.
var sync_catalogs_whitelist = false;
var categories = {};

var ASSETS_PER_REQUEST = 100;

/*=== Defining modes to run the syncronization in ===*/

// What modes can we run the elasticsearch script in?
var MODES = {
    recent: 'recent',
    all: 'all',
    catalog: 'catalog',
    single: 'single'
};
var MODE_DESCRIPTIONS = {
    recent: 'Syncronize the most recently changed assets.',
    all: 'Syncronize all assets across all catalogs.',
    catalog: 'Syncronize only a single catalog or a set of comma seperated catalogs.',
    single: 'Syncronize only a single asset or a comma seperated list of assets.'
};
var mode, reference;

// A function to check that a user supplied mode is valid.
function is_valid_mode(suggested_mode) {
    for(var mode in MODES) {
        if(suggested_mode === MODES[mode]) {
            return mode;
        }
    }
    return false;
}

var args = process.argv;
if(args && args.length <= 2) {
    // No arguments supplied, just node and the app script's path.
    mode = MODES.recent;
} else if(args && args.length >= 3) {
    var suggested_mode = args[2];
    mode = is_valid_mode(suggested_mode);
    if(mode === MODES.catalog || mode === MODES.single) {
        if(args.length >= 4) {
            reference = args[3].split(',');
        }
    }
}

// Report any runtime errors on mode selection.
if(!mode) {
    console.error('Invalid mode - please select from:');
    for(var m in MODES) {
        console.error(' * ' +MODES[m]+ ': '+MODE_DESCRIPTIONS[m]);
    }
    process.exit(1);
} else if(mode === MODES.single) {
    // In the single mode, each asset is a combination of a catalog alias
    // and the asset ID, eg. DNT/101
    for(var r in reference) {
        reference[r] = reference[r].split('-');
    }
}

function is_relevant_catalog(catalog_alias) {
    if(mode === MODES.catalog) {
        // Are there any of the split catalogs that maches?
        for(var c in reference) {
            if(reference[c] === catalog_alias) {
                return true;
            }
        }
        return false;
    } else if(mode === MODES.single) {
        for(var r in reference) {
            if(reference[r][0] == catalog_alias) {
                return true;
            }
        }
        return false;
    } else {
        return true;
    }
}

function is_relevant_asset(catalog_alias, asset_id) {
    if(mode === MODES.single) {
        // Are there any of the split Catalog/ID 2-tuples that matches?
        for(var r in reference) {
            var referenced_catalog_alias = reference[r][0];
            var referenced_asset_id = parseInt(reference[r][1], 10);
            if(referenced_catalog_alias === catalog_alias && referenced_asset_id === asset_id) {
                return true;
            }
        }
        return false;
    } else {
        return is_relevant_catalog(catalog_alias);
    }
}

/*=== END: Defining modes to run the syncronization in ===*/

// Creates the index in the Elasticsearch index.
function create_index() {
    return client.indices.create({
        index: 'assets'
    }).then(function() {
        console.log('Index created');
    });
}

// Cleans up a string, representing a category.
function clean_string(str) {
    var regexp = new RegExp('^[0-9]+[A-Z]+.[0-9]+.[0-9]+ ');
    if(str.search(regexp) === 0) {
        return str.replace(regexp, '');
    }
    regexp = new RegExp('^[A-Z]?[0-9]+[a-z]? - ');
    if(str.search(regexp) === 0) {
        return str.replace(regexp, '');
    }
    regexp = new RegExp('^[A-F] - ');
    if(str.search(regexp) === 0) {
        return str.replace(regexp, '');
    }
    regexp = new RegExp('^[0-9][0-9] ');
    if(str.search(regexp) === 0) {
        return str.replace(regexp, '');
    }
    return str;
}

// Determines wether or not an asset should be visible to the user when searching.
function determine_searchability(cip_client, asset, formatted_result) {
    // First of all - we wouldn't like to have search results on assets
    // which have been cropped into seperate assets.
    if(formatted_result.cropping_status && formatted_result.cropping_status.id === 2) {
        return false;
    } else {
        // Second - We wouldn't like assets which are related to assets that are in the
        // "Rotationsbilleder" category - as these are side-views of an object which will be
        // reachable through the front-facing master asset.
        return cip.get_related_assets(asset, 'isalternateof')
        .then(function parse_relations(related_assets) {
            var related_asset_promises = [];
            for(var i in related_assets.ids) {
                var related_asset_id = related_assets.ids[i];
                var related_asset_promise = cip.get_asset(cip_client, formatted_result.catalog, related_asset_id);
                related_asset_promises.push( related_asset_promise );
            }
            return Q.all(related_asset_promises).then(function(related_assets) {
                for(var r in related_assets) {
                    for(var a in related_assets[r]) {
                        var related_asset = related_assets[r][a];
                        var formatted_asset = asset_mapping.format_result(related_asset.fields);
                        for(var c in formatted_asset.categories) {
                            var category = formatted_asset.categories[c];
                            if(category.name === 'Rotationsbilleder') {
                                return false;
                            }
                        }
                    }
                }
                // None of the related assets was a part of the rotational images category.
                return true;
            });
        });
    }
}

var categories = {};
var catalog_page_index = {};
var catalog_index = 0;

// Handle a specific asset from a result page.
function handle_asset(cip_client, asset, catalog_alias) {
    var formatted_result = asset_mapping.format_result(asset.fields);
    formatted_result.catalog = catalog_alias;

    if(!is_relevant_asset(catalog_alias, formatted_result.id)) {
        console.log('Looks like an irrelevant asset', catalog_alias, formatted_result.id);
        return false;
    }

    return Q.all([
        asset_mapping.extend_metadata(cip_client, catalog_alias, asset, formatted_result),
        determine_searchability(cip_client, asset, formatted_result)
    ])
    .spread(function(formatted_result, is_searchable) {
        formatted_result.searchable = is_searchable;

        if(formatted_result.modification_time !== undefined) {
            var re = new RegExp('\\d+');
            var re_result = re.exec(formatted_result.modification_time);
            if(re_result && re_result.length > 0) {
                formatted_result.modification_time = parseInt(re_result[0], 10);
            }
        }

        if(formatted_result.categories !== undefined) {
            formatted_result.categories_int = [];
            formatted_result.suggest = {'input': []};

            for(var j=0; j < formatted_result.categories.length; ++j) {
                if(formatted_result.categories[j].path.indexOf('$Categories') !== 0) {
                    continue;
                }

                var path = categories[catalog_alias].get_path(formatted_result.categories[j].id);
                if(path) {
                    for(var k=0; k < path.length; k++) {
                        formatted_result.categories_int.push(path[k].id);

                        if(path[k].name.indexOf('$Categories') === 0) {
                            continue;
                        }

                        formatted_result.suggest.input.push(clean_string(path[k].name));
                    }
                }
            }
        }
        return formatted_result;
    })
    .then(function(formatted_result) {
        var es_id = catalog_alias + '-' + formatted_result.id;
        return client.index({
            index: 'assets',
            type: 'asset',
            id: es_id,
            body: formatted_result
        });
    })
    .then(function(resp) {
        console.log('Successfully indexed ' + resp._id);
    })
    .fail(function(err) {
        console.error( 'Something went wrong indexing an asset' );
        console.error( err.stack );
    });
}

// Handle a specific result page, with assets.
function handle_result_page(cip_client, catalog, result, page_index) {
    var deferred = Q.defer();
    var total_pages = Math.ceil(result.total_rows / ASSETS_PER_REQUEST);
    console.log('Queuing page number', page_index+1, 'of', total_pages+1, 'in the', catalog.alias, 'catalog.');

    result.get(ASSETS_PER_REQUEST, page_index * ASSETS_PER_REQUEST, function(assets_on_page) {
        console.log('Got metadata of page ' +(page_index+1)+ ' from the ' +result.catalog.alias+ ' catalog.');
        var asset_promises = [];
        for(var a in assets_on_page) {
            var asset = assets_on_page[a];
            var asset_promise = handle_asset(cip_client, asset, catalog.alias);
            asset_promises.push( asset_promise );
        }
        Q.all(asset_promises).then(function() {
            // Resolve the page promise once all assets on the page has been resolved.
            deferred.resolve( true );
        });
    });

    return deferred.promise;
}

// Recursively handle the assets on pages, giving a breath first traversal
// of the CIP webservice.
function handle_next_result_page(cip_client, catalog, result) {
    var page_index = catalog_page_index[catalog.alias];
    if(page_index * ASSETS_PER_REQUEST < result.total_rows) {
        catalog_page_index[catalog.alias]++;
        return handle_result_page(cip_client, catalog, result, page_index)
        .then(function() {
            return handle_next_result_page(cip_client, catalog, result);
        });
    } else {
        return true; // No more pages in the result.
    }
}

// Handle a specific catalog.
function handle_catalog(cip_client, catalog) {
    // Precondition: The catalog has it's alias defined.
    if(catalog === undefined || catalog.alias === undefined) {
        throw new Error('The catalog´s alias was undefined');
    }
    console.log('Queuing catalog', catalog.alias);
    var modified_since = (mode === MODES.recent ? '$today-2d' : '2003-04-24');
    // Let's find out how many pages of assets we have in this catalog.
    return cip.get_recent_assets(cip_client, catalog, modified_since )
    .then(function(result) {
        // Reset the counter.
        catalog_page_index[catalog.alias] = 0;
        return Q.when(handle_next_result_page(result, catalog, result), function() {
            console.log('Done parsing catalog', catalog.alias);
        });
    });
}

// Recursively handle the catalogs, giving a breath first traversal
// of the CIP webservice.
function handle_next_catalog(cip_client, catalogs) {
    if(catalog_index < catalogs.length) {
        var catalog = catalogs[catalog_index];
        catalog_index++;
        return handle_catalog(cip_client, catalog)
        .then(function() {
            return handle_next_catalog(cip_client, catalogs);
        });
    } else {
        return true; // No more catalogs.
    }
}

// Let's get started.
console.log('Running in mode: ' + MODE_DESCRIPTIONS[mode]);

var main_queue = cip_categories.load_categories()
.then(function(result) {
    // The categories pr catalog has been fetched from Cumulus.
    for(var i=0; i < result.length; ++i) {
        categories[result[i].id] = result[i];
    }
    var categories_count = Object.keys(categories).length;
    console.log('Loaded categories for', categories_count, 'catalogs');
})
.then(function() {
    return create_index().then(function() {
        console.log('Index created');
    }, function(err) {
        // TODO: Add a recursive check for this message.
        if(err.message === 'IndexAlreadyExistsException[[assets] already exists]') {
            return; // No worries ...
        }
        console.log('Failed to create the index:', err);
    });
})
.then(cip.init_session);

if(mode === MODES.recent || mode === MODES.all || mode === MODES.catalog) {
    main_queue = main_queue.then(function(cip_client) {
        return [cip_client, cip.get_catalogs(cip_client)];
    })
    .spread(function(cip_client, catalogs) {
        var relevant_catalogs = [];
        if(mode === MODES.catalog) {
            for(var c in catalogs) {
                var catalog = catalogs[c];
                if(reference.indexOf(catalog.alias) !== -1) {
                    relevant_catalogs.push(catalog);
                }
            }
        } else {
            // All catalogs are relevant catalogs.
            relevant_catalogs = catalogs;
        }
        return handle_next_catalog(cip_client, relevant_catalogs);
    });
} else {
    main_queue = main_queue.then(function(cip_client) {
        var asset_promises = [];
        for(var a in reference) {
            var catalog_alias = reference[a][0];
            var asset_id = reference[a][1];
            var asset_promise = cip.get_asset(cip_client, catalog_alias, asset_id)
            .then(function(assets) {
                if(assets.length === 1) {
                    console.log('Logging asset ', assets[0].fields.id);
                    return handle_asset(cip_client, assets[0], catalog_alias);
                } else {
                    throw new Error( 'No asset with id ' +asset_id+ ' was found in the ' +catalog_alias+ ' catalog.' );
                }
            });
            asset_promises.push( asset_promise );
        }
        return Q.all(asset_promises);
    });
}

main_queue
.fail(function (error) {
    console.error('An error occurred');
    console.error(error.stack);
}).finally(function() {
    console.log('=== All done ===');
    // We are ready to die ..
    process.exit(0);
});<commit_msg>Removed unused functions.<commit_after>'use strict';

var elasticsearch = require('elasticsearch');
var Q = require('q');

var cip = require('../lib/cip-methods.js');
var cip_categories = require('../lib/cip-categories.js');
var asset_mapping = require('../lib/asset-mapping.js');

var client = new elasticsearch.Client({requestTimeout: 30 * 60 * 1000 });

var sync_all = false;

// Aliases of specific catalogs that are to be synced.
var sync_catalogs_whitelist = false;
var categories = {};

var ASSETS_PER_REQUEST = 100;

/*=== Defining modes to run the syncronization in ===*/

// What modes can we run the elasticsearch script in?
var MODES = {
    recent: 'recent',
    all: 'all',
    catalog: 'catalog',
    single: 'single'
};
var MODE_DESCRIPTIONS = {
    recent: 'Syncronize the most recently changed assets.',
    all: 'Syncronize all assets across all catalogs.',
    catalog: 'Syncronize only a single catalog or a set of comma seperated catalogs.',
    single: 'Syncronize only a single asset or a comma seperated list of assets.'
};
var mode, reference;

// A function to check that a user supplied mode is valid.
function is_valid_mode(suggested_mode) {
    for(var mode in MODES) {
        if(suggested_mode === MODES[mode]) {
            return mode;
        }
    }
    return false;
}

var args = process.argv;
if(args && args.length <= 2) {
    // No arguments supplied, just node and the app script's path.
    mode = MODES.recent;
} else if(args && args.length >= 3) {
    var suggested_mode = args[2];
    mode = is_valid_mode(suggested_mode);
    if(mode === MODES.catalog || mode === MODES.single) {
        if(args.length >= 4) {
            reference = args[3].split(',');
        }
    }
}

// Report any runtime errors on mode selection.
if(!mode) {
    console.error('Invalid mode - please select from:');
    for(var m in MODES) {
        console.error(' * ' +MODES[m]+ ': '+MODE_DESCRIPTIONS[m]);
    }
    process.exit(1);
} else if(mode === MODES.single) {
    // In the single mode, each asset is a combination of a catalog alias
    // and the asset ID, eg. DNT/101
    for(var r in reference) {
        reference[r] = reference[r].split('-');
    }
}

/*=== END: Defining modes to run the syncronization in ===*/

// Creates the index in the Elasticsearch index.
function create_index() {
    return client.indices.create({
        index: 'assets'
    }).then(function() {
        console.log('Index created');
    });
}

// Cleans up a string, representing a category.
function clean_string(str) {
    var regexp = new RegExp('^[0-9]+[A-Z]+.[0-9]+.[0-9]+ ');
    if(str.search(regexp) === 0) {
        return str.replace(regexp, '');
    }
    regexp = new RegExp('^[A-Z]?[0-9]+[a-z]? - ');
    if(str.search(regexp) === 0) {
        return str.replace(regexp, '');
    }
    regexp = new RegExp('^[A-F] - ');
    if(str.search(regexp) === 0) {
        return str.replace(regexp, '');
    }
    regexp = new RegExp('^[0-9][0-9] ');
    if(str.search(regexp) === 0) {
        return str.replace(regexp, '');
    }
    return str;
}

// Determines wether or not an asset should be visible to the user when searching.
function determine_searchability(cip_client, asset, formatted_result) {
    // First of all - we wouldn't like to have search results on assets
    // which have been cropped into seperate assets.
    if(formatted_result.cropping_status && formatted_result.cropping_status.id === 2) {
        return false;
    } else {
        // Second - We wouldn't like assets which are related to assets that are in the
        // "Rotationsbilleder" category - as these are side-views of an object which will be
        // reachable through the front-facing master asset.
        return cip.get_related_assets(asset, 'isalternateof')
        .then(function parse_relations(related_assets) {
            var related_asset_promises = [];
            for(var i in related_assets.ids) {
                var related_asset_id = related_assets.ids[i];
                var related_asset_promise = cip.get_asset(cip_client, formatted_result.catalog, related_asset_id);
                related_asset_promises.push( related_asset_promise );
            }
            return Q.all(related_asset_promises).then(function(related_assets) {
                for(var r in related_assets) {
                    for(var a in related_assets[r]) {
                        var related_asset = related_assets[r][a];
                        var formatted_asset = asset_mapping.format_result(related_asset.fields);
                        for(var c in formatted_asset.categories) {
                            var category = formatted_asset.categories[c];
                            if(category.name === 'Rotationsbilleder') {
                                return false;
                            }
                        }
                    }
                }
                // None of the related assets was a part of the rotational images category.
                return true;
            });
        });
    }
}

var categories = {};
var catalog_page_index = {};
var catalog_index = 0;

// Handle a specific asset from a result page.
function handle_asset(cip_client, asset, catalog_alias) {
    var formatted_result = asset_mapping.format_result(asset.fields);
    formatted_result.catalog = catalog_alias;

    return Q.all([
        asset_mapping.extend_metadata(cip_client, catalog_alias, asset, formatted_result),
        determine_searchability(cip_client, asset, formatted_result)
    ])
    .spread(function(formatted_result, is_searchable) {
        formatted_result.searchable = is_searchable;

        if(formatted_result.modification_time !== undefined) {
            var re = new RegExp('\\d+');
            var re_result = re.exec(formatted_result.modification_time);
            if(re_result && re_result.length > 0) {
                formatted_result.modification_time = parseInt(re_result[0], 10);
            }
        }

        if(formatted_result.categories !== undefined) {
            formatted_result.categories_int = [];
            formatted_result.suggest = {'input': []};

            for(var j=0; j < formatted_result.categories.length; ++j) {
                if(formatted_result.categories[j].path.indexOf('$Categories') !== 0) {
                    continue;
                }

                var path = categories[catalog_alias].get_path(formatted_result.categories[j].id);
                if(path) {
                    for(var k=0; k < path.length; k++) {
                        formatted_result.categories_int.push(path[k].id);

                        if(path[k].name.indexOf('$Categories') === 0) {
                            continue;
                        }

                        formatted_result.suggest.input.push(clean_string(path[k].name));
                    }
                }
            }
        }
        return formatted_result;
    })
    .then(function(formatted_result) {
        var es_id = formatted_result.catalog + '-' + formatted_result.id;
        return client.index({
            index: 'assets',
            type: 'asset',
            id: es_id,
            body: formatted_result
        });
    })
    .then(function(resp) {
        console.log('Successfully indexed ' + resp._id);
    })
    .fail(function(err) {
        console.error( 'Something went wrong indexing an asset' );
        console.error( err.stack );
    });
}

// Handle a specific result page, with assets.
function handle_result_page(cip_client, catalog, result, page_index) {
    var deferred = Q.defer();
    var total_pages = Math.ceil(result.total_rows / ASSETS_PER_REQUEST);
    console.log('Queuing page number', page_index+1, 'of', total_pages+1, 'in the', catalog.alias, 'catalog.');

    result.get(ASSETS_PER_REQUEST, page_index * ASSETS_PER_REQUEST, function(assets_on_page) {
        console.log('Got metadata of page ' +(page_index+1)+ ' from the ' +result.catalog.alias+ ' catalog.');
        var asset_promises = [];
        for(var a in assets_on_page) {
            var asset = assets_on_page[a];
            var asset_promise = handle_asset(cip_client, asset, catalog.alias);
            asset_promises.push( asset_promise );
        }
        Q.all(asset_promises).then(function() {
            // Resolve the page promise once all assets on the page has been resolved.
            deferred.resolve( true );
        });
    });

    return deferred.promise;
}

// Recursively handle the assets on pages, giving a breath first traversal
// of the CIP webservice.
function handle_next_result_page(cip_client, catalog, result) {
    var page_index = catalog_page_index[catalog.alias];
    if(page_index * ASSETS_PER_REQUEST < result.total_rows) {
        catalog_page_index[catalog.alias]++;
        return handle_result_page(cip_client, catalog, result, page_index)
        .then(function() {
            return handle_next_result_page(cip_client, catalog, result);
        });
    } else {
        return true; // No more pages in the result.
    }
}

// Handle a specific catalog.
function handle_catalog(cip_client, catalog) {
    // Precondition: The catalog has it's alias defined.
    if(catalog === undefined || catalog.alias === undefined) {
        throw new Error('The catalog´s alias was undefined');
    }
    console.log('Queuing catalog', catalog.alias);
    var modified_since = (mode === MODES.recent ? '$today-2d' : '2003-04-24');
    // Let's find out how many pages of assets we have in this catalog.
    return cip.get_recent_assets(cip_client, catalog, modified_since )
    .then(function(result) {
        // Reset the counter.
        catalog_page_index[catalog.alias] = 0;
        return Q.when(handle_next_result_page(result, catalog, result), function() {
            console.log('Done parsing catalog', catalog.alias);
        });
    });
}

// Recursively handle the catalogs, giving a breath first traversal
// of the CIP webservice.
function handle_next_catalog(cip_client, catalogs) {
    if(catalog_index < catalogs.length) {
        var catalog = catalogs[catalog_index];
        catalog_index++;
        return handle_catalog(cip_client, catalog)
        .then(function() {
            return handle_next_catalog(cip_client, catalogs);
        });
    } else {
        return true; // No more catalogs.
    }
}

// Let's get started.
console.log('Running in mode: ' + MODE_DESCRIPTIONS[mode]);

var main_queue = cip_categories.load_categories()
.then(function(result) {
    // The categories pr catalog has been fetched from Cumulus.
    for(var i=0; i < result.length; ++i) {
        categories[result[i].id] = result[i];
    }
    var categories_count = Object.keys(categories).length;
    console.log('Loaded categories for', categories_count, 'catalogs');
})
.then(function() {
    return create_index().then(function() {
        console.log('Index created');
    }, function(err) {
        // TODO: Add a recursive check for this message.
        if(err.message === 'IndexAlreadyExistsException[[assets] already exists]') {
            return; // No worries ...
        }
        console.log('Failed to create the index:', err);
    });
})
.then(cip.init_session);

if(mode === MODES.recent || mode === MODES.all || mode === MODES.catalog) {
    main_queue = main_queue.then(function(cip_client) {
        return [cip_client, cip.get_catalogs(cip_client)];
    })
    .spread(function(cip_client, catalogs) {
        var relevant_catalogs = [];
        if(mode === MODES.catalog) {
            for(var c in catalogs) {
                var catalog = catalogs[c];
                if(reference.indexOf(catalog.alias) !== -1) {
                    relevant_catalogs.push(catalog);
                }
            }
        } else {
            // All catalogs are relevant catalogs.
            relevant_catalogs = catalogs;
        }
        return handle_next_catalog(cip_client, relevant_catalogs);
    });
} else {
    main_queue = main_queue.then(function(cip_client) {
        var asset_promises = [];
        for(var a in reference) {
            var catalog_alias = reference[a][0];
            var asset_id = reference[a][1];
            var asset_promise = cip.get_asset(cip_client, catalog_alias, asset_id)
            .then(function(assets) {
                if(assets.length === 1) {
                    console.log('Logging asset ', assets[0].fields.id);
                    return handle_asset(cip_client, assets[0], catalog_alias);
                } else {
                    throw new Error( 'No asset with id ' +asset_id+ ' was found in the ' +catalog_alias+ ' catalog.' );
                }
            });
            asset_promises.push( asset_promise );
        }
        return Q.all(asset_promises);
    });
}

main_queue
.fail(function (error) {
    console.error('An error occurred');
    console.error(error.stack);
}).finally(function() {
    console.log('=== All done ===');
    // We are ready to die ..
    process.exit(0);
});<|endoftext|>


<commit_before>import React from 'react';
import styled from '@emotion/styled';
import { css } from '@emotion/core';
import { faClock, faEdit } from '@fortawesome/free-regular-svg-icons';
import {
  faDirections,
  faList,
  faTimes,
  faCheck,
  faPoundSign,
  faBaby,
  faToilet,
  faMale,
  faFemale,
  faChild,
  faKey,
  faCog,
  faQuestion,
  faChevronDown,
} from '@fortawesome/free-solid-svg-icons';
import { faAccessibleIcon } from '@fortawesome/free-brands-svg-icons';
import lightFormat from 'date-fns/lightFormat';
import getISODay from 'date-fns/getISODay';
import parseISO from 'date-fns/parseISO';
import { Link } from 'react-router-dom';
import useComponentSize from '@rehooks/component-size';
import L from 'leaflet';
import { mutate } from 'swr';
import { loader } from 'graphql.macro';
import { print } from 'graphql/language/printer';

import Box from './Box';
import Button from './Button';
import Text from './Text';
import Spacer from './Spacer';
import Icon from './Icon';
import { Media } from './Media';
// Suppress Opening Hours Heading during COVID-19
import { /* getIsOpen, */ WEEKDAYS, rangeTypes } from '../openingTimes';
import { useMapState } from './MapState';
import { useMutation } from '../graphql/fetcher';

const FIND_LOO_BY_ID_QUERY = print(loader('../graphql/findLooById.graphql'));

const Grid = styled(Box)`
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  justify-content: center;
  margin: -${({ theme }) => theme.space[3]}px;
`;

const UnstyledList = styled.ul`
  list-style: none;
`;

function getTimeRangeLabel(range) {
  if (range === rangeTypes.CLOSED) {
    return 'Closed';
  }

  if (range && range.length === 2) {
    if (range[0] === range[1]) {
      return '24 Hours';
    }

    return range.join(' - ');
  }

  return 'Unknown';
}

// Suppress Opening Hours heading during COVID-19
// function getIsOpenLabel(openingTimes = [], dateTime = new Date()) {
//   const isOpen = getIsOpen(openingTimes, dateTime);

//   if (isOpen === null) {
//     return 'Unknown';
//   }

//   return isOpen ? 'Open now' : 'Closed';
// }

const SUBMIT_VERIFICATION_REPORT_MUTATION = `
  mutation submitVerificationReportMutation($id: ID) {
    submitVerificationReport(id: $id) {
      loo {
        id
        verifiedAt
      }
    }
  }
`;

function round(value, precision = 0) {
  const multiplier = Math.pow(10, precision);
  return Math.round(value * multiplier) / multiplier;
}

const DistanceTo = ({ from, to }) => {
  const fromLatLng = L.latLng(from.lat, from.lng);

  const toLatLng = L.latLng(to.lat, to.lng);
  const metersToLoo = fromLatLng.distanceTo(toLatLng);

  const distance =
    metersToLoo < 1000
      ? `${round(metersToLoo, 0)}m`
      : `${round(metersToLoo / 1000, 1)}km`;

  return (
    <Text as="span" fontSize="3" fontWeight="bold">
      {distance}
    </Text>
  );
};

const ToiletDetailsPanel = ({
  data,
  isLoading,
  onDimensionsChange,
  startExpanded = false,
  children,
}) => {
  const [isExpanded, setIsExpanded] = React.useState(startExpanded);

  const [
    submitVerificationMutation,
    { loading: submitVerificationLoading },
  ] = useMutation(SUBMIT_VERIFICATION_REPORT_MUTATION);

  const submitVerificationReport = async (variables) => {
    const responseData = await submitVerificationMutation(variables);

    // update the local cache with the new data
    mutate(
      [FIND_LOO_BY_ID_QUERY, JSON.stringify({ id: data.id })],
      {
        loo: {
          ...data,
          verifiedAt: responseData.submitVerificationReport.loo.verifiedAt,
        },
      },
      false
    );
  };

  const [mapState] = useMapState();

  // programmatically set focus on close button when panel expands
  const closeButtonRef = React.useRef(null);
  React.useEffect(() => {
    if (isExpanded && closeButtonRef.current) {
      closeButtonRef.current.focus();
    }
  }, [isExpanded]);

  // call onDimensionsChange whenever the dimensions of the container change
  const containerRef = React.useRef(null);
  const size = useComponentSize(containerRef);
  React.useEffect(() => {
    onDimensionsChange(size);
  }, [size, onDimensionsChange]);

  if (isLoading) {
    return (
      <Box
        width="100%"
        color="primary"
        bg="white"
        minHeight={100}
        borderTopLeftRadius={4}
        borderTopRightRadius={4}
        padding={4}
        display="flex"
        alignItems="center"
        ref={containerRef}
      >
        Loading toilet...
      </Box>
    );
  }

  const titleFragment = (
    <Box display="flex" justifyContent="space-between">
      <Text fontWeight="bold" fontSize={[3, 4]} lineHeight={1.2}>

{data.name || 'Unnamed Toilet'}

      </Text>
      {mapState.geolocation && (
        <Box ml={5}>
          <DistanceTo from={mapState.geolocation} to={data.location} />
        </Box>
      )}
    </Box>
  );

  const getDirectionsFragment = (
    <Button
      icon={<Icon icon={faDirections} />}
      as="a"
      href={`https://maps.apple.com/?dirflg=w&daddr=${[
        data.location.lat,
        data.location.lng,
      ]}`}
      target="_blank"
      rel="noopener noreferrer"
    >
      Directions
    </Button>
  );

  const crossIconFragment = <Icon icon={faTimes} color="tertiary" />;
  const questionIconFragment = <Icon icon={faQuestion} color="tertiary" />;
  const checkIconFragment = <Icon icon={faCheck} />;

  const getFeatureValueIcon = (value) => {
    if (value === null) {
      return questionIconFragment;
    }

    return value ? checkIconFragment : crossIconFragment;
  };

  const features = [
    {
      icon: <Icon icon={faPoundSign} />,
      label: 'Free',
      valueIcon: getFeatureValueIcon(data.noPayment),
    },
    {
      icon: <Icon icon={faBaby} />,
      label: 'Baby Changing',
      valueIcon: getFeatureValueIcon(data.babyChange),
    },
    {
      icon: <Icon icon={faAccessibleIcon} />,
      label: 'Accessible',
      valueIcon: getFeatureValueIcon(data.accessible),
    },
    ...(data.accessible
      ? [
          {
            icon: <Icon icon={faKey} />,
            label: 'RADAR Key',
            valueIcon: getFeatureValueIcon(data.radar),
          },
        ]
      : []),
    {
      icon: <Icon icon={faToilet} />,
      label: 'Gender Neutral',
      valueIcon: getFeatureValueIcon(data.allGender),
    },
    {
      icon: <Icon icon={faFemale} />,
      label: 'Female Toilets',
      valueIcon: getFeatureValueIcon(data.female),
    },
    {
      icon: <Icon icon={faMale} />,
      label: 'Male Toilets',
      valueIcon: getFeatureValueIcon(data.male),
    },
    {
      icon: <Icon icon={faChild} />,
      label: 'Children Only',
      valueIcon: getFeatureValueIcon(data.childrenOnly),
    },
    {
      icon: <Icon icon={faToilet} />,
      label: 'Urinal Only',
      valueIcon: getFeatureValueIcon(data.urinalOnly),
    },
    {
      icon: <Icon icon={faCog} />,
      label: 'Automatic',
      valueIcon: getFeatureValueIcon(data.automatic),
    },
  ];

  const openingTimes = data.openingTimes || WEEKDAYS.map(() => null);

  const todayWeekdayIndex = getISODay(new Date());

  const editUrl = `/loos/${data.id}/edit`;

  const updatedAt = parseISO(data.updatedAt);
  let verifiedOrUpdatedDate = updatedAt;
  if (data.verifiedAt) {
    const verifiedAt = parseISO(data.verifiedAt);
    if (updatedAt < verifiedAt) {
      verifiedOrUpdatedDate = verifiedAt;
    }
  }

  const lastVerifiedFragment = (
    <Box>
      <Text fontWeight="bold">

Is this information correct?

      </Text>
      <Spacer mb={2} />
      <Box display="flex" alignItems="center">
        <Button
          onClick={() => submitVerificationReport({ id: data.id })}
          disabled={submitVerificationLoading}
        >
          Yes
        </Button>
        <Spacer mr={4} />
        <Box display="flex" alignItems="center">
          No?
          <Spacer mr={2} />
          <Button
            variant="secondary"
            icon={<Icon icon={faEdit} />}
            as={Link}
            to={editUrl}
          >
            Edit
          </Button>
        </Box>
      </Box>
      <Spacer mb={[0, 2]} />
      Last verified:{' '}
      {lightFormat(verifiedOrUpdatedDate, 'dd/MM/yyyy, hh:mm aa')}
    </Box>
  );

  if (isExpanded) {
    return (
      <Box
        width="100%"
        color="primary"
        bg="white"
        borderTopLeftRadius={[3, 4]}
        borderTopRightRadius={[3, 4]}
        as="section"
        aria-labelledby="toilet-details-heading"
        ref={containerRef}
      >
        <Media greaterThanOrEqual="md">
          <Box position="absolute" top={30} right={30}>
            <button
              type="button"
              aria-label="Close toilet details"
              onClick={() => setIsExpanded(false)}
              aria-expanded="true"
              ref={closeButtonRef}
            >
              <Box
                height={26}
                width={26}
                display="flex"
                alignItems="center"
                justifyContent="center"
                borderRadius={13}
                borderColor="primary"
                borderWidth={1}
                borderStyle="solid"
              >
                <Icon icon={faTimes} />
              </Box>
            </button>
          </Box>
        </Media>

        <Media lessThan="md">
          <Box display="flex" justifyContent="center" paddingTop={2}>
            <Box
              as="button"
              type="button"
              aria-label="Close toilet details"
              onClick={() => setIsExpanded(false)}
              aria-expanded="true"
              padding={2}
              ref={closeButtonRef}
            >
              <Icon icon={faChevronDown} />
            </Box>
          </Box>
        </Media>

        <Box
          maxHeight={[325, 400]}
          overflow="auto"
          padding={[3, 4]}
          paddingTop={[0, 4]}
          paddingRight={[4, 5]}
          css={css`
            scroll-behavior: smooth;
          `}
        >
          <Grid>
            <Box
              width={['100%', '50%', '25%']}
              padding={3}
              id="#toilet-details-heading"
            >
              {titleFragment}
              <Spacer mb={2} />
              {getDirectionsFragment}
              <Media greaterThanOrEqual="md">
                <Spacer mb={4} />
                {lastVerifiedFragment}
              </Media>
            </Box>

            <Box width={['100%', '50%', '25%']} padding={3}>
              <Text fontWeight="bold">

Features

              </Text>
              <Spacer mb={2} />
              <UnstyledList>
                {features.map((feature) => (
                  <Box
                    as="li"
                    key={feature.label}
                    display="flex"
                    alignItems="center"
                    justifyContent="space-between"
                    mb={1}
                  >
                    <Box display="flex" alignItems="center">
                      <Box width="20px" display="flex" justifyContent="center">
                        {feature.icon}
                      </Box>
                      <Spacer mr={2} />
                      {feature.label}
                    </Box>
                    {feature.valueIcon}
                  </Box>
                ))}
              </UnstyledList>
            </Box>

            <Box width={['100%', '50%', '25%']} padding={3}>
              {data.noPayment === false && (
                <>
                  <Text fontWeight="bold">

Fee

                  </Text>
                  <Spacer mb={2} />
                  {data.paymentDetails || 'Unknown'}
                  <Spacer mb={3} />
                </>
              )}
              {Boolean(data.notes) && (
                <>
                  <Text fontWeight="bold">

Notes

                  </Text>
                  <Spacer mb={2} />

                    {data.notes.split('\n').map((string, i) => (

{string}

                    ))}

                </>
              )}
            </Box>

            <Box width={['100%', '50%', '25%']} padding={3}>
              <Box display="flex" alignItems="center">
                <Icon icon={faClock} />
                <Spacer mr={2} />
                <Text fontWeight="bold">

Opening Hours

                </Text>
              </Box>
              <Spacer mb={[0, 2]} />
              <UnstyledList>
                {openingTimes.map((timeRange, i) => (
                  <Box
                    as="li"
                    display="flex"
                    justifyContent="space-between"
                    key={i}
                    padding={1}
                    bg={i === todayWeekdayIndex ? 'ice' : 'white'}
                  >
                    {WEEKDAYS[i]}
                    {getTimeRangeLabel(timeRange)}
                  </Box>
                ))}
              </UnstyledList>
              <Spacer mb={2} />
              <Text fontSize={1} color="grey">
                Hours may vary with national holidays or seasonal changes. If
                you know these hours to be out of date please{' '}
                <Button as={Link} to={editUrl} variant="link">
                  edit this toilet
                </Button>
                .
              </Text>

              <Media lessThan="md">
                <Spacer mb={4} />
                {lastVerifiedFragment}
              </Media>
            </Box>

            <Media lessThan="md">
              <Box
                display="flex"
                justifyContent="center"
                width="100%"
                padding={2}
                marginBottom={2}
              >
                <Button as="a" variant="link" href="#toilet-details-heading">
                  Back to top
                </Button>
              </Box>
            </Media>
          </Grid>

          {children}
        </Box>
      </Box>
    );
  }

  return (
    <Box
      width="100%"
      color="primary"
      bg="white"
      minHeight={100}
      borderTopLeftRadius={[3, 4]}
      borderTopRightRadius={[3, 4]}
      padding={[3, 4]}
      as="section"
      aria-labelledby="toilet-details-heading"
      ref={containerRef}
    >
      <Grid>
        <Box width={['100%', '50%', '25%']} padding={[3, 4]}>
          {titleFragment}
        </Box>

        <Box width={['100%', '50%', '25%']} padding={[3, 4]}>
          {/* Supress opening hours heading during COVID-19
          <Box display="flex" alignItems="center">
            <Icon icon={faClock} />
            <Spacer mr={2} />
            <Text fontWeight="bold">

Opening Hours

            </Text>
          </Box>
          <Spacer mb={[0, 2]} />
          {getIsOpenLabel(openingTimes)} */}
        </Box>

        <Box
          width={['100%', '50%']}
          padding={3}
          display="flex"
          justifyContent={['flex-start', 'flex-start', 'flex-end']}
          alignItems="center"
        >
          {getDirectionsFragment}
          <Spacer mr={2} />
          <Button
            variant="secondary"
            icon={<Icon icon={faList} />}
            onClick={() => setIsExpanded(true)}
            aria-expanded="false"
          >
            Details
          </Button>
        </Box>
      </Grid>
    </Box>
  );
};

export default ToiletDetailsPanel;
<commit_msg>fix: differentiate verify and update<commit_after>import React from 'react';
import styled from '@emotion/styled';
import { css } from '@emotion/core';
import { faClock, faEdit } from '@fortawesome/free-regular-svg-icons';
import {
  faDirections,
  faList,
  faTimes,
  faCheck,
  faPoundSign,
  faBaby,
  faToilet,
  faMale,
  faFemale,
  faChild,
  faKey,
  faCog,
  faQuestion,
  faChevronDown,
} from '@fortawesome/free-solid-svg-icons';
import { faAccessibleIcon } from '@fortawesome/free-brands-svg-icons';
import lightFormat from 'date-fns/lightFormat';
import getISODay from 'date-fns/getISODay';
import parseISO from 'date-fns/parseISO';
import add from 'date-fns/add';
import { Link } from 'react-router-dom';
import useComponentSize from '@rehooks/component-size';
import L from 'leaflet';
import { mutate } from 'swr';
import { loader } from 'graphql.macro';
import { print } from 'graphql/language/printer';

import Box from './Box';
import Button from './Button';
import Text from './Text';
import Spacer from './Spacer';
import Icon from './Icon';
import { Media } from './Media';
// Suppress Opening Hours Heading during COVID-19
import { /* getIsOpen, */ WEEKDAYS, rangeTypes } from '../openingTimes';
import { useMapState } from './MapState';
import { useMutation } from '../graphql/fetcher';

const FIND_LOO_BY_ID_QUERY = print(loader('../graphql/findLooById.graphql'));

const Grid = styled(Box)`
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  justify-content: center;
  margin: -${({ theme }) => theme.space[3]}px;
`;

const UnstyledList = styled.ul`
  list-style: none;
`;

function getTimeRangeLabel(range) {
  if (range === rangeTypes.CLOSED) {
    return 'Closed';
  }

  if (range && range.length === 2) {
    if (range[0] === range[1]) {
      return '24 Hours';
    }

    return range.join(' - ');
  }

  return 'Unknown';
}

// Suppress Opening Hours heading during COVID-19
// function getIsOpenLabel(openingTimes = [], dateTime = new Date()) {
//   const isOpen = getIsOpen(openingTimes, dateTime);

//   if (isOpen === null) {
//     return 'Unknown';
//   }

//   return isOpen ? 'Open now' : 'Closed';
// }

const SUBMIT_VERIFICATION_REPORT_MUTATION = `
  mutation submitVerificationReportMutation($id: ID) {
    submitVerificationReport(id: $id) {
      loo {
        id
        verifiedAt
      }
    }
  }
`;

function round(value, precision = 0) {
  const multiplier = Math.pow(10, precision);
  return Math.round(value * multiplier) / multiplier;
}

const DistanceTo = ({ from, to }) => {
  const fromLatLng = L.latLng(from.lat, from.lng);

  const toLatLng = L.latLng(to.lat, to.lng);
  const metersToLoo = fromLatLng.distanceTo(toLatLng);

  const distance =
    metersToLoo < 1000
      ? `${round(metersToLoo, 0)}m`
      : `${round(metersToLoo / 1000, 1)}km`;

  return (
    <Text as="span" fontSize="3" fontWeight="bold">
      {distance}
    </Text>
  );
};

const ToiletDetailsPanel = ({
  data,
  isLoading,
  onDimensionsChange,
  startExpanded = false,
  children,
}) => {
  const [isExpanded, setIsExpanded] = React.useState(startExpanded);

  const [
    submitVerificationMutation,
    { loading: submitVerificationLoading },
  ] = useMutation(SUBMIT_VERIFICATION_REPORT_MUTATION);

  const submitVerificationReport = async (variables) => {
    const responseData = await submitVerificationMutation(variables);

    // update the local cache with the new data
    mutate(
      [FIND_LOO_BY_ID_QUERY, JSON.stringify({ id: data.id })],
      {
        loo: {
          ...data,
          verifiedAt: responseData.submitVerificationReport.loo.verifiedAt,
        },
      },
      false
    );
  };

  const [mapState] = useMapState();

  // programmatically set focus on close button when panel expands
  const closeButtonRef = React.useRef(null);
  React.useEffect(() => {
    if (isExpanded && closeButtonRef.current) {
      closeButtonRef.current.focus();
    }
  }, [isExpanded]);

  // call onDimensionsChange whenever the dimensions of the container change
  const containerRef = React.useRef(null);
  const size = useComponentSize(containerRef);
  React.useEffect(() => {
    onDimensionsChange(size);
  }, [size, onDimensionsChange]);

  if (isLoading) {
    return (
      <Box
        width="100%"
        color="primary"
        bg="white"
        minHeight={100}
        borderTopLeftRadius={4}
        borderTopRightRadius={4}
        padding={4}
        display="flex"
        alignItems="center"
        ref={containerRef}
      >
        Loading toilet...
      </Box>
    );
  }

  const titleFragment = (
    <Box display="flex" justifyContent="space-between">
      <Text fontWeight="bold" fontSize={[3, 4]} lineHeight={1.2}>

{data.name || 'Unnamed Toilet'}

      </Text>
      {mapState.geolocation && (
        <Box ml={5}>
          <DistanceTo from={mapState.geolocation} to={data.location} />
        </Box>
      )}
    </Box>
  );

  const getDirectionsFragment = (
    <Button
      icon={<Icon icon={faDirections} />}
      as="a"
      href={`https://maps.apple.com/?dirflg=w&daddr=${[
        data.location.lat,
        data.location.lng,
      ]}`}
      target="_blank"
      rel="noopener noreferrer"
    >
      Directions
    </Button>
  );

  const crossIconFragment = <Icon icon={faTimes} color="tertiary" />;
  const questionIconFragment = <Icon icon={faQuestion} color="tertiary" />;
  const checkIconFragment = <Icon icon={faCheck} />;

  const getFeatureValueIcon = (value) => {
    if (value === null) {
      return questionIconFragment;
    }

    return value ? checkIconFragment : crossIconFragment;
  };

  const features = [
    {
      icon: <Icon icon={faPoundSign} />,
      label: 'Free',
      valueIcon: getFeatureValueIcon(data.noPayment),
    },
    {
      icon: <Icon icon={faBaby} />,
      label: 'Baby Changing',
      valueIcon: getFeatureValueIcon(data.babyChange),
    },
    {
      icon: <Icon icon={faAccessibleIcon} />,
      label: 'Accessible',
      valueIcon: getFeatureValueIcon(data.accessible),
    },
    ...(data.accessible
      ? [
          {
            icon: <Icon icon={faKey} />,
            label: 'RADAR Key',
            valueIcon: getFeatureValueIcon(data.radar),
          },
        ]
      : []),
    {
      icon: <Icon icon={faToilet} />,
      label: 'Gender Neutral',
      valueIcon: getFeatureValueIcon(data.allGender),
    },
    {
      icon: <Icon icon={faFemale} />,
      label: 'Female Toilets',
      valueIcon: getFeatureValueIcon(data.female),
    },
    {
      icon: <Icon icon={faMale} />,
      label: 'Male Toilets',
      valueIcon: getFeatureValueIcon(data.male),
    },
    {
      icon: <Icon icon={faChild} />,
      label: 'Children Only',
      valueIcon: getFeatureValueIcon(data.childrenOnly),
    },
    {
      icon: <Icon icon={faToilet} />,
      label: 'Urinal Only',
      valueIcon: getFeatureValueIcon(data.urinalOnly),
    },
    {
      icon: <Icon icon={faCog} />,
      label: 'Automatic',
      valueIcon: getFeatureValueIcon(data.automatic),
    },
  ];

  const openingTimes = data.openingTimes || WEEKDAYS.map(() => null);

  const todayWeekdayIndex = getISODay(new Date());

  const editUrl = `/loos/${data.id}/edit`;

  const updatedAt = parseISO(data.updatedAt);
  let verifiedOrUpdatedDate = updatedAt;
  let verifiedOrUpdated = 'updated';
  if (data.verifiedAt) {
    const verifiedAt = parseISO(data.verifiedAt);
    // Add a minute for the comparison to account for the fact that verification causes updatedAt to update :-(
    if (updatedAt < add(verifiedAt, { minutes: 1 })) {
      verifiedOrUpdatedDate = verifiedAt;
      verifiedOrUpdated = 'verified';
    }
  }

  const lastVerifiedFragment = (
    <Box>
      <Text fontWeight="bold">

Is this information correct?

      </Text>
      <Spacer mb={2} />
      <Box display="flex" alignItems="center">
        <Button
          onClick={() => submitVerificationReport({ id: data.id })}
          disabled={submitVerificationLoading}
        >
          Yes
        </Button>
        <Spacer mr={4} />
        <Box display="flex" alignItems="center">
          No?
          <Spacer mr={2} />
          <Button
            variant="secondary"
            icon={<Icon icon={faEdit} />}
            as={Link}
            to={editUrl}
          >
            Edit
          </Button>
        </Box>
      </Box>
      <Spacer mb={[0, 2]} />
      Last {verifiedOrUpdated}:{' '}
      <Link to={`/explorer/loos/${data.id}`}>
        {lightFormat(verifiedOrUpdatedDate, 'dd/MM/yyyy, hh:mm aa')}
      </Link>
    </Box>
  );

  if (isExpanded) {
    return (
      <Box
        width="100%"
        color="primary"
        bg="white"
        borderTopLeftRadius={[3, 4]}
        borderTopRightRadius={[3, 4]}
        as="section"
        aria-labelledby="toilet-details-heading"
        ref={containerRef}
      >
        <Media greaterThanOrEqual="md">
          <Box position="absolute" top={30} right={30}>
            <button
              type="button"
              aria-label="Close toilet details"
              onClick={() => setIsExpanded(false)}
              aria-expanded="true"
              ref={closeButtonRef}
            >
              <Box
                height={26}
                width={26}
                display="flex"
                alignItems="center"
                justifyContent="center"
                borderRadius={13}
                borderColor="primary"
                borderWidth={1}
                borderStyle="solid"
              >
                <Icon icon={faTimes} />
              </Box>
            </button>
          </Box>
        </Media>

        <Media lessThan="md">
          <Box display="flex" justifyContent="center" paddingTop={2}>
            <Box
              as="button"
              type="button"
              aria-label="Close toilet details"
              onClick={() => setIsExpanded(false)}
              aria-expanded="true"
              padding={2}
              ref={closeButtonRef}
            >
              <Icon icon={faChevronDown} />
            </Box>
          </Box>
        </Media>

        <Box
          maxHeight={[325, 400]}
          overflow="auto"
          padding={[3, 4]}
          paddingTop={[0, 4]}
          paddingRight={[4, 5]}
          css={css`
            scroll-behavior: smooth;
          `}
        >
          <Grid>
            <Box
              width={['100%', '50%', '25%']}
              padding={3}
              id="#toilet-details-heading"
            >
              {titleFragment}
              <Spacer mb={2} />
              {getDirectionsFragment}
              <Media greaterThanOrEqual="md">
                <Spacer mb={4} />
                {lastVerifiedFragment}
              </Media>
            </Box>

            <Box width={['100%', '50%', '25%']} padding={3}>
              <Text fontWeight="bold">

Features

              </Text>
              <Spacer mb={2} />
              <UnstyledList>
                {features.map((feature) => (
                  <Box
                    as="li"
                    key={feature.label}
                    display="flex"
                    alignItems="center"
                    justifyContent="space-between"
                    mb={1}
                  >
                    <Box display="flex" alignItems="center">
                      <Box width="20px" display="flex" justifyContent="center">
                        {feature.icon}
                      </Box>
                      <Spacer mr={2} />
                      {feature.label}
                    </Box>
                    {feature.valueIcon}
                  </Box>
                ))}
              </UnstyledList>
            </Box>

            <Box width={['100%', '50%', '25%']} padding={3}>
              {data.noPayment === false && (
                <>
                  <Text fontWeight="bold">

Fee

                  </Text>
                  <Spacer mb={2} />
                  {data.paymentDetails || 'Unknown'}
                  <Spacer mb={3} />
                </>
              )}
              {Boolean(data.notes) && (
                <>
                  <Text fontWeight="bold">

Notes

                  </Text>
                  <Spacer mb={2} />

                    {data.notes.split('\n').map((string, i) => (

{string}

                    ))}

                </>
              )}
            </Box>

            <Box width={['100%', '50%', '25%']} padding={3}>
              <Box display="flex" alignItems="center">
                <Icon icon={faClock} />
                <Spacer mr={2} />
                <Text fontWeight="bold">

Opening Hours

                </Text>
              </Box>
              <Spacer mb={[0, 2]} />
              <UnstyledList>
                {openingTimes.map((timeRange, i) => (
                  <Box
                    as="li"
                    display="flex"
                    justifyContent="space-between"
                    key={i}
                    padding={1}
                    bg={i === todayWeekdayIndex ? 'ice' : 'white'}
                  >
                    {WEEKDAYS[i]}
                    {getTimeRangeLabel(timeRange)}
                  </Box>
                ))}
              </UnstyledList>
              <Spacer mb={2} />
              <Text fontSize={1} color="grey">
                Hours may vary with national holidays or seasonal changes. If
                you know these hours to be out of date please{' '}
                <Button as={Link} to={editUrl} variant="link">
                  edit this toilet
                </Button>
                .
              </Text>

              <Media lessThan="md">
                <Spacer mb={4} />
                {lastVerifiedFragment}
              </Media>
            </Box>

            <Media lessThan="md">
              <Box
                display="flex"
                justifyContent="center"
                width="100%"
                padding={2}
                marginBottom={2}
              >
                <Button as="a" variant="link" href="#toilet-details-heading">
                  Back to top
                </Button>
              </Box>
            </Media>
          </Grid>

          {children}
        </Box>
      </Box>
    );
  }

  return (
    <Box
      width="100%"
      color="primary"
      bg="white"
      minHeight={100}
      borderTopLeftRadius={[3, 4]}
      borderTopRightRadius={[3, 4]}
      padding={[3, 4]}
      as="section"
      aria-labelledby="toilet-details-heading"
      ref={containerRef}
    >
      <Grid>
        <Box width={['100%', '50%', '25%']} padding={[3, 4]}>
          {titleFragment}
        </Box>

        <Box width={['100%', '50%', '25%']} padding={[3, 4]}>
          {/* Supress opening hours heading during COVID-19
          <Box display="flex" alignItems="center">
            <Icon icon={faClock} />
            <Spacer mr={2} />
            <Text fontWeight="bold">

Opening Hours

            </Text>
          </Box>
          <Spacer mb={[0, 2]} />
          {getIsOpenLabel(openingTimes)} */}
        </Box>

        <Box
          width={['100%', '50%']}
          padding={3}
          display="flex"
          justifyContent={['flex-start', 'flex-start', 'flex-end']}
          alignItems="center"
        >
          {getDirectionsFragment}
          <Spacer mr={2} />
          <Button
            variant="secondary"
            icon={<Icon icon={faList} />}
            onClick={() => setIsExpanded(true)}
            aria-expanded="false"
          >
            Details
          </Button>
        </Box>
      </Grid>
    </Box>
  );
};

export default ToiletDetailsPanel;
<|endoftext|>


<commit_before>'use strict';

var elasticsearch = require('elasticsearch');
var Q = require('q');

var cip = require('../lib/cip-methods.js');
var cip_categories = require('../lib/cip-categories.js');
var asset_mapping = require('../lib/asset-mapping.js');

var client = new elasticsearch.Client({requestTimeout: 30 * 60 * 1000 });

var ASSETS_PER_REQUEST = 100;

/*=== Defining modes to run the syncronization in ===*/

// What modes can we run the elasticsearch script in?
var MODES = {
    recent: 'recent',
    all: 'all',
    catalog: 'catalog',
    single: 'single'
};
var MODE_DESCRIPTIONS = {
    recent: 'Syncronize the most recently changed assets.',
    all: 'Syncronize all assets across all catalogs.',
    catalog: 'Syncronize only a single catalog or a set of comma seperated catalogs.',
    single: 'Syncronize only a single asset or a comma seperated list of assets.'
};
var mode, reference;

// A function to check that a user supplied mode is valid.
function is_valid_mode(suggested_mode) {
    for(var mode in MODES) {
        if(suggested_mode === MODES[mode]) {
            return mode;
        }
    }
    return false;
}

var args = process.argv;
if(args && args.length <= 2) {
    // No arguments supplied, just node and the app script's path.
    mode = MODES.recent;
} else if(args && args.length >= 3) {
    var suggested_mode = args[2];
    mode = is_valid_mode(suggested_mode);
    if(mode === MODES.catalog || mode === MODES.single) {
        if(args.length >= 4) {
            reference = args[3].split(',');
        }
    }
}

// Report any runtime errors on mode selection.
if(!mode) {
    console.error('Invalid mode - please select from:');
    for(var m in MODES) {
        console.error(' * ' +MODES[m]+ ': '+MODE_DESCRIPTIONS[m]);
    }
    process.exit(1);
} else if(mode === MODES.single) {
    // In the single mode, each asset is a combination of a catalog alias
    // and the asset ID, eg. DNT/101
    for(var r in reference) {
        reference[r] = reference[r].split('-');
    }
}

/*=== END: Defining modes to run the syncronization in ===*/

// Creates the index in the Elasticsearch index.
function create_index() {
    return client.indices.create({
        index: 'assets'
    }).then(function() {
        console.log('Index created');
    });
}

// Cleans up a string, representing a category.
function clean_string(str) {
    var regexp = new RegExp('^[0-9]+[A-Z]+.[0-9]+.[0-9]+ ');
    if(str.search(regexp) === 0) {
        return str.replace(regexp, '');
    }
    regexp = new RegExp('^[A-Z]?[0-9]+[a-z]? - ');
    if(str.search(regexp) === 0) {
        return str.replace(regexp, '');
    }
    regexp = new RegExp('^[A-F] - ');
    if(str.search(regexp) === 0) {
        return str.replace(regexp, '');
    }
    regexp = new RegExp('^[0-9][0-9] ');
    if(str.search(regexp) === 0) {
        return str.replace(regexp, '');
    }
    return str;
}

// Determines wether or not an asset should be visible to the user when searching.
function determine_searchability(cip_client, asset, formatted_result) {
    // First of all - we wouldn't like to have search results on assets
    // which have been cropped into seperate assets.
    if(formatted_result.cropping_status && formatted_result.cropping_status.id === 2) {
        return false;
    } else {
        // Second - We wouldn't like assets which are related to assets that are in the
        // "Rotationsbilleder" category - as these are side-views of an object which will be
        // reachable through the front-facing master asset.
        return cip.get_related_assets(asset, 'isalternateof')
        .then(function parse_relations(related_assets) {
            var related_asset_promises = [];
            for(var i in related_assets.ids) {
                var related_asset_id = related_assets.ids[i];
                var related_asset_promise = cip.get_asset(cip_client, formatted_result.catalog, related_asset_id);
                related_asset_promises.push( related_asset_promise );
            }
            return Q.all(related_asset_promises).then(function(related_assets) {
                for(var r in related_assets) {
                    for(var a in related_assets[r]) {
                        var related_asset = related_assets[r][a];
                        var formatted_asset = asset_mapping.format_result(related_asset.fields);
                        for(var c in formatted_asset.categories) {
                            var category = formatted_asset.categories[c];
                            if(category.name === 'Rotationsbilleder') {
                                return false;
                            }
                        }
                    }
                }
                // None of the related assets was a part of the rotational images category.
                return true;
            });
        });
    }
}

var categories = {};
var catalog_page_index = {};
var catalog_index = 0;

// Handle a specific asset from a result page.
function handle_asset(cip_client, asset, catalog_alias) {
    var formatted_result = asset_mapping.format_result(asset.fields);
    formatted_result.catalog = catalog_alias;

    // TODO: Consider making the registration of extenstions to assets metadata
    // more maintainable.
    return Q.all([
        asset_mapping.extend_metadata(cip_client, catalog_alias, asset, formatted_result),
        determine_searchability(cip_client, asset, formatted_result)
    ])
    .spread(function(formatted_result, is_searchable) {
        // TODO: Consider having a field for the values that are checked in the
        // call to determine_searchability, but have the web application decide
        // if it wants to display these or not. Alternatively have publication
        // status reflected in this .searchable field.
        formatted_result.searchable = is_searchable;

        if(formatted_result.modification_time !== undefined) {
            var re = new RegExp('\\d+');
            var re_result = re.exec(formatted_result.modification_time);
            if(re_result && re_result.length > 0) {
                formatted_result.modification_time = parseInt(re_result[0], 10);
            }
        }

        if(formatted_result.categories !== undefined) {
            formatted_result.categories_int = [];
            formatted_result.suggest = {'input': []};

            for(var j=0; j < formatted_result.categories.length; ++j) {
                if(formatted_result.categories[j].path.indexOf('$Categories') !== 0) {
                    continue;
                }

                var path = categories[catalog_alias].get_path(formatted_result.categories[j].id);
                if(path) {
                    for(var k=0; k < path.length; k++) {
                        formatted_result.categories_int.push(path[k].id);

                        if(path[k].name.indexOf('$Categories') === 0) {
                            continue;
                        }

                        formatted_result.suggest.input.push(clean_string(path[k].name));
                    }
                }
            }
        }
        return formatted_result;
    })
    .then(function(formatted_result) {
        var es_id = formatted_result.catalog + '-' + formatted_result.id;
        return client.index({
            index: 'assets',
            type: 'asset',
            id: es_id,
            body: formatted_result
        });
    })
    .then(function(resp) {
        console.log('Successfully indexed ' + resp._id);
    })
    .fail(function(err) {
        console.error( 'Something went wrong indexing an asset' );
        console.error( err.stack );
    });
}

// Handle a specific result page, with assets.
function handle_result_page(cip_client, catalog, result, page_index) {
    var deferred = Q.defer();
    var total_pages = Math.ceil(result.total_rows / ASSETS_PER_REQUEST);
    console.log('Queuing page number', page_index+1, 'of', total_pages+1, 'in the', catalog.alias, 'catalog.');

    result.get(ASSETS_PER_REQUEST, page_index * ASSETS_PER_REQUEST, function(assets_on_page) {
        console.log('Got metadata of page ' +(page_index+1)+ ' from the ' +result.catalog.alias+ ' catalog.');
        var asset_promises = [];
        for(var a in assets_on_page) {
            var asset = assets_on_page[a];
            var asset_promise = handle_asset(cip_client, asset, catalog.alias);
            asset_promises.push( asset_promise );
        }
        Q.all(asset_promises).then(function() {
            // Resolve the page promise once all assets on the page has been resolved.
            deferred.resolve( true );
        });
    }, function(err) {
        console.error( 'An error occurred while getting the page', err );
        deferred.resolve( false );
    });

    return deferred.promise;
}

// Recursively handle the assets on pages, giving a breath first traversal
// of the CIP webservice.
function handle_next_result_page(cip_client, catalog, result) {
    var page_index = catalog_page_index[catalog.alias];
    if(page_index * ASSETS_PER_REQUEST < result.total_rows) {
        catalog_page_index[catalog.alias]++;
        return handle_result_page(cip_client, catalog, result, page_index)
        .then(function() {
            return handle_next_result_page(cip_client, catalog, result);
        });
    } else {
        return true; // No more pages in the result.
    }
}

// Handle a specific catalog.
function handle_catalog(cip_client, catalog) {
    // Precondition: The catalog has it's alias defined.
    if(catalog === undefined || catalog.alias === undefined) {
        throw new Error('The catalog´s alias was undefined');
    }
    console.log('Queuing catalog', catalog.alias);
    var modified_since = (mode === MODES.recent ? '$today-2d' : '2003-04-24');
    // Let's find out how many pages of assets we have in this catalog.
    return cip.get_recent_assets(cip_client, catalog, modified_since )
    .then(function(result) {
        // Reset the counter.
        catalog_page_index[catalog.alias] = 0;
        return Q.when(handle_next_result_page(cip_client, catalog, result), function() {
            console.log('Done parsing catalog', catalog.alias);
        });
    }, function(err) {
        console.error('An error occurred getting overview from the catalog');
        return false;
    });
}

// Recursively handle the catalogs, giving a breath first traversal
// of the CIP webservice.
function handle_next_catalog(cip_client, catalogs) {
    if(catalog_index < catalogs.length) {
        var catalog = catalogs[catalog_index];
        catalog_index++;
        return handle_catalog(cip_client, catalog)
        .then(function() {
            return handle_next_catalog(cip_client, catalogs);
        });
    } else {
        return true; // No more catalogs.
    }
}

// Let's get started.
console.log('Running in mode: ' + MODE_DESCRIPTIONS[mode]);

var main_queue = cip_categories.load_categories()
.then(function(result) {
    // The categories pr catalog has been fetched from Cumulus.
    for(var i=0; i < result.length; ++i) {
        categories[result[i].id] = result[i];
    }
    var categories_count = Object.keys(categories).length;
    console.log('Loaded categories for', categories_count, 'catalogs');
})
.then(function() {
    return create_index().then(function() {
        console.log('Index created');
    }, function(err) {
        // TODO: Add a recursive check for this message.
        if(err.message === 'IndexAlreadyExistsException[[assets] already exists]') {
            return; // No worries ...
        }
        console.log('Failed to create the index:', err);
    });
})
.then(cip.init_session);

if(mode === MODES.recent || mode === MODES.all || mode === MODES.catalog) {
    main_queue = main_queue.then(function(cip_client) {
        return [cip_client, cip.get_catalogs(cip_client)];
    })
    .spread(function(cip_client, catalogs) {
        var relevant_catalogs = [];
        if(mode === MODES.catalog) {
            for(var c in catalogs) {
                var catalog = catalogs[c];
                if(reference.indexOf(catalog.alias) !== -1) {
                    relevant_catalogs.push(catalog);
                }
            }
        } else {
            // All catalogs are relevant catalogs.
            relevant_catalogs = catalogs;
        }
        var catalog_aliases = [];
        for(var c in relevant_catalogs) {
            catalog_aliases.push(relevant_catalogs[c].alias);
        }
        console.log("Indexing from these catalogs:", catalog_aliases.join(","));
        return handle_next_catalog(cip_client, relevant_catalogs);
    });
} else {
    main_queue = main_queue.then(function(cip_client) {
        var asset_promises = [];
        for(var a in reference) {
            var catalog_alias = reference[a][0];
            var asset_id = reference[a][1];
            var asset_promise = cip.get_asset(cip_client, catalog_alias, asset_id)
            .then(function(assets) {
                if(assets.length === 1) {
                    console.log('Queuing asset', assets[0].fields.id, 'from the', catalog_alias, 'catalog.');
                    return handle_asset(cip_client, assets[0], catalog_alias);
                } else {
                    throw new Error( 'No asset with id ' +asset_id+ ' was found in the ' +catalog_alias+ ' catalog.' );
                }
            });
            asset_promises.push( asset_promise );
        }
        return Q.all(asset_promises);
    });
}

main_queue
.fail(function (error) {
    console.error('An error occurred:');
    if(error !== null && error !== undefined) {
        if(error.stack) {
            console.error( error.stack );
        } else {
            console.error( error );
        }
    } else {
        console.error( 'No details was provided.' );
    }
}).finally(function() {
    console.log('=== All done ===');
    // We are ready to die ..
    process.exit(0);
});<commit_msg>Adding a .fail, to make the parsing of pages more robust to errors.<commit_after>'use strict';

var elasticsearch = require('elasticsearch');
var Q = require('q');

var cip = require('../lib/cip-methods.js');
var cip_categories = require('../lib/cip-categories.js');
var asset_mapping = require('../lib/asset-mapping.js');

var client = new elasticsearch.Client({requestTimeout: 30 * 60 * 1000 });

var ASSETS_PER_REQUEST = 100;

/*=== Defining modes to run the syncronization in ===*/

// What modes can we run the elasticsearch script in?
var MODES = {
    recent: 'recent',
    all: 'all',
    catalog: 'catalog',
    single: 'single'
};
var MODE_DESCRIPTIONS = {
    recent: 'Syncronize the most recently changed assets.',
    all: 'Syncronize all assets across all catalogs.',
    catalog: 'Syncronize only a single catalog or a set of comma seperated catalogs.',
    single: 'Syncronize only a single asset or a comma seperated list of assets.'
};
var mode, reference;

// A function to check that a user supplied mode is valid.
function is_valid_mode(suggested_mode) {
    for(var mode in MODES) {
        if(suggested_mode === MODES[mode]) {
            return mode;
        }
    }
    return false;
}

var args = process.argv;
if(args && args.length <= 2) {
    // No arguments supplied, just node and the app script's path.
    mode = MODES.recent;
} else if(args && args.length >= 3) {
    var suggested_mode = args[2];
    mode = is_valid_mode(suggested_mode);
    if(mode === MODES.catalog || mode === MODES.single) {
        if(args.length >= 4) {
            reference = args[3].split(',');
        }
    }
}

// Report any runtime errors on mode selection.
if(!mode) {
    console.error('Invalid mode - please select from:');
    for(var m in MODES) {
        console.error(' * ' +MODES[m]+ ': '+MODE_DESCRIPTIONS[m]);
    }
    process.exit(1);
} else if(mode === MODES.single) {
    // In the single mode, each asset is a combination of a catalog alias
    // and the asset ID, eg. DNT/101
    for(var r in reference) {
        reference[r] = reference[r].split('-');
    }
}

/*=== END: Defining modes to run the syncronization in ===*/

// Creates the index in the Elasticsearch index.
function create_index() {
    return client.indices.create({
        index: 'assets'
    }).then(function() {
        console.log('Index created');
    });
}

// Cleans up a string, representing a category.
function clean_string(str) {
    var regexp = new RegExp('^[0-9]+[A-Z]+.[0-9]+.[0-9]+ ');
    if(str.search(regexp) === 0) {
        return str.replace(regexp, '');
    }
    regexp = new RegExp('^[A-Z]?[0-9]+[a-z]? - ');
    if(str.search(regexp) === 0) {
        return str.replace(regexp, '');
    }
    regexp = new RegExp('^[A-F] - ');
    if(str.search(regexp) === 0) {
        return str.replace(regexp, '');
    }
    regexp = new RegExp('^[0-9][0-9] ');
    if(str.search(regexp) === 0) {
        return str.replace(regexp, '');
    }
    return str;
}

// Determines wether or not an asset should be visible to the user when searching.
function determine_searchability(cip_client, asset, formatted_result) {
    // First of all - we wouldn't like to have search results on assets
    // which have been cropped into seperate assets.
    if(formatted_result.cropping_status && formatted_result.cropping_status.id === 2) {
        return false;
    } else {
        // Second - We wouldn't like assets which are related to assets that are in the
        // "Rotationsbilleder" category - as these are side-views of an object which will be
        // reachable through the front-facing master asset.
        return cip.get_related_assets(asset, 'isalternateof')
        .then(function parse_relations(related_assets) {
            var related_asset_promises = [];
            for(var i in related_assets.ids) {
                var related_asset_id = related_assets.ids[i];
                var related_asset_promise = cip.get_asset(cip_client, formatted_result.catalog, related_asset_id);
                related_asset_promises.push( related_asset_promise );
            }
            return Q.all(related_asset_promises).then(function(related_assets) {
                for(var r in related_assets) {
                    for(var a in related_assets[r]) {
                        var related_asset = related_assets[r][a];
                        var formatted_asset = asset_mapping.format_result(related_asset.fields);
                        for(var c in formatted_asset.categories) {
                            var category = formatted_asset.categories[c];
                            if(category.name === 'Rotationsbilleder') {
                                return false;
                            }
                        }
                    }
                }
                // None of the related assets was a part of the rotational images category.
                return true;
            });
        });
    }
}

var categories = {};
var catalog_page_index = {};
var catalog_index = 0;

// Handle a specific asset from a result page.
function handle_asset(cip_client, asset, catalog_alias) {
    var formatted_result = asset_mapping.format_result(asset.fields);
    formatted_result.catalog = catalog_alias;

    // TODO: Consider making the registration of extenstions to assets metadata
    // more maintainable.
    return Q.all([
        asset_mapping.extend_metadata(cip_client, catalog_alias, asset, formatted_result),
        determine_searchability(cip_client, asset, formatted_result)
    ])
    .spread(function(formatted_result, is_searchable) {
        // TODO: Consider having a field for the values that are checked in the
        // call to determine_searchability, but have the web application decide
        // if it wants to display these or not. Alternatively have publication
        // status reflected in this .searchable field.
        formatted_result.searchable = is_searchable;

        if(formatted_result.modification_time !== undefined) {
            var re = new RegExp('\\d+');
            var re_result = re.exec(formatted_result.modification_time);
            if(re_result && re_result.length > 0) {
                formatted_result.modification_time = parseInt(re_result[0], 10);
            }
        }

        if(formatted_result.categories !== undefined) {
            formatted_result.categories_int = [];
            formatted_result.suggest = {'input': []};

            for(var j=0; j < formatted_result.categories.length; ++j) {
                if(formatted_result.categories[j].path.indexOf('$Categories') !== 0) {
                    continue;
                }

                var path = categories[catalog_alias].get_path(formatted_result.categories[j].id);
                if(path) {
                    for(var k=0; k < path.length; k++) {
                        formatted_result.categories_int.push(path[k].id);

                        if(path[k].name.indexOf('$Categories') === 0) {
                            continue;
                        }

                        formatted_result.suggest.input.push(clean_string(path[k].name));
                    }
                }
            }
        }
        return formatted_result;
    })
    .then(function(formatted_result) {
        var es_id = formatted_result.catalog + '-' + formatted_result.id;
        return client.index({
            index: 'assets',
            type: 'asset',
            id: es_id,
            body: formatted_result
        });
    })
    .then(function(resp) {
        console.log('Successfully indexed ' + resp._id);
    })
    .fail(function(err) {
        console.error( 'Something went wrong indexing an asset' );
        console.error( err.stack );
    });
}

// Handle a specific result page, with assets.
function handle_result_page(cip_client, catalog, result, page_index) {
    var deferred = Q.defer();
    var total_pages = Math.ceil(result.total_rows / ASSETS_PER_REQUEST);
    console.log('Queuing page number', page_index+1, 'of', total_pages+1, 'in the', catalog.alias, 'catalog.');

    result.get(ASSETS_PER_REQUEST, page_index * ASSETS_PER_REQUEST, function(assets_on_page) {
        console.log('Got metadata of page ' +(page_index+1)+ ' from the ' +result.catalog.alias+ ' catalog.');
        var asset_promises = [];
        for(var a in assets_on_page) {
            var asset = assets_on_page[a];
            var asset_promise = handle_asset(cip_client, asset, catalog.alias);
            asset_promises.push( asset_promise );
        }
        Q.all(asset_promises).then(function() {
            // Resolve the page promise once all assets on the page has been resolved.
            deferred.resolve( true );
        });
    }, function(err) {
        deferred.reject( err );
    });

    return deferred.promise;
}

// Recursively handle the assets on pages, giving a breath first traversal
// of the CIP webservice.
function handle_next_result_page(cip_client, catalog, result) {
    var page_index = catalog_page_index[catalog.alias];
    if(page_index * ASSETS_PER_REQUEST < result.total_rows) {
        catalog_page_index[catalog.alias]++;
        return handle_result_page(cip_client, catalog, result, page_index)
        .fail(function(err) {
            console.error('An error happened parsing result page #', page_index, '- skipping this');
            console.error(err.stack ? err.stack : err);
        })
        .then(function() {
            return handle_next_result_page(cip_client, catalog, result);
        });
    } else {
        return true; // No more pages in the result.
    }
}

// Handle a specific catalog.
function handle_catalog(cip_client, catalog) {
    // Precondition: The catalog has it's alias defined.
    if(catalog === undefined || catalog.alias === undefined) {
        throw new Error('The catalog´s alias was undefined');
    }
    console.log('Queuing catalog', catalog.alias);
    var modified_since = (mode === MODES.recent ? '$today-2d' : '2003-04-24');
    // Let's find out how many pages of assets we have in this catalog.
    return cip.get_recent_assets(cip_client, catalog, modified_since )
    .then(function(result) {
        // Reset the counter.
        catalog_page_index[catalog.alias] = 0;
        return Q.when(handle_next_result_page(cip_client, catalog, result), function() {
            console.log('Done parsing catalog', catalog.alias);
        });
    }, function() {
        console.error('An error occurred getting overview from the catalog');
        return false;
    });
}

// Recursively handle the catalogs, giving a breath first traversal
// of the CIP webservice.
function handle_next_catalog(cip_client, catalogs) {
    if(catalog_index < catalogs.length) {
        var catalog = catalogs[catalog_index];
        catalog_index++;
        return handle_catalog(cip_client, catalog)
        .then(function() {
            return handle_next_catalog(cip_client, catalogs);
        });
    } else {
        return true; // No more catalogs.
    }
}

// Let's get started.
console.log('Running in mode: ' + MODE_DESCRIPTIONS[mode]);

var main_queue = cip_categories.load_categories()
.then(function(result) {
    // The categories pr catalog has been fetched from Cumulus.
    for(var i=0; i < result.length; ++i) {
        categories[result[i].id] = result[i];
    }
    var categories_count = Object.keys(categories).length;
    console.log('Loaded categories for', categories_count, 'catalogs');
})
.then(function() {
    return create_index().then(function() {
        console.log('Index created');
    }, function(err) {
        // TODO: Add a recursive check for this message.
        if(err.message === 'IndexAlreadyExistsException[[assets] already exists]') {
            return; // No worries ...
        }
        console.log('Failed to create the index:', err);
    });
})
.then(cip.init_session);

if(mode === MODES.recent || mode === MODES.all || mode === MODES.catalog) {
    main_queue = main_queue.then(function(cip_client) {
        return [cip_client, cip.get_catalogs(cip_client)];
    })
    .spread(function(cip_client, catalogs) {
        var relevant_catalogs = [];
        if(mode === MODES.catalog) {
            for(var c in catalogs) {
                var catalog = catalogs[c];
                if(reference.indexOf(catalog.alias) !== -1) {
                    relevant_catalogs.push(catalog);
                }
            }
        } else {
            // All catalogs are relevant catalogs.
            relevant_catalogs = catalogs;
        }
        var catalog_aliases = [];
        for(var c in relevant_catalogs) {
            catalog_aliases.push(relevant_catalogs[c].alias);
        }
        console.log("Indexing from these catalogs:", catalog_aliases.join(","));
        return handle_next_catalog(cip_client, relevant_catalogs);
    });
} else {
    main_queue = main_queue.then(function(cip_client) {
        var asset_promises = [];
        for(var a in reference) {
            var catalog_alias = reference[a][0];
            var asset_id = reference[a][1];
            var asset_promise = cip.get_asset(cip_client, catalog_alias, asset_id)
            .then(function(assets) {
                if(assets.length === 1) {
                    console.log('Queuing asset', assets[0].fields.id, 'from the', catalog_alias, 'catalog.');
                    return handle_asset(cip_client, assets[0], catalog_alias);
                } else {
                    throw new Error( 'No asset with id ' +asset_id+ ' was found in the ' +catalog_alias+ ' catalog.' );
                }
            });
            asset_promises.push( asset_promise );
        }
        return Q.all(asset_promises);
    });
}

main_queue
.fail(function (error) {
    console.error('An error occurred:');
    if(error !== null && error !== undefined) {
        if(error.stack) {
            console.error( error.stack );
        } else {
            console.error( error );
        }
    } else {
        console.error( 'No details was provided.' );
    }
}).finally(function() {
    console.log('=== All done ===');
    // We are ready to die ..
    process.exit(0);
});<|endoftext|>


<commit_before>import React, { PropTypes } from 'react';
import { Collapse } from 'react-bootstrap';
import { Link } from 'react-router';
import MiniInfoPerfil from './MiniInfoPerfil';
import '../../styles/animal-perfil.scss';

const AnimalItem = ({ animal, selectedAnimalId, onClick }) => {

  const focusedbutton = (arrow_drop_up);
  const unfocusedbutton = (arrow_drop_down);
  const showAnimal = selectedAnimalId === animal.id.toString();
  const adopt = (
 ADOPTADO
);
  const notAdopt = (
 EN ADOPCIÓN
);

  return (

        <button className="button-show" onClick={() => onClick(animal.id)}>
          {showAnimal ? focusedbutton : unfocusedbutton}
        </button>

          <Link className="link-style" to={`/animales/${animal.id}`}>
            {animal.name}
          </Link>

 {animal.species}

 {animal.id % 2 === 1 ? adopt : notAdopt}

          <Link className="link-style" to={`/animales/${animal.id}`}>
            description
          </Link>

      <Collapse in={showAnimal}>

          <MiniInfoPerfil animal={animal} />

      </Collapse>

  );
};

const { object, string, func } = PropTypes;

AnimalItem.propTypes = {
  animal: object.isRequired,
  selectedAnimalId: string.isRequired,
  onClick: func.isRequired
};

export default AnimalItem;
<commit_msg>Remove list adopter mock<commit_after>import React, { PropTypes } from 'react';
import { Collapse } from 'react-bootstrap';
import { Link } from 'react-router';
import MiniInfoPerfil from './MiniInfoPerfil';
import '../../styles/animal-perfil.scss';
import _ from 'lodash';

const AnimalItem = ({ animal, selectedAnimalId, onClick }) => {

  const focusedbutton = (arrow_drop_up);
  const unfocusedbutton = (arrow_drop_down);
  const showAnimal = selectedAnimalId === animal.id.toString();
  const adopt = (
 ADOPTADO
);
  const notAdopt = (
 EN ADOPCIÓN
);

  return (

        <button className="button-show" onClick={() => onClick(animal.id)}>
          {showAnimal ? focusedbutton : unfocusedbutton}
        </button>

          <Link className="link-style" to={`/animales/${animal.id}`}>
            {animal.name}
          </Link>

 {animal.species}

 {_.isUndefined(animal.adopted) ? '': animal.adopted ? adopt : notAdopt}

          <Link className="link-style" to={`/animales/${animal.id}`}>
            description
          </Link>

      <Collapse in={showAnimal}>

          <MiniInfoPerfil animal={animal} />

      </Collapse>

  );
};

const { object, string, func } = PropTypes;

AnimalItem.propTypes = {
  animal: object.isRequired,
  selectedAnimalId: string.isRequired,
  onClick: func.isRequired
};

export default AnimalItem;
<|endoftext|>