<commit_before>import {
  interval as intervalObservable,
  Observable,
} from "rxjs";
import {
  takeUntil,
  distinctUntilChanged,
  map,
} from "rxjs/operators";

const POSITION_UPDATES_INTERVAL = 100;

/**
 * Add event listeners to the RxPlayer to update the module's state at the right
 * time.
 * Unsubscribe when $destroy emit.
 * @param {RxPlayer} player
 * @param {Subject} state
 * @param {Subject} $destroy
 */
const linkPlayerEventsToState = (player, state, $destroy) => {
  const fromPlayerEvent = (event) =>
    Observable.create(obs => {
      const func = (payload) => obs.next(payload);
      player.addEventListener(event, func);

      return () => {
        player.removeEventListener(event, func);
      };
    });

  const linkPlayerEventToState = (event, stateItem) =>
    fromPlayerEvent(event)
      .pipe(takeUntil($destroy))
      .subscribe(arg => state.set({ [stateItem]: arg }));

  linkPlayerEventToState("textTrackChange", "subtitle");
  linkPlayerEventToState("audioTrackChange", "language");
  linkPlayerEventToState("videoTrackChange", "videoTrack");
  linkPlayerEventToState("videoBitrateChange", "videoBitrate");
  linkPlayerEventToState("audioBitrateChange", "audioBitrate");
  linkPlayerEventToState("error", "error");
  linkPlayerEventToState("volumeChange", "volume");

  fromPlayerEvent("imageTrackUpdate").pipe(
    distinctUntilChanged(),
    takeUntil($destroy),
    map(({ data }) => data)
  ).subscribe(images => state.set({ images }));

  // use an interval for current position
  // TODO Only active for content playback
  intervalObservable(POSITION_UPDATES_INTERVAL).pipe(
    map(() => {
      const position = player.getPosition();
      return {
        currentTime: player.getPosition(),
        wallClockDiff: player.getWallClockTime() - position,
        bufferGap: player.getVideoLoadedTime() - player.getVideoPlayedTime(),
        duration: player.getVideoDuration(),
        minimumPosition: player.getMinimumPosition(),
        maximumPosition: player.getMaximumPosition(),
      };
    }),
    takeUntil($destroy)
  ).subscribe(arg => {
    state.set(arg);
  });

  fromPlayerEvent("playerStateChange").pipe(
    distinctUntilChanged(),
    takeUntil($destroy)
  ).subscribe((arg) => {
    const stateUpdates = {
      cannotLoadMetadata: false,
      hasEnded: arg === "ENDED",
      hasCurrentContent: !["STOPPED", "LOADING"].includes(arg),
      isContentLoaded: !["STOPPED", "LOADING", "RELOADING"].includes(arg),
      isBuffering: arg === "BUFFERING",
      isLoading: arg === "LOADING",
      isReloading: arg === "RELOADING",
      isSeeking: arg === "SEEKING",
      isStopped: arg === "STOPPED",
    };

    if (arg === "ENDED" || arg === "PAUSED") {
      stateUpdates.isPaused = true;
    } else if (arg === "PLAYING") {
      stateUpdates.isPaused = false;
    } else if (arg === "LOADED") {
      stateUpdates.isPaused = true;
      stateUpdates.isLive = player.isLive();
    } else if (arg === "STOPPED") {
      stateUpdates.audioBitrate = undefined;
      stateUpdates.videoBitrate = undefined;
      stateUpdates.availableAudioBitrates = [];
      stateUpdates.availableVideoBitrates = [];
      stateUpdates.availableVideoTracks = [];
      stateUpdates.availableLanguages = [];
      stateUpdates.availableSubtitles = [];
      stateUpdates.images = [];
      stateUpdates.currentTime = undefined;
      stateUpdates.wallClockDiff = undefined;
      stateUpdates.bufferGap = undefined;
      stateUpdates.duration = undefined;
      stateUpdates.minimumPosition = undefined;
      stateUpdates.maximumPosition = undefined;
    }

    if (arg !== "STOPPED") {
      // error is never cleaned up
      stateUpdates.error = null;
    }

    state.set(stateUpdates);
  });

  fromPlayerEvent("periodChange")
    .pipe(takeUntil($destroy))
    .subscribe(() => {
      state.set({
        availableVideoTracks: player.getAvailableVideoTracks(),
        availableLanguages: player.getAvailableAudioTracks(),
        availableSubtitles: player.getAvailableTextTracks(),
      });
    });

  fromPlayerEvent("audioTrackChange")
    .pipe(takeUntil($destroy))
    .subscribe(() => {
      state.set({
        availableAudioBitrates: player.getAvailableAudioBitrates(),
      });
    });

  fromPlayerEvent("videoTrackChange")
    .pipe(takeUntil($destroy))
    .subscribe(() => {
      state.set({
        availableVideoBitrates: player.getAvailableVideoBitrates(),
      });
    });

  fromPlayerEvent("warning").pipe(
    takeUntil($destroy)
  ).subscribe(warning => {
    if (warning && warning.code === "MEDIA_ERR_NOT_LOADED_METADATA") {
      state.set({ cannotLoadMetadata: true });
    }
  });
};

export {
  linkPlayerEventsToState,
};
<commit_msg>demo: fix initial language selection<commit_after>import {
  interval as intervalObservable,
  Observable,
} from "rxjs";
import {
  takeUntil,
  distinctUntilChanged,
  map,
} from "rxjs/operators";

const POSITION_UPDATES_INTERVAL = 100;

/**
 * Add event listeners to the RxPlayer to update the module's state at the right
 * time.
 * Unsubscribe when $destroy emit.
 * @param {RxPlayer} player
 * @param {Subject} state
 * @param {Subject} $destroy
 */
const linkPlayerEventsToState = (player, state, $destroy) => {
  const fromPlayerEvent = (event) =>
    Observable.create(obs => {
      const func = (payload) => obs.next(payload);
      player.addEventListener(event, func);

      return () => {
        player.removeEventListener(event, func);
      };
    });

  const linkPlayerEventToState = (event, stateItem) =>
    fromPlayerEvent(event)
      .pipe(takeUntil($destroy))
      .subscribe(arg => state.set({ [stateItem]: arg }));

  linkPlayerEventToState("textTrackChange", "subtitle");
  linkPlayerEventToState("audioTrackChange", "language");
  linkPlayerEventToState("videoTrackChange", "videoTrack");
  linkPlayerEventToState("videoBitrateChange", "videoBitrate");
  linkPlayerEventToState("audioBitrateChange", "audioBitrate");
  linkPlayerEventToState("error", "error");
  linkPlayerEventToState("volumeChange", "volume");

  fromPlayerEvent("imageTrackUpdate").pipe(
    distinctUntilChanged(),
    takeUntil($destroy),
    map(({ data }) => data)
  ).subscribe(images => state.set({ images }));

  // use an interval for current position
  // TODO Only active for content playback
  intervalObservable(POSITION_UPDATES_INTERVAL).pipe(
    map(() => {
      const position = player.getPosition();
      return {
        currentTime: player.getPosition(),
        wallClockDiff: player.getWallClockTime() - position,
        bufferGap: player.getVideoLoadedTime() - player.getVideoPlayedTime(),
        duration: player.getVideoDuration(),
        minimumPosition: player.getMinimumPosition(),
        maximumPosition: player.getMaximumPosition(),
      };
    }),
    takeUntil($destroy)
  ).subscribe(arg => {
    state.set(arg);
  });

  fromPlayerEvent("playerStateChange").pipe(
    distinctUntilChanged(),
    takeUntil($destroy)
  ).subscribe((arg) => {
    const stateUpdates = {
      cannotLoadMetadata: false,
      hasEnded: arg === "ENDED",
      hasCurrentContent: !["STOPPED", "LOADING"].includes(arg),
      isContentLoaded: !["STOPPED", "LOADING", "RELOADING"].includes(arg),
      isBuffering: arg === "BUFFERING",
      isLoading: arg === "LOADING",
      isReloading: arg === "RELOADING",
      isSeeking: arg === "SEEKING",
      isStopped: arg === "STOPPED",
    };

    if (arg === "ENDED" || arg === "PAUSED") {
      stateUpdates.isPaused = true;
    } else if (arg === "PLAYING") {
      stateUpdates.isPaused = false;
    } else if (arg === "LOADED") {
      stateUpdates.isPaused = true;
      stateUpdates.isLive = player.isLive();
    } else if (arg === "STOPPED") {
      stateUpdates.audioBitrate = undefined;
      stateUpdates.videoBitrate = undefined;
      stateUpdates.availableAudioBitrates = [];
      stateUpdates.availableVideoBitrates = [];
      stateUpdates.availableVideoTracks = [];
      stateUpdates.availableLanguages = [];
      stateUpdates.availableSubtitles = [];
      stateUpdates.images = [];
      stateUpdates.subtitle = null;
      stateUpdates.language = null;
      stateUpdates.videoTrack = null;
      stateUpdates.currentTime = undefined;
      stateUpdates.wallClockDiff = undefined;
      stateUpdates.bufferGap = undefined;
      stateUpdates.duration = undefined;
      stateUpdates.minimumPosition = undefined;
      stateUpdates.maximumPosition = undefined;
    }

    if (arg !== "STOPPED") {
      // error is never cleaned up
      stateUpdates.error = null;
    }

    state.set(stateUpdates);
  });

  fromPlayerEvent("periodChange")
    .pipe(takeUntil($destroy))
    .subscribe(() => {
      state.set({
        availableVideoTracks: player.getAvailableVideoTracks(),
        availableLanguages: player.getAvailableAudioTracks(),
        availableSubtitles: player.getAvailableTextTracks(),
      });
    });

  fromPlayerEvent("audioTrackChange")
    .pipe(takeUntil($destroy))
    .subscribe(() => {
      state.set({
        availableAudioBitrates: player.getAvailableAudioBitrates(),
      });
    });

  fromPlayerEvent("videoTrackChange")
    .pipe(takeUntil($destroy))
    .subscribe(() => {
      state.set({
        availableVideoBitrates: player.getAvailableVideoBitrates(),
      });
    });

  fromPlayerEvent("warning").pipe(
    takeUntil($destroy)
  ).subscribe(warning => {
    if (warning && warning.code === "MEDIA_ERR_NOT_LOADED_METADATA") {
      state.set({ cannotLoadMetadata: true });
    }
  });
};

export {
  linkPlayerEventsToState,
};
<|endoftext|>


<commit_before>(function (root, factory) {
  'use strict';
  if (typeof define === 'function' && define.amd) {
    define(factory);
  } else if (typeof exports === 'object') {
    module.exports = factory();
  } else {
    root.Reactions = factory();
  }
}(this, function () {
  'use strict';
  var EMPTY_FUNCTION = function () {};

  var Reactions = {};

  /**
   * Creates a 'done' function.
   * A done function can be invoked only once, receives an error and callback
   * in case of error invokes the defaultDone passing the error
   * in case of success invokes sucess passing the data or
   * the defaultDone if no success is present passing false as first argument and
   * data as the second
   *
   * @argument defaultDone - the done function to delegate to.
   * @argument success - [optional] - the success function to delegate to.
   * @argument param - [optional] - a parameter to pass as third one to
   * the callback function or second one to the success function. Intended for passing
   * of additional parameters to newly created Done functions (as faster alternative to closures).
   *
   */
  Reactions.done = function (defaultDone, success, param) {
    var invoked = false;
    if (typeof success !== 'function')
      success = function (data, param) { defaultDone(false, data, param); };

    return function (error, data) {
      if (invoked)
        throw new Error('callback functions should be invoked only once');
      invoked = true;
      Reactions.invokeLater(function () {
        if (error)
          defaultDone(error);
        else
          success(data, param);
      });
    };
  };

  /**
   * Invokes the passed function later in the event queue.
   * Essentially cross-platform implementation of process.nextTick
   */
  //TODO: use a better implementation for the browsers, as setTimeout(fn, 0) is quite slow
  Reactions.invokeLater = (process && process.nextTick && typeof process.nextTick === 'function')?
      process.nextTick:
      function (fn) { setTimeout(fn, 0); };

  Reactions.fn = {};

  /**
   * Invoke all reactions in order, waiting for the previous to complete.
   * All reactions are invoked with the original context. Done is invoked with the array of data returned by the reactions
   * Executions stops on first error and done(error) is invoked
   */
  Reactions.fn.collectSeries = function(reactions, context, done) {
    var results = [];
    var toRun = -1;
    function next(error, value) {
      if (error) {
        done(error);
        return;
      }
      results[toRun] = value;
      toRun += 1;
      if (toRun < reactions.length) {
        reactions[toRun].call(null, context, Reactions.done(next));
      } else {
        done(false, results);
      }
    }
    next();
  };

  /**
   * Invoke all reactions in order, waiting for the previous to complete.
   * All reactions and done are invoked with the original context.
   * Executions stops on first error and done(error) is invoked
   */
  Reactions.fn.series = function(reactions, context, done) {
    Reactions.fn.collectSeries(reactions, context, function (err, data) {
      if (err) {
        done(err);
      } else {
        done(false, context);
      }
    });
  };

  /**
   * Execution of the reactions is started in the order in which they appear.
   * done is invoked when all reactions complete successfully or on the first received error
   * data passed to Done is the array of the datas returned by the reactions
   */
  Reactions.fn.collectParallel = function(reactions, context, done) {
    if (reactions.length === 0) {
      done(false, []);
      return;
    }
    var countback = reactions.length;
    var finished = false;
    var results = [];
    function next(error, data, index) {
      if (finished) return; //callback has been invoked already

      if (error) {
        finished = true;
        done(error);
        return;
      }

      if (index >= 0) {
        results[index] = data;
      }

      countback -= 1;

      if (countback === 0) {
        done(false, results);
      }
    }

    for (var i = 0; i < reactions.length; i ++) {
      reactions[i].call(null, context, Reactions.done(next, undefined, i));
    }
  };

  /**
   * Execution of the reactions is started in the order in which they appear.
   * done is invoked when all reactions complete successfully or on the first received error
   * data passed to Done is the context object. It might have been changed by the reactions
   */
  Reactions.fn.parallel = function(reactions, context, done) {
    Reactions.fn.collectParallel(reactions, context, function (err, data) {
      if(err) {
        done(err);
      } else {
        done(false, context);
      }
    });
  };

 /**
  * Each reactions is invoked using the result of the previous one as context.
  * The first is fed the original context. Done is invoked with the result of the last one.
  * Execution is stopped on error and done(error) is invoked.
  */
  Reactions.fn.waterfall = function (reactions, context, done) {
    if (reactions.length > 0) {
      var head = reactions[0];
      var tail = reactions.slice(1);
      head.call(null, context, Reactions.done(done, function (data) {
        Reactions.fn.waterfall(tail, data, done);
      }));
    } else {
      done(false, context);
    }
  };

  Reactions.fn.ifelse = function (ifReaction, trueReaction, falseReaction, context, done) {
    ifReaction(context, function (err, data) {
      if (err) {
        done(err);
        return;
      }
      var doFunction = data? trueReaction: falseReaction;
      if (typeof doFunction === 'function')
        doFunction(context, done);
      else
        done(false, context);
    });
  };

  Reactions.fn.while = function (ifReaction, trueReaction, context, done) {
    ifReaction(context, function (err, data) {
      if (err) return done(err);

      if (data) {
        trueReaction(context, function (err2, data2) {
          if (err2) return done(err2);
          Reactions.fn.while(ifReaction, trueReaction, context, done);
        });
      } else {
        done(false, context);
      }
    });
  };

  function constructMakeFn(fn) {
    return function () {
      var argcount = fn.length - 2;
      var args = Array.prototype.slice.call(arguments, 0, argcount); //all arguments except context and done
      return function (world, done) {
        args = args.slice(0);
        args.push(world);
        args.push(done);
        fn.apply(null, args);
      };
    };
  }

 /**
  * Reactions.make.XXX is expecting all the arguments of Reactions.fn.XXX except context and done and
  * creates a function(context, done)
  */
  Reactions.make = {};
  //attach the make functions
  for (var key in Reactions.fn) {
    Reactions.make[key] = constructMakeFn(Reactions.fn[key]);
  }

  return Reactions;
}));<commit_msg>switch implementation<commit_after>(function (root, factory) {
  'use strict';
  if (typeof define === 'function' && define.amd) {
    define(factory);
  } else if (typeof exports === 'object') {
    module.exports = factory();
  } else {
    root.Reactions = factory();
  }
}(this, function () {
  'use strict';
  var EMPTY_FUNCTION = function () {};

  var Reactions = {};

  /**
   * Creates a 'done' function.
   * A done function can be invoked only once, receives an error and callback
   * in case of error invokes the defaultDone passing the error
   * in case of success invokes sucess passing the data or
   * the defaultDone if no success is present passing false as first argument and
   * data as the second
   *
   * @argument defaultDone - the done function to delegate to.
   * @argument success - [optional] - the success function to delegate to.
   * @argument param - [optional] - a parameter to pass as third one to
   * the callback function or second one to the success function. Intended for passing
   * of additional parameters to newly created Done functions (as faster alternative to closures).
   *
   */
  Reactions.done = function (defaultDone, success, param) {
    var invoked = false;
    if (typeof success !== 'function')
      success = function (data, param) { defaultDone(false, data, param); };

    return function (error, data) {
      if (invoked)
        throw new Error('callback functions should be invoked only once');
      invoked = true;
      Reactions.invokeLater(function () {
        if (error)
          defaultDone(error);
        else
          success(data, param);
      });
    };
  };

  /**
   * Invokes the passed function later in the event queue.
   * Essentially cross-platform implementation of process.nextTick
   */
  //TODO: use a better implementation for the browsers, as setTimeout(fn, 0) is quite slow
  Reactions.invokeLater = (process && process.nextTick && typeof process.nextTick === 'function')?
      process.nextTick:
      function (fn) { setTimeout(fn, 0); };

  Reactions.fn = {};

  /**
   * Invoke all reactions in order, waiting for the previous to complete.
   * All reactions are invoked with the original context. Done is invoked with the array of data returned by the reactions
   * Executions stops on first error and done(error) is invoked
   */
  Reactions.fn.collectSeries = function(reactions, context, done) {
    var results = [];
    var toRun = -1;
    function next(error, value) {
      if (error) {
        done(error);
        return;
      }
      results[toRun] = value;
      toRun += 1;
      if (toRun < reactions.length) {
        reactions[toRun].call(null, context, Reactions.done(next));
      } else {
        done(false, results);
      }
    }
    next();
  };

  /**
   * Invoke all reactions in order, waiting for the previous to complete.
   * All reactions and done are invoked with the original context.
   * Executions stops on first error and done(error) is invoked
   */
  Reactions.fn.series = function(reactions, context, done) {
    Reactions.fn.collectSeries(reactions, context, function (err, data) {
      if (err) {
        done(err);
      } else {
        done(false, context);
      }
    });
  };

  /**
   * Execution of the reactions is started in the order in which they appear.
   * done is invoked when all reactions complete successfully or on the first received error
   * data passed to Done is the array of the datas returned by the reactions
   */
  Reactions.fn.collectParallel = function(reactions, context, done) {
    if (reactions.length === 0) {
      done(false, []);
      return;
    }
    var countback = reactions.length;
    var finished = false;
    var results = [];
    function next(error, data, index) {
      if (finished) return; //callback has been invoked already

      if (error) {
        finished = true;
        done(error);
        return;
      }

      if (index >= 0) {
        results[index] = data;
      }

      countback -= 1;

      if (countback === 0) {
        done(false, results);
      }
    }

    for (var i = 0; i < reactions.length; i ++) {
      reactions[i].call(null, context, Reactions.done(next, undefined, i));
    }
  };

  /**
   * Execution of the reactions is started in the order in which they appear.
   * done is invoked when all reactions complete successfully or on the first received error
   * data passed to Done is the context object. It might have been changed by the reactions
   */
  Reactions.fn.parallel = function(reactions, context, done) {
    Reactions.fn.collectParallel(reactions, context, function (err, data) {
      if(err) {
        done(err);
      } else {
        done(false, context);
      }
    });
  };

 /**
  * Each reactions is invoked using the result of the previous one as context.
  * The first is fed the original context. Done is invoked with the result of the last one.
  * Execution is stopped on error and done(error) is invoked.
  */
  Reactions.fn.waterfall = function (reactions, context, done) {
    if (reactions.length > 0) {
      var head = reactions[0];
      var tail = reactions.slice(1);
      head.call(null, context, Reactions.done(done, function (data) {
        Reactions.fn.waterfall(tail, data, done);
      }));
    } else {
      done(false, context);
    }
  };

  Reactions.fn.ifelse = function (ifReaction, trueReaction, falseReaction, context, done) {
    ifReaction(context, function (err, data) {
      if (err) {
        done(err);
        return;
      }
      var doFunction = data? trueReaction: falseReaction;
      if (typeof doFunction === 'function')
        doFunction(context, done);
      else
        done(false, context);
    });
  };

  Reactions.fn.while = function (ifReaction, trueReaction, context, done) {
    ifReaction(context, function (err, data) {
      if (err) return done(err);

      if (data) {
        trueReaction(context, function (err2, data2) {
          if (err2) return done(err2);
          Reactions.fn.while(ifReaction, trueReaction, context, done);
        });
      } else {
        done(false, context);
      }
    });
  };

  Reactions.fn.switch = function (keyReaction, reactionMap, context, done) {
    keyReaction(context, function (err, data) {
      if (err) return done(err);
      var doFunction = reactionMap[data] || reactionMap['default'];
      if (typeof doFunction === 'function')
        doFunction(context, done);
      else
        done(false, context);
    });
  };

  function constructMakeFn(fn) {
    return function () {
      var argcount = fn.length - 2;
      var args = Array.prototype.slice.call(arguments, 0, argcount); //all arguments except context and done
      return function (world, done) {
        args = args.slice(0);
        args.push(world);
        args.push(done);
        fn.apply(null, args);
      };
    };
  }

 /**
  * Reactions.make.XXX is expecting all the arguments of Reactions.fn.XXX except context and done and
  * creates a function(context, done)
  */
  Reactions.make = {};
  //attach the make functions
  for (var key in Reactions.fn) {
    Reactions.make[key] = constructMakeFn(Reactions.fn[key]);
  }

  return Reactions;
}));<|endoftext|>


<commit_before>"use strict";

// add pound symbol for jquery element id addressing
function tag_id(id_name) {

    return '#' + id_name;
}

// translate domain (search category such as disease, biomarker, etc.)
// into standardized field ids following the convention:
// disease ->
//      disease_names -> for hidden field holding the concept names as comma delimited string
//      disease_codes -> for hidden field holding the concept codes as comma delimited string
//      diseases      -> for unordered list of selected concept names
function get_fields(dom) {

    return {
        'name_list': tag_id(dom + '_names'),
        'code_list': tag_id(dom + '_codes'),
        'ul_list': tag_id(dom + 's')
    }
}

// load comma delimited list of concept codes from hidden string field and populate an UL
// will callback to retrieve names for the concept codes
// code_list_id - id of the text form field
// ul_list_id - id of the ul list
function refresh_ul_list_from_hidden(dom) {

    var fields = get_fields(dom);
    var names = $(fields.name_list).val().split(', ');
    var codes = $(fields.code_list).val().split(', ');

    $.each(names, function (i, name) {
        if (name !== '') {
            var code = codes[i];
            $(fields.ul_list).append(create_li(code, name, dom));
        }
    });
}

// wrapper to call the remove_term for both names and code hidden fields
function rm_term(code, name, dom) {

    var fields = get_fields(dom);
    // remove ul list item
    $(tag_id(code)).remove();
    // remove name from hidden form field
    remove_term(name, fields.name_list);
    // remove code from hidden form field
    remove_term(code, fields.code_list);
}

// remove term from comma separated list in text field
function remove_term(term, list_id) {

    var hidden_field_value = $(list_id).val();
    var terms = hidden_field_value.split(', ');
    var pos = terms.indexOf(term);
    terms.splice(pos, 1);
    $(list_id).val(terms.join(", "));
}

// execute ajax callback, loop through server response
// execute process_entry function for each return object (name code).
function _do_code_callback(dom, url, process_entry) {

    $.get(url, function (data) {
        data.forEach(function (entry) {
            var code = entry.code;
            var name = entry.name.replace(/_/g, ' ');
            process_entry(code, name, dom);
        });
    });
}

// code is nci thesaurus concept id
// dom is domain either biomarker or disease (or other in the future)
function add_child_terms(code, dom) {
    _do_code_callback(dom, 'get_child_codes?code=' + code + '&dom=' + dom, add_term);
}

// code is nci thesaurus concept id
// dom is domain either biomarker or disease (or other in the future)
function rm_child_terms(code, dom) {
    _do_code_callback(dom, 'get_child_codes?code=' + code + '&dom=' + dom, rm_term);
}

// code is nci thesaurus concept id
// dom is domain either biomarker or disease (or other in the future)
function add_parent_terms(code, dom) {
    _do_code_callback(dom, 'get_parent_codes?code=' + code + '&dom=' + dom, add_term);
}

// code is nci thesaurus concept id
// dom is domain either biomarker or disease (or other in the future)
function rm_parent_terms(code, dom) {
    _do_code_callback(dom, 'get_parent_codes?code=' + code, rm_term);
}

// generates link for selected code and name
// to be appended in the ul
function create_li(code, name, dom) {

    return '
'
        + 'remove'
        + ' | add child terms'
        + ' | remove child terms'
        + ' | add parent terms'
        + ' | remove parent terms'
        + ' | ' + name + '
';
}

// update the hidden fields for codes and names
// and the unordered list for selected terms
function add_term(code, name, dom) {

    var fields = get_fields(dom);

    if (!$(fields.code_list).val().includes(code)) {
        add_item_to_comma_delimited_list(fields.name_list, name);
        add_item_to_comma_delimited_list(fields.code_list, code);
        $(fields.ul_list).append(create_li(code, name, dom));
    }
}

// split the comma-delimited string into array
// add item to list if it doesn't already exist in the list
// join it back together and replace the value
function add_item_to_comma_delimited_list(field_name, item) {
    $(field_name).val($.grep([$(field_name).val(), item], Boolean).join(", "));
}

function update_tree(code, item) {
    var tree = $('#jstree').jstree(true);
    tree.settings.core.data = build_jstree_node(code, item);
    tree.refresh();
}

function get_code_for_id(node_id) {
    var codes = node_id.split('*');
    return codes[codes.length - 1];
}

function get_id_for_code(parent_code, code) {
    return parent_code ? parent_code + '*' + code : code;
}

function build_jstree_node(code, name, parent_id) {
    var id;
    if (parent_id) {
        var parent_code = get_code_for_id(parent_id);
        id = get_id_for_code(parent_code, code);
    } else {
        id = code;
    }
    return {
        'id': id,
        'text': name.replace(/_/g, ' '),
        'state': {'opened': false, 'selected': false}
    };
}

function add_jstree_node(parent, node, tree) {
    if (tree.get_node(node)) {
        return node.id;
    }
    var position = 'last';

    function callback() {
    }

    function is_loaded() {
    }

    return tree.create_node(parent, node, position, callback, is_loaded);
}

function add_children(parent, tree, node_ids_to_recurse) {
    parent.state.opened = true;
    parent.state.selected = true;
    $.get('/get_child_codes', {code: get_code_for_id(parent.id), dom: dom}, function (data) {
        data.forEach(function (item) {
            var node = build_jstree_node(item.code, item.name, parent.id);
            if (tree.get_node(node)) {
                if (tree.is_leaf(node) === false) {
                    node_ids_to_recurse = node_ids_to_recurse ? node_ids_to_recurse : [];
                    node_ids_to_recurse.push(node.id);
                }
                tree.delete_node(node);
            }
            var new_id = add_jstree_node(parent, node, tree);
            if (node_ids_to_recurse && node_ids_to_recurse.includes(new_id)) {
                tree.select_node(node);
                add_children(node, tree);
                node_ids_to_recurse.pop(new_id);
            }
        });
    });
}

function remove_children(parent, tree) {
    parent.state.opened = false;
    var children = tree.get_children_dom(parent);
    children.each(
        function (index, child) {
            tree.delete_node(child);
        }
    );
}

function get_callback_url(dom) {
    var url = '';
    if ('disease' === dom) {
        url = 'search_diseases?q=';
    } else if ('finding' === dom) {
        url = 'search_findings?q=';
    } else if ('diagnostic' === dom) {
        url = 'search_diagnostic_test?q=';
    } else if ('test' === dom) {
        url = 'search_lab_test?q=';
    } else if ('drug' === dom) {
        url = 'search_drugs?q=';
    } else if ('procedure' === dom) {
        url = 'search_therapies?q=';
    } else if ('biomarker' === dom) {
        url = 'search_biomarkers?q=';
    } else if ('anatomicsite' === dom) {
        url = 'search_anatomicsites?q=';
    } else if ('tissue' === dom) {
        url = 'search_tissues?q=';
    }
    return url;
}

function search_selected_nodes(dom, datatable, node_ids) {
    $("[name='results_length']").val(10); // reset the row number to 10
    datatable.search(JSON.stringify({})).draw(); // reset the datatable to all trials
    // reset the total record counter and display temporary spinner
    var records_total = $("#records_total");
    records_total.text('');
    records_total.prepend('');
    var codes = [];
    for (var i = 0; i < node_ids.length; i++) {
        var code = get_code_for_id(node_ids[i]);
        codes.push(code);
    }
    console.log(codes);

    var search_params = {};

    if ('disease' === dom) {
        search_params['diseases.nci_thesaurus_concept_id'] = codes;
        datatable.search(JSON.stringify(search_params)).draw();
    } else if ('biomarker' === dom) {
        search_params['biomarkers.nci_thesaurus_concept_id'] = codes;
        datatable.search(JSON.stringify(search_params)).draw();
    }
}

function select_subtree(node, dom, datatable) {
    var code = get_code_for_id(node.id);
    $.get('get_subtree_codes', {code: code, dom: dom}, function (codes) {
        codes.push(code); // add in the parent concept that was selected
        if ('disease' === dom) {
            search_params['diseases.nci_thesaurus_concept_id'] = codes;
        } else if ('biomarker' === dom) {
            search_params['biomarkers.nci_thesaurus_concept_id'] = codes;
        }
        datatable.search(JSON.stringify(search_params)).draw();
    });
}

function _build_jstree_context_menu_item(label, action) {
    return {
        "separator_before": false,
        "separator_after": false,
        "icon": 'glyphicon glyphicon-minus',
        "label": label,
        "action": action
    }
}

// timeout to avoid single click jstree event trigger on double click
var is_enabled = true;
function timer() {
    is_enabled = false;
    setTimeout(function () {
        is_enabled = true;
    }, 250); // 250 ms seems to be the sweet spot for me wrt double click speed
}

function _toggle_leafnode(selector, leafnode_id) {
    var tree = $(selector).jstree(true);
    var node = tree.get_node(leafnode_id);
    if (tree.is_leaf(node)) {
        add_children(node, tree, dom);
    } else {
        remove_children(node, tree);
    }
}

function build_jstree(selector) {
    return $(selector).jstree({
        plugins: ['contextmenu', 'dnd', 'search', 'sort', 'checkbox'],
        core: {
            check_callback: true,
            data: [],
            themes: {"icons": false}
        },
        checkbox: {
            three_state: false,
            cascade: 'undetermined',
            whole_node: false
        },
        search: {
            case_insensitive: true,
            show_only_matches: true
        },
        contextmenu: get_jstree_context_menu(selector)

    }).on('dblclick', '.jstree-anchor', function () {
        _toggle_leafnode(selector, this);

    }).on('click', '.jstree-anchor', function () {
        // avoid triggering the single click event on the doubleclick
        if (is_enabled) {
            search_selected_nodes(dom, datatable, $(selector).jstree(true).get_checked());
            timer();
        }
    });
}

function get_jstree_context_menu(selector) {
    return {
        "items": function (node) {
            return {
                "SelectChildren": _build_jstree_context_menu_item(
                    "Select Child Concepts",
                    function check_children() {
                        _toggle_children_checkboxes(selector, node, true);
                    }
                ),

                "DeSelectChildren": _build_jstree_context_menu_item(
                    "Deselect Child Concepts",
                    function check_children() {
                        _toggle_children_checkboxes(selector, node, false);
                    }
                ),

                "Expand": _build_jstree_context_menu_item(
                    "Expand Tree",
                    function check_children() {
                        $(selector).jstree(true).open_all();
                    }
                ),

                "Collapse": _build_jstree_context_menu_item(
                    "Collapse Tree",
                    function check_children() {
                        $(selector).jstree(true).close_all();
                    }
                )
            }
        }
    }
}

function _toggle_children_checkboxes(selector, node, check) {
    var tree = $(selector).jstree(true);

    // add children before trying to check them
    if (tree.is_leaf(node)) {
        add_children(node, tree, dom);
    }

    setTimeout(function () {

        // open the node and expose the children
        if (tree.is_closed(node)) {
            tree.open_node(node);
        }

        // if selecting the child nodes deselect parent
        // if deselecting the child nodes select parent
        if (check) {
            tree.uncheck_node(node);
        } else {
            tree.check_node(node);
        }

        // enable the checkboxes for all children
        var children = tree.get_children_dom(node);
        // avoid asynchronous loop to make sure all nodes are checked before search commences
        for(var i = 0; i < children.length; i++){
            var child = children[i];
            if (check) {
                console.log(child);
                tree.check_node(child); // using the children directly only selects first node
            } else {
                tree.uncheck_node(child);
            }
        }
        search_selected_nodes(dom, datatable,tree.get_checked());
    }, 500);
}
<commit_msg>reduced cyclomatic complexity by moving if else block to javascript object keys<commit_after>"use strict";

// add pound symbol for jquery element id addressing
function tag_id(id_name) {

    return '#' + id_name;
}

// translate domain (search category such as disease, biomarker, etc.)
// into standardized field ids following the convention:
// disease ->
//      disease_names -> for hidden field holding the concept names as comma delimited string
//      disease_codes -> for hidden field holding the concept codes as comma delimited string
//      diseases      -> for unordered list of selected concept names
function get_fields(dom) {

    return {
        'name_list': tag_id(dom + '_names'),
        'code_list': tag_id(dom + '_codes'),
        'ul_list': tag_id(dom + 's')
    }
}

// load comma delimited list of concept codes from hidden string field and populate an UL
// will callback to retrieve names for the concept codes
// code_list_id - id of the text form field
// ul_list_id - id of the ul list
function refresh_ul_list_from_hidden(dom) {

    var fields = get_fields(dom);
    var names = $(fields.name_list).val().split(', ');
    var codes = $(fields.code_list).val().split(', ');

    $.each(names, function (i, name) {
        if (name !== '') {
            var code = codes[i];
            $(fields.ul_list).append(create_li(code, name, dom));
        }
    });
}

// wrapper to call the remove_term for both names and code hidden fields
function rm_term(code, name, dom) {

    var fields = get_fields(dom);
    // remove ul list item
    $(tag_id(code)).remove();
    // remove name from hidden form field
    remove_term(name, fields.name_list);
    // remove code from hidden form field
    remove_term(code, fields.code_list);
}

// remove term from comma separated list in text field
function remove_term(term, list_id) {

    var hidden_field_value = $(list_id).val();
    var terms = hidden_field_value.split(', ');
    var pos = terms.indexOf(term);
    terms.splice(pos, 1);
    $(list_id).val(terms.join(", "));
}

// execute ajax callback, loop through server response
// execute process_entry function for each return object (name code).
function _do_code_callback(dom, url, process_entry) {

    $.get(url, function (data) {
        data.forEach(function (entry) {
            var code = entry.code;
            var name = entry.name.replace(/_/g, ' ');
            process_entry(code, name, dom);
        });
    });
}

// code is nci thesaurus concept id
// dom is domain either biomarker or disease (or other in the future)
function add_child_terms(code, dom) {
    _do_code_callback(dom, 'get_child_codes?code=' + code + '&dom=' + dom, add_term);
}

// code is nci thesaurus concept id
// dom is domain either biomarker or disease (or other in the future)
function rm_child_terms(code, dom) {
    _do_code_callback(dom, 'get_child_codes?code=' + code + '&dom=' + dom, rm_term);
}

// code is nci thesaurus concept id
// dom is domain either biomarker or disease (or other in the future)
function add_parent_terms(code, dom) {
    _do_code_callback(dom, 'get_parent_codes?code=' + code + '&dom=' + dom, add_term);
}

// code is nci thesaurus concept id
// dom is domain either biomarker or disease (or other in the future)
function rm_parent_terms(code, dom) {
    _do_code_callback(dom, 'get_parent_codes?code=' + code, rm_term);
}

// generates link for selected code and name
// to be appended in the ul
function create_li(code, name, dom) {

    return '
'
        + 'remove'
        + ' | add child terms'
        + ' | remove child terms'
        + ' | add parent terms'
        + ' | remove parent terms'
        + ' | ' + name + '
';
}

// update the hidden fields for codes and names
// and the unordered list for selected terms
function add_term(code, name, dom) {

    var fields = get_fields(dom);

    if (!$(fields.code_list).val().includes(code)) {
        add_item_to_comma_delimited_list(fields.name_list, name);
        add_item_to_comma_delimited_list(fields.code_list, code);
        $(fields.ul_list).append(create_li(code, name, dom));
    }
}

// split the comma-delimited string into array
// add item to list if it doesn't already exist in the list
// join it back together and replace the value
function add_item_to_comma_delimited_list(field_name, item) {
    $(field_name).val($.grep([$(field_name).val(), item], Boolean).join(", "));
}

function update_tree(code, item) {
    var tree = $('#jstree').jstree(true);
    tree.settings.core.data = build_jstree_node(code, item);
    tree.refresh();
}

function get_code_for_id(node_id) {
    var codes = node_id.split('*');
    return codes[codes.length - 1];
}

function get_id_for_code(parent_code, code) {
    return parent_code ? parent_code + '*' + code : code;
}

function build_jstree_node(code, name, parent_id) {
    var id;
    if (parent_id) {
        var parent_code = get_code_for_id(parent_id);
        id = get_id_for_code(parent_code, code);
    } else {
        id = code;
    }
    return {
        'id': id,
        'text': name.replace(/_/g, ' '),
        'state': {'opened': false, 'selected': false}
    };
}

function add_jstree_node(parent, node, tree) {
    if (tree.get_node(node)) {
        return node.id;
    }
    var position = 'last';

    function callback() {
    }

    function is_loaded() {
    }

    return tree.create_node(parent, node, position, callback, is_loaded);
}

function add_children(parent, tree, node_ids_to_recurse) {
    parent.state.opened = true;
    parent.state.selected = true;
    $.get('/get_child_codes', {code: get_code_for_id(parent.id), dom: dom}, function (data) {
        data.forEach(function (item) {
            var node = build_jstree_node(item.code, item.name, parent.id);
            if (tree.get_node(node)) {
                if (tree.is_leaf(node) === false) {
                    node_ids_to_recurse = node_ids_to_recurse ? node_ids_to_recurse : [];
                    node_ids_to_recurse.push(node.id);
                }
                tree.delete_node(node);
            }
            var new_id = add_jstree_node(parent, node, tree);
            if (node_ids_to_recurse && node_ids_to_recurse.includes(new_id)) {
                tree.select_node(node);
                add_children(node, tree);
                node_ids_to_recurse.pop(new_id);
            }
        });
    });
}

function remove_children(parent, tree) {
    parent.state.opened = false;
    var children = tree.get_children_dom(parent);
    children.each(
        function (index, child) {
            tree.delete_node(child);
        }
    );
}

var urls = {
    'disease': 'search_diseases?q=',
    'finding': 'search_findings?q=',
    'diagnostic': 'search_diagnostic_test?q=',
    'test': 'search_lab_test?q=',
    'drug': 'search_drugs?q=',
    'procedure': 'search_therapies?q=',
    'biomarker': 'search_biomarkers?q=',
    'anatomicsite': 'search_anatomicsites?q=',
    'tissue': 'search_tissues?q='
}

function get_callback_url(dom) {
    if (dom in urls) {
        return urls[dom];
    } else {
        return '';
    }
}

function search_selected_nodes(dom, datatable, node_ids) {
    $("[name='results_length']").val(10); // reset the row number to 10
    datatable.search(JSON.stringify({})).draw(); // reset the datatable to all trials
    // reset the total record counter and display temporary spinner
    var records_total = $("#records_total");
    records_total.text('');
    records_total.prepend('');
    var codes = [];
    for (var i = 0; i < node_ids.length; i++) {
        var code = get_code_for_id(node_ids[i]);
        codes.push(code);
    }
    console.log(codes);

    var search_params = {};

    if ('disease' === dom) {
        search_params['diseases.nci_thesaurus_concept_id'] = codes;
        datatable.search(JSON.stringify(search_params)).draw();
    } else if ('biomarker' === dom) {
        search_params['biomarkers.nci_thesaurus_concept_id'] = codes;
        datatable.search(JSON.stringify(search_params)).draw();
    }
}

function select_subtree(node, dom, datatable) {
    var code = get_code_for_id(node.id);
    $.get('get_subtree_codes', {code: code, dom: dom}, function (codes) {
        codes.push(code); // add in the parent concept that was selected
        if ('disease' === dom) {
            search_params['diseases.nci_thesaurus_concept_id'] = codes;
        } else if ('biomarker' === dom) {
            search_params['biomarkers.nci_thesaurus_concept_id'] = codes;
        }
        datatable.search(JSON.stringify(search_params)).draw();
    });
}

function _build_jstree_context_menu_item(label, action) {
    return {
        "separator_before": false,
        "separator_after": false,
        "icon": 'glyphicon glyphicon-minus',
        "label": label,
        "action": action
    }
}

// timeout to avoid single click jstree event trigger on double click
var is_enabled = true;
function timer() {
    is_enabled = false;
    setTimeout(function () {
        is_enabled = true;
    }, 250); // 250 ms seems to be the sweet spot for me wrt double click speed
}

function _toggle_leafnode(selector, leafnode_id) {
    var tree = $(selector).jstree(true);
    var node = tree.get_node(leafnode_id);
    if (tree.is_leaf(node)) {
        add_children(node, tree, dom);
    } else {
        remove_children(node, tree);
    }
}

function build_jstree(selector) {
    return $(selector).jstree({
        plugins: ['contextmenu', 'dnd', 'search', 'sort', 'checkbox'],
        core: {
            check_callback: true,
            data: [],
            themes: {"icons": false}
        },
        checkbox: {
            three_state: false,
            cascade: 'undetermined',
            whole_node: false
        },
        search: {
            case_insensitive: true,
            show_only_matches: true
        },
        contextmenu: get_jstree_context_menu(selector)

    }).on('dblclick', '.jstree-anchor', function () {
        _toggle_leafnode(selector, this);

    }).on('click', '.jstree-anchor', function () {
        // avoid triggering the single click event on the doubleclick
        if (is_enabled) {
            search_selected_nodes(dom, datatable, $(selector).jstree(true).get_checked());
            timer();
        }
    });
}

function get_jstree_context_menu(selector) {
    return {
        "items": function (node) {
            return {
                "SelectChildren": _build_jstree_context_menu_item(
                    "Select Child Concepts",
                    function check_children() {
                        _toggle_children_checkboxes(selector, node, true);
                    }
                ),

                "DeSelectChildren": _build_jstree_context_menu_item(
                    "Deselect Child Concepts",
                    function check_children() {
                        _toggle_children_checkboxes(selector, node, false);
                    }
                ),

                "Expand": _build_jstree_context_menu_item(
                    "Expand Tree",
                    function check_children() {
                        $(selector).jstree(true).open_all();
                    }
                ),

                "Collapse": _build_jstree_context_menu_item(
                    "Collapse Tree",
                    function check_children() {
                        $(selector).jstree(true).close_all();
                    }
                )
            }
        }
    }
}

function _toggle_children_checkboxes(selector, node, check) {
    var tree = $(selector).jstree(true);

    // add children before trying to check them
    if (tree.is_leaf(node)) {
        add_children(node, tree, dom);
    }

    setTimeout(function () {

        // open the node and expose the children
        if (tree.is_closed(node)) {
            tree.open_node(node);
        }

        // if selecting the child nodes deselect parent
        // if deselecting the child nodes select parent
        if (check) {
            tree.uncheck_node(node);
        } else {
            tree.check_node(node);
        }

        // enable the checkboxes for all children
        var children = tree.get_children_dom(node);
        // avoid asynchronous loop to make sure all nodes are checked before search commences
        for(var i = 0; i < children.length; i++){
            var child = children[i];
            if (check) {
                console.log(child);
                tree.check_node(child); // using the children directly only selects first node
            } else {
                tree.uncheck_node(child);
            }
        }
        search_selected_nodes(dom, datatable,tree.get_checked());
    }, 500);
}
<|endoftext|>


<commit_before>import _ from 'lodash';
const {knex} = require('../util/database').connect();
import {GCS_CONFIG} from '../util/gcs';
import CONST from '../constants';
const logger = require('../util/logger')(__filename);
import * as score from './hot-score';
import moment from 'moment-timezone';

const FEED_ITEM_TYPES = new Set(['IMAGE', 'TEXT', 'CHECK_IN']);

function getStickySqlString(city) {
  const whereCitySql = !city
    ? ' AND feed_items.city_id IS NULL'
    : knex.raw(` AND (feed_items.city_id = ? OR feed_items.city_id IS NULL)`, city).toString();

  // TODO top_score optimization
  return `
    (SELECT
      feed_items.id as id,
      feed_items.location as location,
      feed_items.created_at as created_at,
      feed_items.image_path as image_path,
      feed_items.text as text,
      feed_items.type as action_type_code,
      COALESCE(users.name, 'SYSTEM') as user_name,
      users.id as user_id,
      teams.name as team_name,
      vote_score(feed_items) as votes,
      feed_items.hot_score as hot_score,
      0 as top_score,
      feed_items.is_sticky,
      COALESCE(votes.value, 0) as user_vote
    FROM feed_items
    LEFT JOIN users ON users.id = feed_items.user_id
    LEFT JOIN teams ON teams.id = users.team_id
    LEFT JOIN votes ON votes.user_id = ? AND votes.feed_item_id = feed_items.id
    WHERE feed_items.is_sticky ${ whereCitySql }
    GROUP BY
      feed_items.id,
      users.name,
      users.id,
      teams.name,
      votes.value
    ORDER BY feed_items.id DESC
    LIMIT 2)`;
}

/**
 *
 * @param {object} opts
 * @param {number} opts.client.id  Client's database ID
 * @param {number} [opts.limit=20] How many results to return
 * @param {number} [opts.beforeId] Return only items before this feed item
 * @param {number} [opts.city]     Return feed items only from given city
 * @param {string} [opts.sort=new] Either 'hot' or 'new'
 * @param {number} [opts.eventId]  Event whose feed items to return
 * @param {string} [opts.type]     Return only certain type items
 * @param {string} [opts.userId]   Return only certain user's items
 * @param {boolean} [opts.includeSticky = true]   Should sticky messages be included
 */
function getFeed(opts) {
  opts = _.merge({
    includeSticky: true,
    limit: 20
  }, opts);

  const sortTop = opts.sort === CONST.FEED_SORT_TYPES.TOP

  // TODO top_score optimization
  let sqlString = `
    (SELECT
      feed_items.id as id,
      feed_items.location as location,
      feed_items.created_at as created_at,
      feed_items.image_path as image_path,
      feed_items.text as text,
      feed_items.type as action_type_code,
      COALESCE(users.name, 'SYSTEM') as user_name,
      users.id as user_id,
      ${ _getTeamNameSql(opts.city) } as team_name,
      vote_score(feed_items) as votes,
      feed_items.hot_score as hot_score,
      ${ sortTop ? `COALESCE(top_score.value, 0)` : `0` } as top_score,
      feed_items.is_sticky,
      COALESCE(votes.value, 0) as user_vote
    FROM feed_items
    LEFT JOIN users ON users.id = feed_items.user_id
    LEFT JOIN teams ON teams.id = users.team_id
    LEFT JOIN votes ON votes.user_id = ? AND votes.feed_item_id = feed_items.id
    LEFT JOIN cities ON cities.id = teams.city_id
    ${ sortTop ? _getSortTopSql() : `` }
    ${ _getWhereSql(opts) }
    GROUP BY
        feed_items.id,
        users.name,
        users.id,
        teams.name,
        votes.value,
        teams.city_id,
        ${ sortTop ? `top_score.value,` : '' }
        cities.id)
    `;

  let params = [opts.client.id];

  // TODO: Sticky messages should have their own endpoint
  const includeSticky = opts.includeSticky && !opts.beforeId && sortTop;
  if (includeSticky) {
    sqlString = getStickySqlString(opts.city) + " UNION ALL " + sqlString;
    params.push(opts.client.id);
  }

  sqlString += _getSortingSql(opts.sort);
  sqlString += ` LIMIT ?`;
  params.push(opts.limit);

  if (opts.after) {
    sqlString +=  ` OFFSET ?`;
    params.push(opts.after);
  }

  return knex.raw(sqlString, params)
  .then(result => {
    const rows = result.rows;

    if (_.isEmpty(rows)) {
      return [];
    }

    return _.map(rows, row => _actionToFeedObject(row, opts.client));
  });
}

function _sanitizeText(text) {
  if (!text) {
    return text;
  }

  return text.replace(/(\n|\r)+/g, " ");
}

function createFeedItem(feedItem, trx) {
  if (!FEED_ITEM_TYPES.has(feedItem.type)) {
    throw new Error('Invalid feed item type ' + feedItem.type);
  }

  const dbRow = {
    'image_path': feedItem.imagePath,
    'text':       _sanitizeText(feedItem.text),
    'type':       feedItem.type,
    'city_id':    feedItem.city || knex.raw('(SELECT city_id FROM teams WHERE id = ?)', [feedItem.client.team]),
    // Division to bring time stamp's accuracy inline with postgres values.
    'hot_score':  _.round(score.hotScore(0, moment.utc().valueOf() / 1000), 4),
  };

  const location = feedItem.location;
  if (location) {
    // Tuple is in longitude, latitude format in Postgis
    dbRow.location = location.longitude + ',' + location.latitude;
  }
  if (feedItem.isBanned) {
    dbRow.is_banned = feedItem.isBanned;
  }

  if (feedItem.isSticky) {
    dbRow.is_sticky = feedItem.isSticky;
  }

  if (feedItem.type === 'IMAGE' && feedItem.client) {
    // Get event_id from user's last check in and check
    // that the event is still ongoing
    dbRow.event_id = knex.raw(`
      (SELECT id
      FROM events
      WHERE
        id = (
          SELECT MAX(event_id)
          FROM actions
          JOIN action_types ON action_types.id = actions.action_type_id
          WHERE
            code = 'CHECK_IN_EVENT' AND
            user_id = ?
        ) AND now() BETWEEN start_time AND end_time)
    `, [feedItem.client.id]);
  }

  if (feedItem.client) {
    dbRow.user_id = feedItem.client.id;
  }

  trx = trx || knex;

  return trx.returning('id').insert(dbRow).into('feed_items')
    .then(rows => {
      if (_.isEmpty(rows)) {
        throw new Error('Feed item row creation failed: ' + dbRow);
      }

      return rows.length;
    })
    .catch(err => {
      if (err.constraint === 'feed_items_city_id_foreign') {
        err.status = 404;
        err.message = `No such city id: ${ dbRow.city_id }`;
      }

      throw err;
    });
}

function deleteFeedItem(id, opts) {
  opts = opts || {};

  return knex('feed_items').delete().where({
    'id': id,
    'user_id': knex.raw('(SELECT id from users WHERE uuid = ?)', [opts.client.uuid])
  })
  .then(deletedCount => {
    if (deletedCount > 1) {
      logger.error('Deleted feed item', id, 'client uuid:', opts.client.uuid);
      throw new Error('Unexpected amount of deletes happened: ' + deletedCount)
    }

    return deletedCount;
  });
}

function _actionToFeedObject(row, client) {
  if (!client) {
    throw new Error('Client information not passed as a parameter');
  }

  var feedObj = {
    id: row['id'],
    type: row['action_type_code'],
    votes: row['votes'],
    userVote: row['user_vote'],
    hotScore: row['hot_score'],
    topScore: row['top_score'],
    author: {
      id: row['user_id'],
      name: row['user_name'],
      team: row['team_name'],
      type: _resolveAuthorType(row, client)
    },
    createdAt: row['created_at']
  };

  if (row.location) {
    feedObj.location = {
      latitude: row.location.y,
      longitude: row.location.x
    };
  }

  if (feedObj.type === 'IMAGE') {
    const imagePath = row['image_path'];

    if (process.env.DISABLE_IMGIX === 'true' || _.endsWith(imagePath, 'gif')) {
      feedObj.url = GCS_CONFIG.baseUrl + '/' + GCS_CONFIG.bucketName + '/' + imagePath;
    } else {
      feedObj.url =
        'https://' + GCS_CONFIG.bucketName + '.imgix.net/' + imagePath +
        process.env.IMGIX_QUERY;
    }
  } else if (feedObj.type === 'TEXT') {
    feedObj.text = row.text;
  }

  return feedObj;
}

function _getWhereSql(opts) {
  const whereClauses = ['NOT feed_items.is_sticky'];
  const params = [];

  if (opts.beforeId) {
    whereClauses.push('feed_items.id < ?');
    params.push(opts.beforeId);
  }

  if (!opts.client.isBanned) {
    whereClauses.push('NOT feed_items.is_banned');
  }

  if (opts.city) {
    whereClauses.push(`feed_items.city_id = ?`);
    params.push(opts.city);
  }

  if (opts.eventId) {
    whereClauses.push(`feed_items.event_id = ?`);
    params.push(opts.eventId);
  }

  if (opts.type) {
    whereClauses.push(`feed_items.type = ?`);
    params.push(opts.type);
  }

  if (_.isNumber(opts.userId)) {
    whereClauses.push(`feed_items.user_id = ?`);
    params.push(opts.userId);
  }

  if (opts.since) {
    console.log(opts.since);
    whereClauses.push(`feed_items.created_at >= ?`);
    params.push(opts.since);
  }

  return whereClauses.length > 0
    ? knex.raw(` WHERE ${ whereClauses.join(' AND ')}`, params).toString()
    : '';
}

function _getSortingSql(sort) {
  const { HOT, TOP } = CONST.FEED_SORT_TYPES;

  if (sort === HOT) {
    return 'ORDER BY is_sticky DESC, hot_score DESC, id DESC';
  } else if (sort === TOP) {
    return 'ORDER BY top_score DESC, id DESC';
  } else {
    // Defaults to 'NEW'
    return 'ORDER BY is_sticky DESC, id DESC';
  }
}

function _getTeamNameSql(cityId) {
  return !cityId
    ? `teams.name`
    : knex.raw(`CASE WHEN teams.city_id=? THEN teams.name ELSE cities.name END`, [cityId]).toString();
}

function _getSortTopSql() {
  return `
    LEFT JOIN (SELECT
      votes.feed_item_id as feed_item_id,
      wilsons(
        COUNT(CASE votes.value WHEN 1 THEN 1 ELSE NULL END),
        COUNT(CASE votes.value WHEN -1 THEN 1 ELSE NULL END)
      ) as value
      FROM votes
      GROUP BY votes.feed_item_id
    ) as top_score ON top_score.feed_item_id = feed_items.id
  `;
}

function _resolveAuthorType(row, client) {
  const rowUserId = row['user_id'];

  if (rowUserId === null) {
    return CONST.AUTHOR_TYPES.SYSTEM;
  } else if (rowUserId === client.id) {
    return CONST.AUTHOR_TYPES.ME;
  }

  return CONST.AUTHOR_TYPES.OTHER_USER;
}

export {
  getFeed,
  createFeedItem,
  deleteFeedItem,
};
<commit_msg>Updated commenting<commit_after>import _ from 'lodash';
const {knex} = require('../util/database').connect();
import {GCS_CONFIG} from '../util/gcs';
import CONST from '../constants';
const logger = require('../util/logger')(__filename);
import * as score from './hot-score';
import moment from 'moment-timezone';

const FEED_ITEM_TYPES = new Set(['IMAGE', 'TEXT', 'CHECK_IN']);

function getStickySqlString(city) {
  const whereCitySql = !city
    ? ' AND feed_items.city_id IS NULL'
    : knex.raw(` AND (feed_items.city_id = ? OR feed_items.city_id IS NULL)`, city).toString();

  // TODO top_score optimization
  return `
    (SELECT
      feed_items.id as id,
      feed_items.location as location,
      feed_items.created_at as created_at,
      feed_items.image_path as image_path,
      feed_items.text as text,
      feed_items.type as action_type_code,
      COALESCE(users.name, 'SYSTEM') as user_name,
      users.id as user_id,
      teams.name as team_name,
      vote_score(feed_items) as votes,
      feed_items.hot_score as hot_score,
      0 as top_score,
      feed_items.is_sticky,
      COALESCE(votes.value, 0) as user_vote
    FROM feed_items
    LEFT JOIN users ON users.id = feed_items.user_id
    LEFT JOIN teams ON teams.id = users.team_id
    LEFT JOIN votes ON votes.user_id = ? AND votes.feed_item_id = feed_items.id
    WHERE feed_items.is_sticky ${ whereCitySql }
    GROUP BY
      feed_items.id,
      users.name,
      users.id,
      teams.name,
      votes.value
    ORDER BY feed_items.id DESC
    LIMIT 2)`;
}

/**
 *
 * @param {object} opts
 * @param {number} opts.client.id  Client's database ID
 * @param {number} [opts.limit=20] How many results to return
 * @param {number} [opts.beforeId] Return only items before this feed item
 * @param {number} [opts.city]     Return feed items only from given city
 * @param {string} [opts.sort=new] One of ['hot', 'new', 'top']
 * @param {number} [opts.eventId]  Event whose feed items to return
 * @param {string} [opts.type]     Return only certain type items
 * @param {string} [opts.userId]   Return only certain user's items
 * @param {boolean} [opts.includeSticky = true]   Should sticky messages be included
 * @param {string} [opts.since]    Limit search to feed items created between 'since' and now.
 * @param {number} [opts.after]    Offset results by given amount.
 */
function getFeed(opts) {
  opts = _.merge({
    includeSticky: true,
    limit: 20
  }, opts);

  const sortTop = opts.sort === CONST.FEED_SORT_TYPES.TOP

  // TODO top_score optimization
  let sqlString = `
    (SELECT
      feed_items.id as id,
      feed_items.location as location,
      feed_items.created_at as created_at,
      feed_items.image_path as image_path,
      feed_items.text as text,
      feed_items.type as action_type_code,
      COALESCE(users.name, 'SYSTEM') as user_name,
      users.id as user_id,
      ${ _getTeamNameSql(opts.city) } as team_name,
      vote_score(feed_items) as votes,
      feed_items.hot_score as hot_score,
      ${ sortTop ? `COALESCE(top_score.value, 0)` : '0' } as top_score,
      feed_items.is_sticky,
      COALESCE(votes.value, 0) as user_vote
    FROM feed_items
    LEFT JOIN users ON users.id = feed_items.user_id
    LEFT JOIN teams ON teams.id = users.team_id
    LEFT JOIN votes ON votes.user_id = ? AND votes.feed_item_id = feed_items.id
    LEFT JOIN cities ON cities.id = teams.city_id
    ${ sortTop ? _getSortTopSql() : '' }
    ${ _getWhereSql(opts) }
    GROUP BY
        feed_items.id,
        users.name,
        users.id,
        teams.name,
        votes.value,
        teams.city_id,
        ${ sortTop ? `top_score.value,` : '' }
        cities.id)
    `;

  let params = [opts.client.id];

  // TODO: Sticky messages should have their own endpoint
  const includeSticky = opts.includeSticky && !opts.beforeId && sortTop;
  if (includeSticky) {
    sqlString = getStickySqlString(opts.city) + " UNION ALL " + sqlString;
    params.push(opts.client.id);
  }

  sqlString += _getSortingSql(opts.sort);
  sqlString += ` LIMIT ?`;
  params.push(opts.limit);

  if (opts.after) {
    sqlString +=  ` OFFSET ?`;
    params.push(opts.after);
  }

  return knex.raw(sqlString, params)
  .then(result => {
    const rows = result.rows;

    if (_.isEmpty(rows)) {
      return [];
    }

    return _.map(rows, row => _actionToFeedObject(row, opts.client));
  });
}

function _sanitizeText(text) {
  if (!text) {
    return text;
  }

  return text.replace(/(\n|\r)+/g, " ");
}

function createFeedItem(feedItem, trx) {
  if (!FEED_ITEM_TYPES.has(feedItem.type)) {
    throw new Error('Invalid feed item type ' + feedItem.type);
  }

  const dbRow = {
    'image_path': feedItem.imagePath,
    'text':       _sanitizeText(feedItem.text),
    'type':       feedItem.type,
    'city_id':    feedItem.city || knex.raw('(SELECT city_id FROM teams WHERE id = ?)', [feedItem.client.team]),
    // Division to bring time stamp's accuracy inline with postgres values.
    'hot_score':  _.round(score.hotScore(0, moment.utc().valueOf() / 1000), 4),
  };

  const location = feedItem.location;
  if (location) {
    // Tuple is in longitude, latitude format in Postgis
    dbRow.location = location.longitude + ',' + location.latitude;
  }
  if (feedItem.isBanned) {
    dbRow.is_banned = feedItem.isBanned;
  }

  if (feedItem.isSticky) {
    dbRow.is_sticky = feedItem.isSticky;
  }

  if (feedItem.type === 'IMAGE' && feedItem.client) {
    // Get event_id from user's last check in and check
    // that the event is still ongoing
    dbRow.event_id = knex.raw(`
      (SELECT id
      FROM events
      WHERE
        id = (
          SELECT MAX(event_id)
          FROM actions
          JOIN action_types ON action_types.id = actions.action_type_id
          WHERE
            code = 'CHECK_IN_EVENT' AND
            user_id = ?
        ) AND now() BETWEEN start_time AND end_time)
    `, [feedItem.client.id]);
  }

  if (feedItem.client) {
    dbRow.user_id = feedItem.client.id;
  }

  trx = trx || knex;

  return trx.returning('id').insert(dbRow).into('feed_items')
    .then(rows => {
      if (_.isEmpty(rows)) {
        throw new Error('Feed item row creation failed: ' + dbRow);
      }

      return rows.length;
    })
    .catch(err => {
      if (err.constraint === 'feed_items_city_id_foreign') {
        err.status = 404;
        err.message = `No such city id: ${ dbRow.city_id }`;
      }

      throw err;
    });
}

function deleteFeedItem(id, opts) {
  opts = opts || {};

  return knex('feed_items').delete().where({
    'id': id,
    'user_id': knex.raw('(SELECT id from users WHERE uuid = ?)', [opts.client.uuid])
  })
  .then(deletedCount => {
    if (deletedCount > 1) {
      logger.error('Deleted feed item', id, 'client uuid:', opts.client.uuid);
      throw new Error('Unexpected amount of deletes happened: ' + deletedCount)
    }

    return deletedCount;
  });
}

function _actionToFeedObject(row, client) {
  if (!client) {
    throw new Error('Client information not passed as a parameter');
  }

  var feedObj = {
    id: row['id'],
    type: row['action_type_code'],
    votes: row['votes'],
    userVote: row['user_vote'],
    hotScore: row['hot_score'],
    topScore: row['top_score'],
    author: {
      id: row['user_id'],
      name: row['user_name'],
      team: row['team_name'],
      type: _resolveAuthorType(row, client)
    },
    createdAt: row['created_at']
  };

  if (row.location) {
    feedObj.location = {
      latitude: row.location.y,
      longitude: row.location.x
    };
  }

  if (feedObj.type === 'IMAGE') {
    const imagePath = row['image_path'];

    if (process.env.DISABLE_IMGIX === 'true' || _.endsWith(imagePath, 'gif')) {
      feedObj.url = GCS_CONFIG.baseUrl + '/' + GCS_CONFIG.bucketName + '/' + imagePath;
    } else {
      feedObj.url =
        'https://' + GCS_CONFIG.bucketName + '.imgix.net/' + imagePath +
        process.env.IMGIX_QUERY;
    }
  } else if (feedObj.type === 'TEXT') {
    feedObj.text = row.text;
  }

  return feedObj;
}

function _getWhereSql(opts) {
  const whereClauses = ['NOT feed_items.is_sticky'];
  const params = [];

  if (opts.beforeId) {
    whereClauses.push('feed_items.id < ?');
    params.push(opts.beforeId);
  }

  if (!opts.client.isBanned) {
    whereClauses.push('NOT feed_items.is_banned');
  }

  if (opts.city) {
    whereClauses.push(`feed_items.city_id = ?`);
    params.push(opts.city);
  }

  if (opts.eventId) {
    whereClauses.push(`feed_items.event_id = ?`);
    params.push(opts.eventId);
  }

  if (opts.type) {
    whereClauses.push(`feed_items.type = ?`);
    params.push(opts.type);
  }

  if (_.isNumber(opts.userId)) {
    whereClauses.push(`feed_items.user_id = ?`);
    params.push(opts.userId);
  }

  if (opts.since) {
    console.log(opts.since);
    whereClauses.push(`feed_items.created_at >= ?`);
    params.push(opts.since);
  }

  return whereClauses.length > 0
    ? knex.raw(` WHERE ${ whereClauses.join(' AND ')}`, params).toString()
    : '';
}

function _getSortingSql(sort) {
  const { HOT, TOP } = CONST.FEED_SORT_TYPES;

  if (sort === HOT) {
    return 'ORDER BY is_sticky DESC, hot_score DESC, id DESC';
  } else if (sort === TOP) {
    return 'ORDER BY top_score DESC, id DESC';
  } else {
    // Defaults to 'NEW'
    return 'ORDER BY is_sticky DESC, id DESC';
  }
}

function _getTeamNameSql(cityId) {
  return !cityId
    ? `teams.name`
    : knex.raw(`CASE WHEN teams.city_id=? THEN teams.name ELSE cities.name END`, [cityId]).toString();
}

function _getSortTopSql() {
  return `
    LEFT JOIN (SELECT
      votes.feed_item_id as feed_item_id,
      wilsons(
        COUNT(CASE votes.value WHEN 1 THEN 1 ELSE NULL END),
        COUNT(CASE votes.value WHEN -1 THEN 1 ELSE NULL END)
      ) as value
      FROM votes
      GROUP BY votes.feed_item_id
    ) as top_score ON top_score.feed_item_id = feed_items.id
  `;
}

function _resolveAuthorType(row, client) {
  const rowUserId = row['user_id'];

  if (rowUserId === null) {
    return CONST.AUTHOR_TYPES.SYSTEM;
  } else if (rowUserId === client.id) {
    return CONST.AUTHOR_TYPES.ME;
  }

  return CONST.AUTHOR_TYPES.OTHER_USER;
}

export {
  getFeed,
  createFeedItem,
  deleteFeedItem,
};
<|endoftext|>


<commit_before>import _ from 'lodash';
const {knex} = require('../util/database').connect();
import {GCS_CONFIG} from '../util/gcs';
import CONST from '../constants';
const logger = require('../util/logger')(__filename);

const FEED_ITEM_TYPES = new Set(['IMAGE', 'TEXT', 'CHECK_IN']);

function getStickySqlString() {
  return `
    (SELECT
      feed_items.id as id,
      feed_items.location as location,
      feed_items.created_at as created_at,
      feed_items.image_path as image_path,
      feed_items.text as text,
      feed_items.type as action_type_code,
      COALESCE(users.name, 'SYSTEM') as user_name,
      users.uuid as user_uuid,
      teams.name as team_name,
      vote_score(feed_items) as votes,
      feed_items.hot_score as hot_score,
      feed_items.is_sticky,
      COALESCE(votes.value, 0) as user_vote
    FROM feed_items
    LEFT JOIN users ON users.id = feed_items.user_id
    LEFT JOIN teams ON teams.id = users.team_id
    LEFT JOIN votes ON votes.user_id = ? AND votes.feed_item_id = feed_items.id
    WHERE feed_items.is_sticky
    GROUP BY
      feed_items.id,
      users.name,
      users.uuid,
      teams.name,
      votes.value
    ORDER BY feed_items.id DESC
    LIMIT 2)`;
}

/**
 *
 * @param {object} opts
 * @param {number} opts.client.id  Client's database ID
 * @param {number} [opts.limit=20] How many results to return
 * @param {number} [opts.beforeId] Return only items before this feed item
 * @param {number} [opts.city]     Return feed items only from given city
 * @param {string} [opts.sort=new] Either 'hot' or 'new'
 * @param {number} [opts.eventId]  Event whose feed items to return
 * @param {string} [opts.type]     Return only certain type items
 */
function getFeed(opts) {
  opts = _.merge({
    limit: 20
  }, opts);

  let sqlString = `
    (SELECT
      feed_items.id as id,
      feed_items.location as location,
      feed_items.created_at as created_at,
      feed_items.image_path as image_path,
      feed_items.text as text,
      feed_items.type as action_type_code,
      COALESCE(users.name, 'SYSTEM') as user_name,
      users.uuid as user_uuid,
      teams.name as team_name,
      vote_score(feed_items) as votes,
      feed_items.hot_score as hot_score,
      feed_items.is_sticky,
      COALESCE(votes.value, 0) as user_vote
    FROM feed_items
    LEFT JOIN users ON users.id = feed_items.user_id
    LEFT JOIN teams ON teams.id = users.team_id
    LEFT JOIN votes ON votes.user_id = ? AND votes.feed_item_id = feed_items.id
    `;

  let params = [opts.client.id];
  let whereClauses = ['NOT feed_items.is_sticky'];

  // TODO: Sticky messages should have their own endpoint
  const includeSticky = !opts.beforeId && !opts.eventId;
  if (includeSticky) {
    sqlString = getStickySqlString() + " UNION ALL " + sqlString;
    params.push(opts.client.id);
  }

  if (opts.beforeId) {
    whereClauses.push('feed_items.id < ?');
    params.push(opts.beforeId);
  }

  if (!opts.client.isBanned) {
    whereClauses.push('NOT feed_items.is_banned');
  }

  if (opts.city) {
    whereClauses.push(`feed_items.city_id = ?`);
    params.push(opts.city);
  }

  if (opts.eventId) {
    whereClauses.push(`feed_items.event_id = ?`);
    params.push(opts.eventId);
  }

  if (opts.type) {
    whereClauses.push(`feed_items.type = ?`);
    params.push(opts.type);
  }

  if (whereClauses.length > 0) {
    sqlString += ` WHERE ${ whereClauses.join(' AND ')}`;
  }
  sqlString += ` GROUP BY feed_items.id, users.name, users.uuid, teams.name, votes.value ) `;
  sqlString += _getSortingSql(opts.sort);
  sqlString += ` LIMIT ?`;
  params.push(opts.limit);

  return knex.raw(sqlString, params)
  .then(result => {
    const rows = result.rows;

    if (_.isEmpty(rows)) {
      return [];
    }

    return _.map(rows, row => _actionToFeedObject(row, opts.client));
  });
}

function createFeedItem(feedItem, trx) {
  if (!FEED_ITEM_TYPES.has(feedItem.type)) {
    throw new Error('Invalid feed item type ' + feedItem.type);
  }

  const dbRow = {
    'image_path': feedItem.imagePath,
    'text':       feedItem.text,
    'type':       feedItem.type,
    'city_id':    feedItem.city || knex.raw('(SELECT city_id FROM teams WHERE id = ?)', [feedItem.client.team]),
  };

  const location = feedItem.location;
  if (location) {
    // Tuple is in longitude, latitude format in Postgis
    dbRow.location = location.longitude + ',' + location.latitude;
  }
  if (feedItem.isBanned) {
    dbRow.is_banned = feedItem.isBanned;
  }

  if (feedItem.isSticky) {
    dbRow.is_sticky = feedItem.isSticky;
  }

  if (feedItem.type === 'IMAGE' && feedItem.client) {
    // Get event_id from user's last check in and check
    // that the event is still ongoing
    dbRow.event_id = knex.raw(`
      (SELECT id
      FROM events
      WHERE
        id = (
          SELECT MAX(event_id)
          FROM actions
          JOIN action_types ON action_types.id = actions.action_type_id
          WHERE
            code = 'CHECK_IN_EVENT' AND
            user_id = ?
        ) AND now() BETWEEN start_time AND end_time)
    `, [feedItem.client.id]);
  }

  if (feedItem.client) {
    dbRow.user_id = feedItem.client.id;
  }

  trx = trx || knex;

  return trx.returning('id').insert(dbRow).into('feed_items')
    .then(rows => {
      if (_.isEmpty(rows)) {
        throw new Error('Feed item row creation failed: ' + dbRow);
      }

      return rows.length;
    })
    .catch(err => {
      if (err.constraint === 'feed_items_city_id_foreign') {
        err.status = 404;
        err.message = `No such city id: ${ dbRow.city_id }`;
      }

      throw err;
    });
}

function deleteFeedItem(id, opts) {
  opts = opts || {};

  return knex('feed_items').delete().where({
    'id': id,
    'user_id': knex.raw('(SELECT id from users WHERE uuid = ?)', [opts.client.uuid])
  })
  .then(deletedCount => {
    if (deletedCount > 1) {
      logger.error('Deleted feed item', id, 'client uuid:', opts.client.uuid);
      throw new Error('Unexpected amount of deletes happened: ' + deletedCount)
    }

    return deletedCount;
  });
}

function _actionToFeedObject(row, client) {
  if (!client) {
    throw new Error('Client information not passed as a parameter');
  }

  var feedObj = {
    id: row['id'],
    type: row['action_type_code'],
    votes: row['votes'],
    userVote: row['user_vote'],
    hotScore: row['hot_score'],
    author: {
      name: row['user_name'],
      team: row['team_name'],
      type: _resolveAuthorType(row, client)
    },
    createdAt: row['created_at']
  };

  if (row.location) {
    feedObj.location = {
      latitude: row.location.y,
      longitude: row.location.x
    };
  }

  if (feedObj.type === 'IMAGE') {
    const imagePath = row['image_path'];

    if (process.env.DISABLE_IMGIX === 'true' || _.endsWith(imagePath, 'gif')) {
      feedObj.url = GCS_CONFIG.baseUrl + '/' + GCS_CONFIG.bucketName + '/' + imagePath;
    } else {
      feedObj.url =
        'https://' + GCS_CONFIG.bucketName + '.imgix.net/' + imagePath +
        process.env.IMGIX_QUERY;
    }
  } else if (feedObj.type === 'TEXT') {
    feedObj.text = row.text;
  }

  return feedObj;
}

function _getSortingSql(sort) {
  const { HOT } = CONST.FEED_SORT_TYPES;

  if (sort === HOT) {
    return 'ORDER BY is_sticky DESC, hot_score DESC, id DESC';
  } else {
    // Defaults to 'NEW'
    return 'ORDER BY is_sticky DESC, id DESC';
  }
}

function _resolveAuthorType(row, client) {
  const rowUserUuid = row['user_uuid'];

  if (rowUserUuid === null) {
    return CONST.AUTHOR_TYPES.SYSTEM;
  } else if (rowUserUuid === client.uuid) {
    return CONST.AUTHOR_TYPES.ME;
  }

  return CONST.AUTHOR_TYPES.OTHER_USER;
}

export {
  getFeed,
  createFeedItem,
  deleteFeedItem,
};
<commit_msg>Give post a hotscore on creation<commit_after>import _ from 'lodash';
const {knex} = require('../util/database').connect();
import {GCS_CONFIG} from '../util/gcs';
import CONST from '../constants';
const logger = require('../util/logger')(__filename);
import * as score from './hot-score';
import moment from 'moment-timezone';

const FEED_ITEM_TYPES = new Set(['IMAGE', 'TEXT', 'CHECK_IN']);

function getStickySqlString() {
  return `
    (SELECT
      feed_items.id as id,
      feed_items.location as location,
      feed_items.created_at as created_at,
      feed_items.image_path as image_path,
      feed_items.text as text,
      feed_items.type as action_type_code,
      COALESCE(users.name, 'SYSTEM') as user_name,
      users.uuid as user_uuid,
      teams.name as team_name,
      vote_score(feed_items) as votes,
      feed_items.hot_score as hot_score,
      feed_items.is_sticky,
      COALESCE(votes.value, 0) as user_vote
    FROM feed_items
    LEFT JOIN users ON users.id = feed_items.user_id
    LEFT JOIN teams ON teams.id = users.team_id
    LEFT JOIN votes ON votes.user_id = ? AND votes.feed_item_id = feed_items.id
    WHERE feed_items.is_sticky
    GROUP BY
      feed_items.id,
      users.name,
      users.uuid,
      teams.name,
      votes.value
    ORDER BY feed_items.id DESC
    LIMIT 2)`;
}

/**
 *
 * @param {object} opts
 * @param {number} opts.client.id  Client's database ID
 * @param {number} [opts.limit=20] How many results to return
 * @param {number} [opts.beforeId] Return only items before this feed item
 * @param {number} [opts.city]     Return feed items only from given city
 * @param {string} [opts.sort=new] Either 'hot' or 'new'
 * @param {number} [opts.eventId]  Event whose feed items to return
 * @param {string} [opts.type]     Return only certain type items
 */
function getFeed(opts) {
  opts = _.merge({
    limit: 20
  }, opts);

  let sqlString = `
    (SELECT
      feed_items.id as id,
      feed_items.location as location,
      feed_items.created_at as created_at,
      feed_items.image_path as image_path,
      feed_items.text as text,
      feed_items.type as action_type_code,
      COALESCE(users.name, 'SYSTEM') as user_name,
      users.uuid as user_uuid,
      teams.name as team_name,
      vote_score(feed_items) as votes,
      feed_items.hot_score as hot_score,
      feed_items.is_sticky,
      COALESCE(votes.value, 0) as user_vote
    FROM feed_items
    LEFT JOIN users ON users.id = feed_items.user_id
    LEFT JOIN teams ON teams.id = users.team_id
    LEFT JOIN votes ON votes.user_id = ? AND votes.feed_item_id = feed_items.id
    `;

  let params = [opts.client.id];
  let whereClauses = ['NOT feed_items.is_sticky'];

  // TODO: Sticky messages should have their own endpoint
  const includeSticky = !opts.beforeId && !opts.eventId;
  if (includeSticky) {
    sqlString = getStickySqlString() + " UNION ALL " + sqlString;
    params.push(opts.client.id);
  }

  if (opts.beforeId) {
    whereClauses.push('feed_items.id < ?');
    params.push(opts.beforeId);
  }

  if (!opts.client.isBanned) {
    whereClauses.push('NOT feed_items.is_banned');
  }

  if (opts.city) {
    whereClauses.push(`feed_items.city_id = ?`);
    params.push(opts.city);
  }

  if (opts.eventId) {
    whereClauses.push(`feed_items.event_id = ?`);
    params.push(opts.eventId);
  }

  if (opts.type) {
    whereClauses.push(`feed_items.type = ?`);
    params.push(opts.type);
  }

  if (whereClauses.length > 0) {
    sqlString += ` WHERE ${ whereClauses.join(' AND ')}`;
  }
  sqlString += ` GROUP BY feed_items.id, users.name, users.uuid, teams.name, votes.value ) `;
  sqlString += _getSortingSql(opts.sort);
  sqlString += ` LIMIT ?`;
  params.push(opts.limit);

  return knex.raw(sqlString, params)
  .then(result => {
    const rows = result.rows;

    if (_.isEmpty(rows)) {
      return [];
    }

    return _.map(rows, row => _actionToFeedObject(row, opts.client));
  });
}

function createFeedItem(feedItem, trx) {
  if (!FEED_ITEM_TYPES.has(feedItem.type)) {
    throw new Error('Invalid feed item type ' + feedItem.type);
  }

  const dbRow = {
    'image_path': feedItem.imagePath,
    'text':       feedItem.text,
    'type':       feedItem.type,
    'city_id':    feedItem.city || knex.raw('(SELECT city_id FROM teams WHERE id = ?)', [feedItem.client.team]),
    // Division to bring time stamp's accuracy inline with postgres values.
    'hot_score':  _.round(score.hotScore(0, moment.utc().valueOf() / 1000), 4),
  };

  const location = feedItem.location;
  if (location) {
    // Tuple is in longitude, latitude format in Postgis
    dbRow.location = location.longitude + ',' + location.latitude;
  }
  if (feedItem.isBanned) {
    dbRow.is_banned = feedItem.isBanned;
  }

  if (feedItem.isSticky) {
    dbRow.is_sticky = feedItem.isSticky;
  }

  if (feedItem.type === 'IMAGE' && feedItem.client) {
    // Get event_id from user's last check in and check
    // that the event is still ongoing
    dbRow.event_id = knex.raw(`
      (SELECT id
      FROM events
      WHERE
        id = (
          SELECT MAX(event_id)
          FROM actions
          JOIN action_types ON action_types.id = actions.action_type_id
          WHERE
            code = 'CHECK_IN_EVENT' AND
            user_id = ?
        ) AND now() BETWEEN start_time AND end_time)
    `, [feedItem.client.id]);
  }

  if (feedItem.client) {
    dbRow.user_id = feedItem.client.id;
  }

  trx = trx || knex;

  return trx.returning('id').insert(dbRow).into('feed_items')
    .then(rows => {
      if (_.isEmpty(rows)) {
        throw new Error('Feed item row creation failed: ' + dbRow);
      }

      return rows.length;
    })
    .catch(err => {
      if (err.constraint === 'feed_items_city_id_foreign') {
        err.status = 404;
        err.message = `No such city id: ${ dbRow.city_id }`;
      }

      throw err;
    });
}

function deleteFeedItem(id, opts) {
  opts = opts || {};

  return knex('feed_items').delete().where({
    'id': id,
    'user_id': knex.raw('(SELECT id from users WHERE uuid = ?)', [opts.client.uuid])
  })
  .then(deletedCount => {
    if (deletedCount > 1) {
      logger.error('Deleted feed item', id, 'client uuid:', opts.client.uuid);
      throw new Error('Unexpected amount of deletes happened: ' + deletedCount)
    }

    return deletedCount;
  });
}

function _actionToFeedObject(row, client) {
  if (!client) {
    throw new Error('Client information not passed as a parameter');
  }

  var feedObj = {
    id: row['id'],
    type: row['action_type_code'],
    votes: row['votes'],
    userVote: row['user_vote'],
    hotScore: row['hot_score'],
    author: {
      name: row['user_name'],
      team: row['team_name'],
      type: _resolveAuthorType(row, client)
    },
    createdAt: row['created_at']
  };

  if (row.location) {
    feedObj.location = {
      latitude: row.location.y,
      longitude: row.location.x
    };
  }

  if (feedObj.type === 'IMAGE') {
    const imagePath = row['image_path'];

    if (process.env.DISABLE_IMGIX === 'true' || _.endsWith(imagePath, 'gif')) {
      feedObj.url = GCS_CONFIG.baseUrl + '/' + GCS_CONFIG.bucketName + '/' + imagePath;
    } else {
      feedObj.url =
        'https://' + GCS_CONFIG.bucketName + '.imgix.net/' + imagePath +
        process.env.IMGIX_QUERY;
    }
  } else if (feedObj.type === 'TEXT') {
    feedObj.text = row.text;
  }

  return feedObj;
}

function _getSortingSql(sort) {
  const { HOT } = CONST.FEED_SORT_TYPES;

  if (sort === HOT) {
    return 'ORDER BY is_sticky DESC, hot_score DESC, id DESC';
  } else {
    // Defaults to 'NEW'
    return 'ORDER BY is_sticky DESC, id DESC';
  }
}

function _resolveAuthorType(row, client) {
  const rowUserUuid = row['user_uuid'];

  if (rowUserUuid === null) {
    return CONST.AUTHOR_TYPES.SYSTEM;
  } else if (rowUserUuid === client.uuid) {
    return CONST.AUTHOR_TYPES.ME;
  }

  return CONST.AUTHOR_TYPES.OTHER_USER;
}

export {
  getFeed,
  createFeedItem,
  deleteFeedItem,
};
<|endoftext|>