<commit_before>// @flow

import {
  messageTypes,
  messageDataLocalID,
  type MessageData,
  type RawMessageInfo,
} from 'lib/types/message-types';
import { threadPermissions } from 'lib/types/thread-types';
import { updateTypes } from 'lib/types/update-types';
import { redisMessageTypes } from 'lib/types/redis-types';
import type { Viewer } from '../session/viewer';

import invariant from 'invariant';

import {
  rawMessageInfoFromMessageData,
  messageTypeGeneratesNotifs,
  shimUnsupportedRawMessageInfos,
} from 'lib/shared/message-utils';
import { permissionLookup } from 'lib/permissions/thread-permissions';

import {
  dbQuery,
  SQL,
  SQLStatement,
  appendSQLArray,
  mergeAndConditions,
  mergeOrConditions,
} from '../database';
import createIDs from './id-creator';
import { sendPushNotifs } from '../push/send';
import { createUpdates } from './update-creator';
import { handleAsyncPromise } from '../responders/handlers';
import { earliestFocusedTimeConsideredCurrent } from '../shared/focused-times';
import { fetchOtherSessionsForViewer } from '../fetchers/session-fetchers';
import { publisher } from '../socket/redis';
import { fetchMessageInfoForLocalID } from '../fetchers/message-fetchers';
import { creationString } from '../utils/idempotent';

// Does not do permission checks! (checkThreadPermission)
async function createMessages(
  viewer: Viewer,
  messageDatas: $ReadOnlyArray<MessageData>,
): Promise<RawMessageInfo[]> {
  if (messageDatas.length === 0) {
    return [];
  }

  const messageInfos: RawMessageInfo[] = [];
  const newMessageDatas: MessageData[] = [];
  const existingMessages = await Promise.all(
    messageDatas.map(messageData => fetchMessageInfoForLocalID(
      viewer,
      messageDataLocalID(messageData),
    ))
  );
  for (let i = 0; i < existingMessages.length; i++) {
    const existingMessage = existingMessages[i];
    if (existingMessage) {
      messageInfos.push(existingMessage);
    } else {
      newMessageDatas.push(messageDatas[i]);
    }
  }
  if (newMessageDatas.length === 0) {
    return shimUnsupportedRawMessageInfos(
      messageInfos,
      viewer.platformDetails,
    );
  }

  const ids = await createIDs("messages", newMessageDatas.length);

  const subthreadPermissionsToCheck: Set<string> = new Set();
  const threadsToMessageIndices: Map<string, number[]> = new Map();
  const messageInsertRows = [];
  for (let i = 0; i < newMessageDatas.length; i++) {
    const messageData = newMessageDatas[i];
    const threadID = messageData.threadID;
    const creatorID = messageData.creatorID;

    if (messageData.type === messageTypes.CREATE_SUB_THREAD) {
      subthreadPermissionsToCheck.add(messageData.childThreadID);
    }

    let messageIndices = threadsToMessageIndices.get(threadID);
    if (!messageIndices) {
      messageIndices = [];
      threadsToMessageIndices.set(threadID, messageIndices);
    }
    messageIndices.push(i);

    let content;
    if (messageData.type === messageTypes.CREATE_THREAD) {
      content = JSON.stringify(messageData.initialThreadState);
    } else if (messageData.type === messageTypes.CREATE_SUB_THREAD) {
      content = messageData.childThreadID;
    } else if (messageData.type === messageTypes.TEXT) {
      content = messageData.text;
    } else if (messageData.type === messageTypes.ADD_MEMBERS) {
      content = JSON.stringify(messageData.addedUserIDs);
    } else if (messageData.type === messageTypes.CHANGE_SETTINGS) {
      content = JSON.stringify({
        [messageData.field]: messageData.value,
      });
    } else if (messageData.type === messageTypes.REMOVE_MEMBERS) {
      content = JSON.stringify(messageData.removedUserIDs);
    } else if (messageData.type === messageTypes.CHANGE_ROLE) {
      content = JSON.stringify({
        userIDs: messageData.userIDs,
        newRole: messageData.newRole,
      });
    } else if (
      messageData.type === messageTypes.CREATE_ENTRY ||
      messageData.type === messageTypes.EDIT_ENTRY ||
      messageData.type === messageTypes.DELETE_ENTRY ||
      messageData.type === messageTypes.RESTORE_ENTRY
    ) {
      content = JSON.stringify({
        entryID: messageData.entryID,
        date: messageData.date,
        text: messageData.text,
      });
    } else if (messageData.type === messageTypes.MULTIMEDIA) {
      content = JSON.stringify(messageData.media.map(({ id }) => parseInt(id)));
    }

    const creation = messageData.localID && viewer.hasSessionInfo
      ? creationString(viewer, messageData.localID)
      : null;

    messageInsertRows.push([
      ids[i],
      threadID,
      creatorID,
      messageData.type,
      content,
      messageData.time,
      creation,
    ]);
    messageInfos.push(rawMessageInfoFromMessageData(messageData, ids[i]));
  }

  handleAsyncPromise(postMessageSend(
    viewer,
    threadsToMessageIndices,
    subthreadPermissionsToCheck,
    messageInfos,
  ));

  const messageInsertQuery = SQL`
    INSERT INTO messages(id, thread, user, type, content, time, creation)
    VALUES ${messageInsertRows}
  `;
  await dbQuery(messageInsertQuery);

  return shimUnsupportedRawMessageInfos(
    messageInfos,
    viewer.platformDetails,
  );
}

// Handles:
// (1) Sending push notifs
// (2) Setting threads to unread and generating corresponding UpdateInfos
// (3) Publishing to Redis so that active sockets pass on new messages
async function postMessageSend(
  viewer: Viewer,
  threadsToMessageIndices: Map<string, number[]>,
  subthreadPermissionsToCheck: Set<string>,
  messageInfos: RawMessageInfo[],
) {
  let joinIndex = 0;
  let subthreadSelects = "";
  const subthreadJoins = [];
  for (let subthread of subthreadPermissionsToCheck) {
    const index = joinIndex++;
    subthreadSelects += `
      ,
      stm${index}.permissions AS subthread${subthread}_permissions,
      stm${index}.role AS subthread${subthread}_role
    `;
    const join = SQL`LEFT JOIN memberships `;
    join.append(`stm${index} ON stm${index}.`);
    join.append(SQL`thread = ${subthread} AND `);
    join.append(`stm${index}.user = m.user`);
    subthreadJoins.push(join);
  }

  const time = earliestFocusedTimeConsideredCurrent();
  const visibleExtractString = `$.${threadPermissions.VISIBLE}.value`;
  const query = SQL`
    SELECT m.user, m.thread, c.platform, c.device_token, c.versions,
      f.user AS focused_user
  `;
  query.append(subthreadSelects);
  query.append(SQL`
    FROM memberships m
    LEFT JOIN cookies c ON c.user = m.user AND c.device_token IS NOT NULL
    LEFT JOIN focused f ON f.user = m.user AND f.thread = m.thread
      AND f.time > ${time}
  `);
  appendSQLArray(query, subthreadJoins, SQL` `);
  query.append(SQL`
    WHERE (m.role != 0 OR f.user IS NOT NULL) AND
      JSON_EXTRACT(m.permissions, ${visibleExtractString}) IS TRUE AND
      m.thread IN (${[...threadsToMessageIndices.keys()]})
  `);

  const perUserInfo = new Map();
  const [ result ] = await dbQuery(query);
  for (let row of result) {
    const userID = row.user.toString();
    const threadID = row.thread.toString();
    const deviceToken = row.device_token;
    const focusedUser = !!row.focused_user;
    const { platform, versions } = row;
    let thisUserInfo = perUserInfo.get(userID);
    if (!thisUserInfo) {
      thisUserInfo = {
        devices: new Map(),
        threadIDs: new Set(),
        notFocusedThreadIDs: new Set(),
        subthreadsCanNotify: new Set(),
        subthreadsCanSetToUnread: new Set(),
      };
      perUserInfo.set(userID, thisUserInfo);
      // Subthread info will be the same for each subthread, so we only parse
      // it once
      for (let subthread of subthreadPermissionsToCheck) {
        const isSubthreadMember = !!row[`subthread${subthread}_role`];
        const permissions = row[`subthread${subthread}_permissions`];
        const canSeeSubthread =
          permissionLookup(permissions, threadPermissions.KNOW_OF);
        if (!canSeeSubthread) {
          continue;
        }
        thisUserInfo.subthreadsCanSetToUnread.add(subthread);
        // Only include the notification from the superthread if there is no
        // notification from the subthread
        if (
          !isSubthreadMember ||
          !permissionLookup(permissions, threadPermissions.VISIBLE)
        ) {
          thisUserInfo.subthreadsCanNotify.add(subthread);
        }
      }
    }
    if (deviceToken) {
      thisUserInfo.devices.set(deviceToken, {
        deviceType: platform,
        deviceToken,
        codeVersion: versions ? versions.codeVersion : null,
      });
    }
    thisUserInfo.threadIDs.add(threadID);
    if (!focusedUser) {
      thisUserInfo.notFocusedThreadIDs.add(threadID);
    }
  }

  const pushInfo = {}, setUnreadPairs = [], messageInfosPerUser = {};
  for (let pair of perUserInfo) {
    const [ userID, preUserPushInfo ] = pair;
    const { subthreadsCanSetToUnread, subthreadsCanNotify } = preUserPushInfo;
    const userPushInfo = {
      devices: [...preUserPushInfo.devices.values()],
      messageInfos: [],
    };
    const threadIDsToSetToUnread = new Set();
    for (let threadID of preUserPushInfo.notFocusedThreadIDs) {
      const messageIndices = threadsToMessageIndices.get(threadID);
      invariant(messageIndices, `indices should exist for thread ${threadID}`);
      for (let messageIndex of messageIndices) {
        const messageInfo = messageInfos[messageIndex];
        if (messageInfo.creatorID === userID) {
          continue;
        }
        if (
          messageInfo.type !== messageTypes.CREATE_SUB_THREAD ||
          subthreadsCanSetToUnread.has(messageInfo.childThreadID)
        ) {
          threadIDsToSetToUnread.add(threadID);
        }
        if (!messageTypeGeneratesNotifs(messageInfo.type)) {
          continue;
        }
        if (
          messageInfo.type !== messageTypes.CREATE_SUB_THREAD ||
          subthreadsCanNotify.has(messageInfo.childThreadID)
        ) {
          userPushInfo.messageInfos.push(messageInfo);
        }
      }
    }
    if (
      userPushInfo.devices.length > 0 &&
      userPushInfo.messageInfos.length > 0
    ) {
      pushInfo[userID] = userPushInfo;
    }
    for (let threadID of threadIDsToSetToUnread) {
      setUnreadPairs.push({ userID, threadID });
    }
    const userMessageInfos = [];
    for (let threadID of preUserPushInfo.threadIDs) {
      const messageIndices = threadsToMessageIndices.get(threadID);
      invariant(messageIndices, `indices should exist for thread ${threadID}`);
      for (let messageIndex of messageIndices) {
        const messageInfo = messageInfos[messageIndex];
        userMessageInfos.push(messageInfo);
      }
    }
    if (userMessageInfos.length > 0) {
      messageInfosPerUser[userID] = userMessageInfos;
    }
  }

  await Promise.all([
    sendPushNotifs(pushInfo),
    updateUnreadStatus(setUnreadPairs),
    redisPublish(viewer, messageInfosPerUser),
  ]);
}

async function updateUnreadStatus(
  setUnreadPairs: $ReadOnlyArray<{| userID: string, threadID: string |}>,
) {
  if (setUnreadPairs.length === 0) {
    return;
  }
  const updateConditions = setUnreadPairs.map(
    pair => SQL`(user = ${pair.userID} AND thread = ${pair.threadID})`,
  );
  const updateQuery = SQL`
    UPDATE memberships
    SET unread = 1
    WHERE
  `;
  updateQuery.append(mergeOrConditions(updateConditions));

  const now = Date.now();
  await Promise.all([
    dbQuery(updateQuery),
    createUpdates(setUnreadPairs.map(pair => ({
      type: updateTypes.UPDATE_THREAD_READ_STATUS,
      userID: pair.userID,
      time: now,
      threadID: pair.threadID,
      unread: true,
    }))),
  ]);
}

async function redisPublish(
  viewer: Viewer,
  messageInfosPerUser: {[userID: string]: $ReadOnlyArray<RawMessageInfo>},
) {
  for (let userID in messageInfosPerUser) {
    if (userID === viewer.userID && viewer.hasSessionInfo) {
      continue;
    }
    const messageInfos = messageInfosPerUser[userID];
    publisher.sendMessage(
      { userID },
      {
        type: redisMessageTypes.NEW_MESSAGES,
        messages: messageInfos,
      },
    );
  }
  const viewerMessageInfos = messageInfosPerUser[viewer.userID];
  if (!viewerMessageInfos || !viewer.hasSessionInfo) {
    return;
  }
  const sessionIDs = await fetchOtherSessionsForViewer(viewer);
  for (let sessionID of sessionIDs) {
    publisher.sendMessage(
      { userID: viewer.userID, sessionID },
      {
        type: redisMessageTypes.NEW_MESSAGES,
        messages: viewerMessageInfos,
      },
    );
  }
}

export default createMessages;
<commit_msg>[server] Wait until unread status is updated before calling sendPushNotifs<commit_after>// @flow

import {
  messageTypes,
  messageDataLocalID,
  type MessageData,
  type RawMessageInfo,
} from 'lib/types/message-types';
import { threadPermissions } from 'lib/types/thread-types';
import { updateTypes } from 'lib/types/update-types';
import { redisMessageTypes } from 'lib/types/redis-types';
import type { Viewer } from '../session/viewer';

import invariant from 'invariant';

import {
  rawMessageInfoFromMessageData,
  messageTypeGeneratesNotifs,
  shimUnsupportedRawMessageInfos,
} from 'lib/shared/message-utils';
import { permissionLookup } from 'lib/permissions/thread-permissions';

import {
  dbQuery,
  SQL,
  SQLStatement,
  appendSQLArray,
  mergeAndConditions,
  mergeOrConditions,
} from '../database';
import createIDs from './id-creator';
import { sendPushNotifs } from '../push/send';
import { createUpdates } from './update-creator';
import { handleAsyncPromise } from '../responders/handlers';
import { earliestFocusedTimeConsideredCurrent } from '../shared/focused-times';
import { fetchOtherSessionsForViewer } from '../fetchers/session-fetchers';
import { publisher } from '../socket/redis';
import { fetchMessageInfoForLocalID } from '../fetchers/message-fetchers';
import { creationString } from '../utils/idempotent';

// Does not do permission checks! (checkThreadPermission)
async function createMessages(
  viewer: Viewer,
  messageDatas: $ReadOnlyArray<MessageData>,
): Promise<RawMessageInfo[]> {
  if (messageDatas.length === 0) {
    return [];
  }

  const messageInfos: RawMessageInfo[] = [];
  const newMessageDatas: MessageData[] = [];
  const existingMessages = await Promise.all(
    messageDatas.map(messageData => fetchMessageInfoForLocalID(
      viewer,
      messageDataLocalID(messageData),
    ))
  );
  for (let i = 0; i < existingMessages.length; i++) {
    const existingMessage = existingMessages[i];
    if (existingMessage) {
      messageInfos.push(existingMessage);
    } else {
      newMessageDatas.push(messageDatas[i]);
    }
  }
  if (newMessageDatas.length === 0) {
    return shimUnsupportedRawMessageInfos(
      messageInfos,
      viewer.platformDetails,
    );
  }

  const ids = await createIDs("messages", newMessageDatas.length);

  const subthreadPermissionsToCheck: Set<string> = new Set();
  const threadsToMessageIndices: Map<string, number[]> = new Map();
  const messageInsertRows = [];
  for (let i = 0; i < newMessageDatas.length; i++) {
    const messageData = newMessageDatas[i];
    const threadID = messageData.threadID;
    const creatorID = messageData.creatorID;

    if (messageData.type === messageTypes.CREATE_SUB_THREAD) {
      subthreadPermissionsToCheck.add(messageData.childThreadID);
    }

    let messageIndices = threadsToMessageIndices.get(threadID);
    if (!messageIndices) {
      messageIndices = [];
      threadsToMessageIndices.set(threadID, messageIndices);
    }
    messageIndices.push(i);

    let content;
    if (messageData.type === messageTypes.CREATE_THREAD) {
      content = JSON.stringify(messageData.initialThreadState);
    } else if (messageData.type === messageTypes.CREATE_SUB_THREAD) {
      content = messageData.childThreadID;
    } else if (messageData.type === messageTypes.TEXT) {
      content = messageData.text;
    } else if (messageData.type === messageTypes.ADD_MEMBERS) {
      content = JSON.stringify(messageData.addedUserIDs);
    } else if (messageData.type === messageTypes.CHANGE_SETTINGS) {
      content = JSON.stringify({
        [messageData.field]: messageData.value,
      });
    } else if (messageData.type === messageTypes.REMOVE_MEMBERS) {
      content = JSON.stringify(messageData.removedUserIDs);
    } else if (messageData.type === messageTypes.CHANGE_ROLE) {
      content = JSON.stringify({
        userIDs: messageData.userIDs,
        newRole: messageData.newRole,
      });
    } else if (
      messageData.type === messageTypes.CREATE_ENTRY ||
      messageData.type === messageTypes.EDIT_ENTRY ||
      messageData.type === messageTypes.DELETE_ENTRY ||
      messageData.type === messageTypes.RESTORE_ENTRY
    ) {
      content = JSON.stringify({
        entryID: messageData.entryID,
        date: messageData.date,
        text: messageData.text,
      });
    } else if (messageData.type === messageTypes.MULTIMEDIA) {
      content = JSON.stringify(messageData.media.map(({ id }) => parseInt(id)));
    }

    const creation = messageData.localID && viewer.hasSessionInfo
      ? creationString(viewer, messageData.localID)
      : null;

    messageInsertRows.push([
      ids[i],
      threadID,
      creatorID,
      messageData.type,
      content,
      messageData.time,
      creation,
    ]);
    messageInfos.push(rawMessageInfoFromMessageData(messageData, ids[i]));
  }

  handleAsyncPromise(postMessageSend(
    viewer,
    threadsToMessageIndices,
    subthreadPermissionsToCheck,
    messageInfos,
  ));

  const messageInsertQuery = SQL`
    INSERT INTO messages(id, thread, user, type, content, time, creation)
    VALUES ${messageInsertRows}
  `;
  await dbQuery(messageInsertQuery);

  return shimUnsupportedRawMessageInfos(
    messageInfos,
    viewer.platformDetails,
  );
}

// Handles:
// (1) Sending push notifs
// (2) Setting threads to unread and generating corresponding UpdateInfos
// (3) Publishing to Redis so that active sockets pass on new messages
async function postMessageSend(
  viewer: Viewer,
  threadsToMessageIndices: Map<string, number[]>,
  subthreadPermissionsToCheck: Set<string>,
  messageInfos: RawMessageInfo[],
) {
  let joinIndex = 0;
  let subthreadSelects = "";
  const subthreadJoins = [];
  for (let subthread of subthreadPermissionsToCheck) {
    const index = joinIndex++;
    subthreadSelects += `
      ,
      stm${index}.permissions AS subthread${subthread}_permissions,
      stm${index}.role AS subthread${subthread}_role
    `;
    const join = SQL`LEFT JOIN memberships `;
    join.append(`stm${index} ON stm${index}.`);
    join.append(SQL`thread = ${subthread} AND `);
    join.append(`stm${index}.user = m.user`);
    subthreadJoins.push(join);
  }

  const time = earliestFocusedTimeConsideredCurrent();
  const visibleExtractString = `$.${threadPermissions.VISIBLE}.value`;
  const query = SQL`
    SELECT m.user, m.thread, c.platform, c.device_token, c.versions,
      f.user AS focused_user
  `;
  query.append(subthreadSelects);
  query.append(SQL`
    FROM memberships m
    LEFT JOIN cookies c ON c.user = m.user AND c.device_token IS NOT NULL
    LEFT JOIN focused f ON f.user = m.user AND f.thread = m.thread
      AND f.time > ${time}
  `);
  appendSQLArray(query, subthreadJoins, SQL` `);
  query.append(SQL`
    WHERE (m.role != 0 OR f.user IS NOT NULL) AND
      JSON_EXTRACT(m.permissions, ${visibleExtractString}) IS TRUE AND
      m.thread IN (${[...threadsToMessageIndices.keys()]})
  `);

  const perUserInfo = new Map();
  const [ result ] = await dbQuery(query);
  for (let row of result) {
    const userID = row.user.toString();
    const threadID = row.thread.toString();
    const deviceToken = row.device_token;
    const focusedUser = !!row.focused_user;
    const { platform, versions } = row;
    let thisUserInfo = perUserInfo.get(userID);
    if (!thisUserInfo) {
      thisUserInfo = {
        devices: new Map(),
        threadIDs: new Set(),
        notFocusedThreadIDs: new Set(),
        subthreadsCanNotify: new Set(),
        subthreadsCanSetToUnread: new Set(),
      };
      perUserInfo.set(userID, thisUserInfo);
      // Subthread info will be the same for each subthread, so we only parse
      // it once
      for (let subthread of subthreadPermissionsToCheck) {
        const isSubthreadMember = !!row[`subthread${subthread}_role`];
        const permissions = row[`subthread${subthread}_permissions`];
        const canSeeSubthread =
          permissionLookup(permissions, threadPermissions.KNOW_OF);
        if (!canSeeSubthread) {
          continue;
        }
        thisUserInfo.subthreadsCanSetToUnread.add(subthread);
        // Only include the notification from the superthread if there is no
        // notification from the subthread
        if (
          !isSubthreadMember ||
          !permissionLookup(permissions, threadPermissions.VISIBLE)
        ) {
          thisUserInfo.subthreadsCanNotify.add(subthread);
        }
      }
    }
    if (deviceToken) {
      thisUserInfo.devices.set(deviceToken, {
        deviceType: platform,
        deviceToken,
        codeVersion: versions ? versions.codeVersion : null,
      });
    }
    thisUserInfo.threadIDs.add(threadID);
    if (!focusedUser) {
      thisUserInfo.notFocusedThreadIDs.add(threadID);
    }
  }

  const pushInfo = {}, setUnreadPairs = [], messageInfosPerUser = {};
  for (let pair of perUserInfo) {
    const [ userID, preUserPushInfo ] = pair;
    const { subthreadsCanSetToUnread, subthreadsCanNotify } = preUserPushInfo;
    const userPushInfo = {
      devices: [...preUserPushInfo.devices.values()],
      messageInfos: [],
    };
    const threadIDsToSetToUnread = new Set();
    for (let threadID of preUserPushInfo.notFocusedThreadIDs) {
      const messageIndices = threadsToMessageIndices.get(threadID);
      invariant(messageIndices, `indices should exist for thread ${threadID}`);
      for (let messageIndex of messageIndices) {
        const messageInfo = messageInfos[messageIndex];
        if (messageInfo.creatorID === userID) {
          continue;
        }
        if (
          messageInfo.type !== messageTypes.CREATE_SUB_THREAD ||
          subthreadsCanSetToUnread.has(messageInfo.childThreadID)
        ) {
          threadIDsToSetToUnread.add(threadID);
        }
        if (!messageTypeGeneratesNotifs(messageInfo.type)) {
          continue;
        }
        if (
          messageInfo.type !== messageTypes.CREATE_SUB_THREAD ||
          subthreadsCanNotify.has(messageInfo.childThreadID)
        ) {
          userPushInfo.messageInfos.push(messageInfo);
        }
      }
    }
    if (
      userPushInfo.devices.length > 0 &&
      userPushInfo.messageInfos.length > 0
    ) {
      pushInfo[userID] = userPushInfo;
    }
    for (let threadID of threadIDsToSetToUnread) {
      setUnreadPairs.push({ userID, threadID });
    }
    const userMessageInfos = [];
    for (let threadID of preUserPushInfo.threadIDs) {
      const messageIndices = threadsToMessageIndices.get(threadID);
      invariant(messageIndices, `indices should exist for thread ${threadID}`);
      for (let messageIndex of messageIndices) {
        const messageInfo = messageInfos[messageIndex];
        userMessageInfos.push(messageInfo);
      }
    }
    if (userMessageInfos.length > 0) {
      messageInfosPerUser[userID] = userMessageInfos;
    }
  }

  await Promise.all([
    updateUnreadStatus(setUnreadPairs),
    redisPublish(viewer, messageInfosPerUser),
  ]);

  await sendPushNotifs(pushInfo);
}

async function updateUnreadStatus(
  setUnreadPairs: $ReadOnlyArray<{| userID: string, threadID: string |}>,
) {
  if (setUnreadPairs.length === 0) {
    return;
  }
  const updateConditions = setUnreadPairs.map(
    pair => SQL`(user = ${pair.userID} AND thread = ${pair.threadID})`,
  );
  const updateQuery = SQL`
    UPDATE memberships
    SET unread = 1
    WHERE
  `;
  updateQuery.append(mergeOrConditions(updateConditions));

  const now = Date.now();
  await Promise.all([
    dbQuery(updateQuery),
    createUpdates(setUnreadPairs.map(pair => ({
      type: updateTypes.UPDATE_THREAD_READ_STATUS,
      userID: pair.userID,
      time: now,
      threadID: pair.threadID,
      unread: true,
    }))),
  ]);
}

async function redisPublish(
  viewer: Viewer,
  messageInfosPerUser: {[userID: string]: $ReadOnlyArray<RawMessageInfo>},
) {
  for (let userID in messageInfosPerUser) {
    if (userID === viewer.userID && viewer.hasSessionInfo) {
      continue;
    }
    const messageInfos = messageInfosPerUser[userID];
    publisher.sendMessage(
      { userID },
      {
        type: redisMessageTypes.NEW_MESSAGES,
        messages: messageInfos,
      },
    );
  }
  const viewerMessageInfos = messageInfosPerUser[viewer.userID];
  if (!viewerMessageInfos || !viewer.hasSessionInfo) {
    return;
  }
  const sessionIDs = await fetchOtherSessionsForViewer(viewer);
  for (let sessionID of sessionIDs) {
    publisher.sendMessage(
      { userID: viewer.userID, sessionID },
      {
        type: redisMessageTypes.NEW_MESSAGES,
        messages: viewerMessageInfos,
      },
    );
  }
}

export default createMessages;
<|endoftext|>


<commit_before>/*File: fileStorageService.js
 *
 * Copyright (c) 2014
 * Centre National d’Enseignement à Distance (Cned), Boulevard Nicephore Niepce, 86360 CHASSENEUIL-DU-POITOU, France
 * (direction-innovation@cned.fr)
 *
 * GNU Affero General Public License (AGPL) version 3.0 or later version
 *
 * This file is part of a program which is free software: you can
 * redistribute it and/or modify it under the terms of the
 * GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public
 * License along with this program.
 * If not, see <http://www.gnu.org/licenses/>.
 *
 */
'use strict';

var cnedApp = cnedApp;

cnedApp.service('fileStorageService', function($localForage, configuration, dropbox) {

    var self = this;

    /**
      * Recherche les fichiers sur dropbox, met à jour le cache si les fichiers ont été trouvés.
      * Retourne la liste des fichiers depuis le cache
      * @param token le token dropbox
      * @method searchAllFiles
      */
    this.searchAllFiles = function(token) {
        return dropbox.search('.html', token, configuration.DROPBOX_TYPE).then(self.updateFileListInStorage,function() {
          // en cas d'erreur lors de l'appel a dropbox on prend dans le cache
          return $localForage.getItem('listDocument');
        }).then($localForage.getItem('listDocument'));
    };

    /**
      * Recherche des fichiers correspondants à la requete sur dropbox
      * La requête de recherche
      * @param token le token dropbox
      * @method searchFilesInDropbox
      */
    this.searchFilesInDropbox = function(query, token) {
        return dropbox.search(query, token, configuration.DROPBOX_TYPE);
    };

    /**
      * Converti le format des fichiers dropbox en format de fichier interne
      * @param dropboxFiles les fichiers dropbox
      * @method transformDropboxFilesToStorageFiles
      */
    this.transformDropboxFilesToStorageFiles = function(dropboxFiles) {
      var files = [];
      for (var i = 0; i < dropboxFiles.length; i++) {
          var file = self.transformDropboxFileToStorageFile(dropboxFiles[i]);
          files.push(file);
      }
      return files;
    };

    /**
      * Converti le format du fichier dropbox en format de fichier interne
      * @param dropboxFiles les fichiers dropbox
      * @method transformDropboxFileToStorageFile
      */
    this.transformDropboxFileToStorageFile = function(dropboxFile) {
      var filepath = dropboxFile.path;
      var filenameStartIndex = filepath.indexOf('_')+1;
      var filenameEndIndex = filepath.lastIndexOf('_');
      var filename = filepath.substring(filenameStartIndex, filenameEndIndex);
      var dateModification = /((\d+)(-)(\d+)(-)(\d+))/i.exec(filepath)[0];
      var file = {
        filepath : filepath,
        filename : filename,
        dateModification : dateModification
      };
      return file;
    };

    /**
      * Recherche des fichiers dans le cache
      * @param query la requete de recherche
      * @method searchFilesInStorage
      */
    this.searchFilesInStorage = function(query) {
      return $localForage.getItem('listDocument').then(function(files) {
        var filesFound = [];
        for (var i = 0; i < files.length; i++) {
            var filepath = files[i].filepath;
            if(filepath.indexOf(query) !== -1) {
              filesFound.push(files[i]);
            }
        }
        return filesFound;
      });
    };

    /**
      * Recherche des fichiers dans le dropbox ou dans le cache si dropbox n'est pas accessible
      * @param query la requete de recherche
      * @param token le token dropbox
      * @method searchFiles
      */
    this.searchFiles = function(query, token) {
      return self.searchFilesInDropbox(query,token).then(function(data) {
        return self.transformDropboxFilesToStorageFiles(data);
      },
      // si dropbox n'est pas joignable on cherche dans le cache
      function() {
        return self.searchFilesInStorage(query);
      });
    };

    /**
      * Mets à jour la liste des fichiers dans le cache
      * @param dropboxfiles la liste des fichiers
      * @method updateFileListInStorage
      */
    this.updateFileListInStorage = function(dropboxfiles) {
      var filesList = self.transformDropboxFilesToStorageFiles(dropboxfiles);
      return $localForage.setItem('listDocument', filesList);
    };

    /**
      * Sauvegarde le contenu du fichier dans le cache
      * @param filename le nom du fichier
      * @param le contenu du fichier
      * @method saveFileInStorage
      */
    this.saveFileInStorage = function(filename, fileContent) {
      return $localForage.setItem('document.'+filename, fileContent);
    };

    /**
      * Supprime un fichier du cache
      * @param filename le nom du fichier
      * @method deleteFileInStorage
      */
    this.deleteFileInStorage = function(filename) {
      return $localForage.removeItem(filename);
    };

    /**
      * Recupere le contenu du fichier dans le cache
      * @param filename le nom du fichier
      * @method getFileInStorage
      */
    this.getFileInStorage = function(filename) {
      return $localForage.getItem('document.'+filename);
    };

    /**
     * Sauvegarde l'objet dans le cache
     * @param cssUrl l'objet URL
     * @method saveCSSInStorage
     */
    this.saveCSSInStorage = function(cssURL, id) {
        return $localForage.setItem('cssURL.'+id, cssURL);
    };

    /**
     * Sauvegarde l'objet dans le cache
     * @param cssUrl l'objet URL
     * @method getCSSInStorage
     */
    this.getCSSInStorage = function(id) {
        return $localForage.getItem('cssURL.'+id);
    };

    /**
      * Recupere le contenu du fichier dans dropbox
      * @param filepath le nom complet du fichier
      * @param le token dropbox
      * @method getDropboxFileContent
      */
    this.getDropboxFileContent = function(filepath, token) {
      return dropbox.download(filepath, token, configuration.DROPBOX_TYPE);
    };

    /**
      * Recupere le contenu du fichier sur dropbox si possible. sinon le recupere dans le cache
      * @param filename le nom du fichier
      * @param le token dropbox
      * @method getFile
      */
    this.getFile = function(filename, token) {
      return self.searchFilesInDropbox(filename,token).then(function(files) {
          return self.getDropboxFileContent(files[0].path, token);
        },
        // si dropbox n'est pas joignable, on récupère le fichier dans le cache
        function() {
          return self.getFileInStorage(filename);
        }).then(function(filecontent) {
          return self.saveFileInStorage(filename, filecontent);
        }).then(function() {
        return self.getFileInStorage(filename);
      });
    };

    /**
      * Renomme le du fichier sur dropbox si possible et dans le cache
      * @param oldFilename l'ancien nom du fichier
      * @param newFilename le nouveau nom du fichier
      * @param le token dropbox
      * @method renameFile
      */
    this.renameFile = function(oldFilename, newFilename, token) {
      return dropbox.rename(oldFilename, newFilename, token, configuration.DROPBOX_TYPE).then(function(){
        self.getFileInStorage(oldFilename).then(function(filecontent) {
          self.saveFileInStorage(newFilename, filecontent).then(function () {
            return self.deleteFileInStorage(oldFilename);
          });
        });
      });
    };

    /**
      * Supprime le fichier sur dropbox et dans le cache
      * @param filename le nom du fichier
      * @param le token dropbox
      * @method deleteFile
      */
    this.deleteFile = function(filename, token) {
      return dropbox.delete(filename, token, configuration.DROPBOX_TYPE).then(function() {
        return self.deleteFileInStorage(filename);
      });
    };

    /**
      * Sauvegarde le contenu du fichier sur dropbox et dans le cache
      * @param filename le nom du fichier
      * @param filecontent le contenu du fichier
      * @param le token dropbox
      * @method saveFile
      */
    this.saveFile = function(filename, filecontent, token) {
      return dropbox.upload(filename, filecontent, token, configuration.DROPBOX_TYPE).then(function(dropboxFile) {
        var storageFile = self.transformDropboxFileToStorageFile(dropboxFile);
        return self.saveFileInStorage(storageFile.filename, filecontent).then(function() {
          return storageFile;
        });
      });
    };

    /**
      * Sauvegarde le contenu du fichier temporaire
      * @param filecontent le contenu du fichier
      * @method saveTempFile
      */
    this.saveTempFile = function(filecontent) {
      return self.saveFileInStorage('temp',filecontent);
    };

    /**
      * Recupere le contenu du fichier temporaire
      * @method getTempFile
      */
    this.getTempFile = function() {
      return self.getFileInStorage('temp');
    };

    /**
      * Partage le fichier sur dropbox et retourne l'url du partage
      * @method shareFile
      */
    this.shareFile = function(filename, token) {
      return dropbox.shareLink(filename, token, configuration.DROPBOX_TYPE).then(function(result) {
        return result.url;
      });
    };
});
<commit_msg>US2416 - Gestion des fichiers non conformes sur dropbox<commit_after>/*File: fileStorageService.js
 *
 * Copyright (c) 2014
 * Centre National d’Enseignement à Distance (Cned), Boulevard Nicephore Niepce, 86360 CHASSENEUIL-DU-POITOU, France
 * (direction-innovation@cned.fr)
 *
 * GNU Affero General Public License (AGPL) version 3.0 or later version
 *
 * This file is part of a program which is free software: you can
 * redistribute it and/or modify it under the terms of the
 * GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public
 * License along with this program.
 * If not, see <http://www.gnu.org/licenses/>.
 *
 */
'use strict';

var cnedApp = cnedApp;

cnedApp.service('fileStorageService', function($localForage, configuration, dropbox) {

    var self = this;

    /**
      * Recherche les fichiers sur dropbox, met à jour le cache si les fichiers ont été trouvés.
      * Retourne la liste des fichiers depuis le cache
      * @param token le token dropbox
      * @method searchAllFiles
      */
    this.searchAllFiles = function(token) {
        return dropbox.search('.html', token, configuration.DROPBOX_TYPE).then(self.updateFileListInStorage,function() {
          // en cas d'erreur lors de l'appel a dropbox on prend dans le cache
          return $localForage.getItem('listDocument');
        }).then($localForage.getItem('listDocument'));
    };

    /**
      * Recherche des fichiers correspondants à la requete sur dropbox
      * La requête de recherche
      * @param token le token dropbox
      * @method searchFilesInDropbox
      */
    this.searchFilesInDropbox = function(query, token) {
        return dropbox.search(query, token, configuration.DROPBOX_TYPE);
    };

    /**
      * Converti le format des fichiers dropbox en format de fichier interne
      * @param dropboxFiles les fichiers dropbox
      * @method transformDropboxFilesToStorageFiles
      */
    this.transformDropboxFilesToStorageFiles = function(dropboxFiles) {
      var files = [];
      for (var i = 0; i < dropboxFiles.length; i++) {
          var file = self.transformDropboxFileToStorageFile(dropboxFiles[i]);
          if(file !== null) {
        	  files.push(file);
          }
      }
      return files;
    };

    /**
      * Converti le format du fichier dropbox en format de fichier interne
      * @param dropboxFiles les fichiers dropbox
      * @method transformDropboxFileToStorageFile
      */
    this.transformDropboxFileToStorageFile = function(dropboxFile) {
      var filepath = dropboxFile.path;
      var filenameStartIndex = filepath.indexOf('_')+1;
      var filenameEndIndex = filepath.lastIndexOf('_');
      var filename = filepath.substring(filenameStartIndex, filenameEndIndex);
      var dateModificationRegExpResult = /((\d+)(-)(\d+)(-)(\d+))/i.exec(filepath);
      var file = null;
      if(dateModificationRegExpResult) {
    	  var dateModification = /((\d+)(-)(\d+)(-)(\d+))/i.exec(filepath)[0];
    	  file = {
    		        filepath : filepath,
    		        filename : filename,
    		        dateModification : dateModification
    		      };
      }
      return file;
    };

    /**
      * Recherche des fichiers dans le cache
      * @param query la requete de recherche
      * @method searchFilesInStorage
      */
    this.searchFilesInStorage = function(query) {
      return $localForage.getItem('listDocument').then(function(files) {
        var filesFound = [];
        for (var i = 0; i < files.length; i++) {
            var filepath = files[i].filepath;
            if(filepath.indexOf(query) !== -1) {
              filesFound.push(files[i]);
            }
        }
        return filesFound;
      });
    };

    /**
      * Recherche des fichiers dans le dropbox ou dans le cache si dropbox n'est pas accessible
      * @param query la requete de recherche
      * @param token le token dropbox
      * @method searchFiles
      */
    this.searchFiles = function(query, token) {
      return self.searchFilesInDropbox(query,token).then(function(data) {
        return self.transformDropboxFilesToStorageFiles(data);
      },
      // si dropbox n'est pas joignable on cherche dans le cache
      function() {
        return self.searchFilesInStorage(query);
      });
    };

    /**
      * Mets à jour la liste des fichiers dans le cache
      * @param dropboxfiles la liste des fichiers
      * @method updateFileListInStorage
      */
    this.updateFileListInStorage = function(dropboxfiles) {
      var filesList = self.transformDropboxFilesToStorageFiles(dropboxfiles);
      return $localForage.setItem('listDocument', filesList);
    };

    /**
      * Sauvegarde le contenu du fichier dans le cache
      * @param filename le nom du fichier
      * @param le contenu du fichier
      * @method saveFileInStorage
      */
    this.saveFileInStorage = function(filename, fileContent) {
      return $localForage.setItem('document.'+filename, fileContent);
    };

    /**
      * Supprime un fichier du cache
      * @param filename le nom du fichier
      * @method deleteFileInStorage
      */
    this.deleteFileInStorage = function(filename) {
      return $localForage.removeItem(filename);
    };

    /**
      * Recupere le contenu du fichier dans le cache
      * @param filename le nom du fichier
      * @method getFileInStorage
      */
    this.getFileInStorage = function(filename) {
      return $localForage.getItem('document.'+filename);
    };

    /**
     * Sauvegarde l'objet dans le cache
     * @param cssUrl l'objet URL
     * @method saveCSSInStorage
     */
    this.saveCSSInStorage = function(cssURL, id) {
        return $localForage.setItem('cssURL.'+id, cssURL);
    };

    /**
     * Sauvegarde l'objet dans le cache
     * @param cssUrl l'objet URL
     * @method getCSSInStorage
     */
    this.getCSSInStorage = function(id) {
        return $localForage.getItem('cssURL.'+id);
    };

    /**
      * Recupere le contenu du fichier dans dropbox
      * @param filepath le nom complet du fichier
      * @param le token dropbox
      * @method getDropboxFileContent
      */
    this.getDropboxFileContent = function(filepath, token) {
      return dropbox.download(filepath, token, configuration.DROPBOX_TYPE);
    };

    /**
      * Recupere le contenu du fichier sur dropbox si possible. sinon le recupere dans le cache
      * @param filename le nom du fichier
      * @param le token dropbox
      * @method getFile
      */
    this.getFile = function(filename, token) {
      return self.searchFilesInDropbox(filename,token).then(function(files) {
          return self.getDropboxFileContent(files[0].path, token);
        },
        // si dropbox n'est pas joignable, on récupère le fichier dans le cache
        function() {
          return self.getFileInStorage(filename);
        }).then(function(filecontent) {
          return self.saveFileInStorage(filename, filecontent);
        }).then(function() {
        return self.getFileInStorage(filename);
      });
    };

    /**
      * Renomme le du fichier sur dropbox si possible et dans le cache
      * @param oldFilename l'ancien nom du fichier
      * @param newFilename le nouveau nom du fichier
      * @param le token dropbox
      * @method renameFile
      */
    this.renameFile = function(oldFilename, newFilename, token) {
      return dropbox.rename(oldFilename, newFilename, token, configuration.DROPBOX_TYPE).then(function(){
        self.getFileInStorage(oldFilename).then(function(filecontent) {
          self.saveFileInStorage(newFilename, filecontent).then(function () {
            return self.deleteFileInStorage(oldFilename);
          });
        });
      });
    };

    /**
      * Supprime le fichier sur dropbox et dans le cache
      * @param filename le nom du fichier
      * @param le token dropbox
      * @method deleteFile
      */
    this.deleteFile = function(filename, token) {
      return dropbox.delete(filename, token, configuration.DROPBOX_TYPE).then(function() {
        return self.deleteFileInStorage(filename);
      });
    };

    /**
      * Sauvegarde le contenu du fichier sur dropbox et dans le cache
      * @param filename le nom du fichier
      * @param filecontent le contenu du fichier
      * @param le token dropbox
      * @method saveFile
      */
    this.saveFile = function(filename, filecontent, token) {
      return dropbox.upload(filename, filecontent, token, configuration.DROPBOX_TYPE).then(function(dropboxFile) {
        var storageFile = self.transformDropboxFileToStorageFile(dropboxFile);
        return self.saveFileInStorage(storageFile.filename, filecontent).then(function() {
          return storageFile;
        });
      });
    };

    /**
      * Sauvegarde le contenu du fichier temporaire
      * @param filecontent le contenu du fichier
      * @method saveTempFile
      */
    this.saveTempFile = function(filecontent) {
      return self.saveFileInStorage('temp',filecontent);
    };

    /**
      * Recupere le contenu du fichier temporaire
      * @method getTempFile
      */
    this.getTempFile = function() {
      return self.getFileInStorage('temp');
    };

    /**
      * Partage le fichier sur dropbox et retourne l'url du partage
      * @method shareFile
      */
    this.shareFile = function(filename, token) {
      return dropbox.shareLink(filename, token, configuration.DROPBOX_TYPE).then(function(result) {
        return result.url;
      });
    };
});
<|endoftext|>


<commit_before>import Ember from 'ember';
import DS from 'ember-data';

const { computed, inject, RSVP, isEmpty } = Ember;
const { service } = inject;
const { PromiseArray } = DS;

export default Ember.Component.extend({
  store: service(),
  i18n: service(),
  currentUser: service(),
  flashMessages: service(),
  classNames: ['form-container', 'detail-view', 'new-myreport', 'mesh-manager'],
  title: null,
  currentSubject: 'course',
  currentPrepositionalObject: null,
  currentPrepositionalObjectId: null,
  isAssociatedTo: false,
  subjectList: computed('i18n.locale', function(){
    let list = [
      {value: 'course', label: this.get('i18n').t('general.courses')},
      {value: 'session', label: this.get('i18n').t('general.sessions')},
      {value: 'program', label: this.get('i18n').t('general.programs')},
      {value: 'program year', label: this.get('i18n').t('general.programYears')},
      {value: 'instructor', label: this.get('i18n').t('general.instructors')},
      {value: 'instructor group', label: this.get('i18n').t('general.instructorGroups')},
      {value: 'learning material', label: this.get('i18n').t('general.learningMaterials')},
      {value: 'competency', label: this.get('i18n').t('general.competencies')},
      {value: 'topic', label: this.get('i18n').t('general.topics')},
      {value: 'mesh term', label: this.get('i18n').t('general.meshTerms')},
      {value: 'session type', label: this.get('i18n').t('general.sessionTypes')},
    ];

    return list;
  }),
  prepositionalObjectList: computed('i18n.locale', 'currentSubject', function(){
    let list = [
      {value: 'course', label: this.get('i18n').t('general.courses'), subjects: ['session', 'program', 'program year', 'instructor', 'instructor group', 'learning material', 'competency', 'topic', 'mesh term']},
      {value: 'session', label: this.get('i18n').t('general.sessions'), subjects: ['course', 'program', 'program year', 'instructor', 'instructor group', 'learning material', 'competency', 'topic', 'mesh term', 'session type']},
      {value: 'program', label: this.get('i18n').t('general.programs'), subjects: ['course', 'session', 'topic']},
      {value: 'program year', label: this.get('i18n').t('general.programYears'), subjects: ['course', 'session', 'topic']},
      {value: 'instructor', label: this.get('i18n').t('general.instructors'), subjects: ['course', 'session', 'instructor group', 'learning material', 'topic', 'session type']},
      {value: 'instructor group', label: this.get('i18n').t('general.instructorGroups'), subjects: ['course', 'session', 'instructor', 'learning material', 'topic', 'session type']},
      {value: 'learning material', label: this.get('i18n').t('general.learningMaterials'), subjects: ['course', 'session', 'instructor', 'instructor group', 'topic', 'mesh term', 'sessiontype']},
      {value: 'competency', label: this.get('i18n').t('general.competencies'), subjects: ['course', 'session', 'topic', 'session type']},
      {value: 'topic', label: this.get('i18n').t('general.topics'), subjects: ['course', 'session', 'program', 'program year', 'instructor', 'instructor group', 'learning material', 'competency', 'mesh term']},
      {value: 'mesh term', label: this.get('i18n').t('general.meshTerms'), subjects: ['course', 'session', 'learning material', 'topic', 'session type']},
      {value: 'session type', label: this.get('i18n').t('general.sessionTypes'), subjects: ['session', 'instructor', 'instructor group', 'learning material', 'competency', 'topic', 'mesh term']},
    ];

    const subject = this.get('currentSubject');

    return list.filter(item =>item.subjects.contains(subject));
  }),
  prepositionalObjectIdList: computed('currentPrepositionalObject', 'isAssociatedTo', function(){
    const type = this.get('currentPrepositionalObject');
    if(isEmpty(type) || type === 'instructor' || !this.get('isAssociatedTo')){
      return [];
    }
    let defer = RSVP.defer();
    let model = type.dasherize();
    const store = this.get('store');
    let query = {
      limit: 1000
    };
    store.query(model, query).then(objects => {
      if(type === 'program year'){
        RSVP.all(objects.mapBy('cohort')).then(cohorts => {
          let promises = [];
          let values = [];
          cohorts.forEach(cohort => {
            promises.pushObject(cohort.get('programYear').then(programYear => {
              return programYear.get('program').then(program => {
                return program.get('school').then(school => {
                  let value = programYear.get('id');
                  let label = school.get('title') + ' ' +
                    program.get('title') + ' ' +
                    cohort.get('displayTitle');
                  values.pushObject({value,label});
                });
              });
            }));
            RSVP.all(promises).then(()=> {
              defer.resolve(values.sortBy('label'));
            });
          });
        });
      } else {
        let label = type === 'mesh term'?'name':'title';
        let values = objects.map(object => {
          return {
            value: object.get('id'),
            label: object.get(label)
          };
        }).sortBy('label');

        defer.resolve(values);
      }

    });
    return PromiseArray.create({
      promise: defer.promise
    });
  }),
  currentSubjectLabel: computed('currentSubject', 'subjectList.[]', function(){
    const currentSubjectValue = this.get('currentSubject');
    let currentSubject = this.get('subjectList').find(subject => {
      return subject.value === currentSubjectValue;
    });

    return currentSubject.label;
  }),
  selectedUser: computed('currentPrepositionalObject', 'currentPrepositionalObjectId', function(){
    if(
      this.get('currentPrepositionalObject') === 'instructor' &&
      this.get('currentPrepositionalObjectId')
    ){
      return this.get('store').peekRecord('user', this.get('currentPrepositionalObjectId'));
    } else {
      return null;
    }
  }),
  selectedMeshTerm: computed('currentPrepositionalObject', 'currentPrepositionalObjectId', function(){
    if(
      this.get('currentPrepositionalObject') === 'mesh term' &&
      this.get('currentPrepositionalObjectId')
    ){
      return this.get('store').peekRecord('mesh-descriptor', this.get('currentPrepositionalObjectId'));
    } else {
      return null;
    }
  }),
  actions: {
    changeSubject(subject){
      this.set('currentSubject', subject);
    },
    changePrepositionalObject(object){
      this.set('currentPrepositionalObject', object);
      this.set('currentPrepositionalObjectId', null);
    },
    changePrepositionalObjectId(id){
      this.set('currentPrepositionalObjectId', id);
    },
    toggleIsAssciatedTo(){
      this.set('isAssociatedTo', !this.get('isAssociatedTo'));
    },
    chooseInstructor(user){
      this.set('currentPrepositionalObjectId', user.get('id'));
    },
    chooseMeshTerm(term){
      this.set('currentPrepositionalObjectId', term.get('id'));
    },
    closeEditor(){
      this.sendAction('close');
    },
    save(){
      const flashMessages = this.get('flashMessages');
      const store = this.get('store');
      let object = this.get('currentPrepositionalObject');
      if(
        object &&
        !this.get('currentPrepositionalObjectId')
      ) {
        if(object === 'instructor'){
          flashMessages.alert('dashboard.reportMissingInstructor');
        }
        if(object === 'mesh term'){
          flashMessages.alert('dashboard.reportMissingMeshTerm');
        }
        return;
      }
      this.get('currentUser.model').then(user => {

        let title = this.get('title');
        let subject = this.get('currentSubject');
        let prepositionalObject = this.get('currentPrepositionalObject');
        let prepositionalObjectTableRowId = this.get('currentPrepositionalObjectId');
        let report = store.createRecord('report', {
          title,
          user,
          subject,
          prepositionalObject,
          prepositionalObjectTableRowId
        });

        report.save().then(() => {
          this.sendAction('close');
        });
      });
    }
  }
});
<commit_msg>Remove ProgramYear as an object for reports<commit_after>import Ember from 'ember';
import DS from 'ember-data';

const { computed, inject, RSVP, isEmpty } = Ember;
const { service } = inject;
const { PromiseArray } = DS;

export default Ember.Component.extend({
  store: service(),
  i18n: service(),
  currentUser: service(),
  flashMessages: service(),
  classNames: ['form-container', 'detail-view', 'new-myreport', 'mesh-manager'],
  title: null,
  currentSubject: 'course',
  currentPrepositionalObject: null,
  currentPrepositionalObjectId: null,
  isAssociatedTo: false,
  subjectList: computed('i18n.locale', function(){
    let list = [
      {value: 'course', label: this.get('i18n').t('general.courses')},
      {value: 'session', label: this.get('i18n').t('general.sessions')},
      {value: 'program', label: this.get('i18n').t('general.programs')},
      {value: 'program year', label: this.get('i18n').t('general.programYears')},
      {value: 'instructor', label: this.get('i18n').t('general.instructors')},
      {value: 'instructor group', label: this.get('i18n').t('general.instructorGroups')},
      {value: 'learning material', label: this.get('i18n').t('general.learningMaterials')},
      {value: 'competency', label: this.get('i18n').t('general.competencies')},
      {value: 'topic', label: this.get('i18n').t('general.topics')},
      {value: 'mesh term', label: this.get('i18n').t('general.meshTerms')},
      {value: 'session type', label: this.get('i18n').t('general.sessionTypes')},
    ];

    return list;
  }),
  prepositionalObjectList: computed('i18n.locale', 'currentSubject', function(){
    let list = [
      {value: 'course', label: this.get('i18n').t('general.courses'), subjects: ['session', 'program', 'program year', 'instructor', 'instructor group', 'learning material', 'competency', 'topic', 'mesh term']},
      {value: 'session', label: this.get('i18n').t('general.sessions'), subjects: ['course', 'program', 'program year', 'instructor', 'instructor group', 'learning material', 'competency', 'topic', 'mesh term', 'session type']},
      {value: 'program', label: this.get('i18n').t('general.programs'), subjects: ['course', 'session', 'topic']},
      {value: 'instructor', label: this.get('i18n').t('general.instructors'), subjects: ['course', 'session', 'instructor group', 'learning material', 'topic', 'session type']},
      {value: 'instructor group', label: this.get('i18n').t('general.instructorGroups'), subjects: ['course', 'session', 'instructor', 'learning material', 'topic', 'session type']},
      {value: 'learning material', label: this.get('i18n').t('general.learningMaterials'), subjects: ['course', 'session', 'instructor', 'instructor group', 'topic', 'mesh term', 'sessiontype']},
      {value: 'competency', label: this.get('i18n').t('general.competencies'), subjects: ['course', 'session', 'topic', 'session type']},
      {value: 'topic', label: this.get('i18n').t('general.topics'), subjects: ['course', 'session', 'program', 'program year', 'instructor', 'instructor group', 'learning material', 'competency', 'mesh term']},
      {value: 'mesh term', label: this.get('i18n').t('general.meshTerms'), subjects: ['course', 'session', 'learning material', 'topic', 'session type']},
      {value: 'session type', label: this.get('i18n').t('general.sessionTypes'), subjects: ['session', 'instructor', 'instructor group', 'learning material', 'competency', 'topic', 'mesh term']},
    ];

    const subject = this.get('currentSubject');

    return list.filter(item =>item.subjects.contains(subject));
  }),
  prepositionalObjectIdList: computed('currentPrepositionalObject', 'isAssociatedTo', function(){
    const type = this.get('currentPrepositionalObject');
    if(isEmpty(type) || type === 'instructor' || !this.get('isAssociatedTo')){
      return [];
    }
    let defer = RSVP.defer();
    let model = type.dasherize();
    const store = this.get('store');
    let query = {
      limit: 1000
    };
    store.query(model, query).then(objects => {
      let label = type === 'mesh term'?'name':'title';
      let values = objects.map(object => {
        return {
          value: object.get('id'),
          label: object.get(label)
        };
      }).sortBy('label');

      defer.resolve(values);
    });
    return PromiseArray.create({
      promise: defer.promise
    });
  }),
  currentSubjectLabel: computed('currentSubject', 'subjectList.[]', function(){
    const currentSubjectValue = this.get('currentSubject');
    let currentSubject = this.get('subjectList').find(subject => {
      return subject.value === currentSubjectValue;
    });

    return currentSubject.label;
  }),
  selectedUser: computed('currentPrepositionalObject', 'currentPrepositionalObjectId', function(){
    if(
      this.get('currentPrepositionalObject') === 'instructor' &&
      this.get('currentPrepositionalObjectId')
    ){
      return this.get('store').peekRecord('user', this.get('currentPrepositionalObjectId'));
    } else {
      return null;
    }
  }),
  selectedMeshTerm: computed('currentPrepositionalObject', 'currentPrepositionalObjectId', function(){
    if(
      this.get('currentPrepositionalObject') === 'mesh term' &&
      this.get('currentPrepositionalObjectId')
    ){
      return this.get('store').peekRecord('mesh-descriptor', this.get('currentPrepositionalObjectId'));
    } else {
      return null;
    }
  }),
  actions: {
    changeSubject(subject){
      this.set('currentSubject', subject);
    },
    changePrepositionalObject(object){
      this.set('currentPrepositionalObject', object);
      this.set('currentPrepositionalObjectId', null);
    },
    changePrepositionalObjectId(id){
      this.set('currentPrepositionalObjectId', id);
    },
    toggleIsAssciatedTo(){
      this.set('isAssociatedTo', !this.get('isAssociatedTo'));
    },
    chooseInstructor(user){
      this.set('currentPrepositionalObjectId', user.get('id'));
    },
    chooseMeshTerm(term){
      this.set('currentPrepositionalObjectId', term.get('id'));
    },
    closeEditor(){
      this.sendAction('close');
    },
    save(){
      const flashMessages = this.get('flashMessages');
      const store = this.get('store');
      let object = this.get('currentPrepositionalObject');
      if(
        object &&
        !this.get('currentPrepositionalObjectId')
      ) {
        if(object === 'instructor'){
          flashMessages.alert('dashboard.reportMissingInstructor');
        }
        if(object === 'mesh term'){
          flashMessages.alert('dashboard.reportMissingMeshTerm');
        }
        return;
      }
      this.get('currentUser.model').then(user => {

        let title = this.get('title');
        let subject = this.get('currentSubject');
        let prepositionalObject = this.get('currentPrepositionalObject');
        let prepositionalObjectTableRowId = this.get('currentPrepositionalObjectId');
        let report = store.createRecord('report', {
          title,
          user,
          subject,
          prepositionalObject,
          prepositionalObjectTableRowId
        });

        report.save().then(() => {
          this.sendAction('close');
        });
      });
    }
  }
});
<|endoftext|>


<commit_before>import { getDuration, approximateTimeFormat, dateFormat, daysBetween } from './timeDateUtils';

const NOW = Date.now();
const ONE_SECOND = 1000;
const ONE_MINUTE = 60 * 1000;
const ONE_HOUR = 3600 * 1000;
const ONE_DAY = 24 * 3600 * 1000;

describe('getDuration', () => {
  test('should return sting', () => {
    expect(typeof getDuration(NOW, NOW + ONE_HOUR)).toBe('string');
  });

  test('should return zero seconds', () => {
    expect(getDuration(NOW, NOW)).toBe('0s');
  });

  test('should contain seconds', () => {
    expect(getDuration(NOW, NOW + ONE_SECOND * 20)).toMatch(/[0-9]+s+/);
  });
  test('should contain minutes', () => {
    expect(getDuration(NOW, NOW + ONE_MINUTE * 20)).toMatch(/[0-9]+m$/);
  });
  test('should contain hours', () => {
    expect(getDuration(NOW, NOW + ONE_HOUR * 20)).toMatch(/[0-9]+h$/);
  });
  test('should contain days', () => {
    expect(getDuration(NOW, NOW + ONE_DAY * 20)).toMatch(/[0-9]+d$/);
  });
  test('should contain days, hours and minutes', () => {
    expect(getDuration(NOW, NOW + (ONE_DAY + ONE_HOUR + ONE_MINUTE + ONE_SECOND) * 20)).toMatch(
      /[0-9]+d [0-9]+h [0-9]+m$/,
    );
  });
  test('should contain minutes and seconds', () => {
    expect(getDuration(NOW, NOW + (ONE_MINUTE + ONE_SECOND) * 20)).toMatch(/[0-9]+m [0-9]+s$/);
  });
});

describe('approximateTimeFormat', () => {
  test('should return sting', () => {
    expect(typeof approximateTimeFormat(NOW + ONE_HOUR)).toBe('string');
  });

  test('should return zero seconds', () => {
    expect(approximateTimeFormat(0)).toBe('0s');
  });

  test('should contain seconds', () => {
    expect(approximateTimeFormat(ONE_SECOND / 1000 * 20)).toMatch(/[0-9]+s+/);
  });
  test('should contain minutes', () => {
    expect(approximateTimeFormat(ONE_MINUTE / 1000 * 20)).toMatch(/[0-9]+m$/);
  });
  test('should contain hours', () => {
    expect(approximateTimeFormat(ONE_HOUR / 1000 * 20)).toMatch(/[0-9]+h$/);
  });
  test('should contain days', () => {
    expect(approximateTimeFormat(ONE_DAY / 1000 * 20)).toMatch(/[0-9]+d$/);
  });
  test('should contain days, hours and minutes', () => {
    expect(
      approximateTimeFormat((ONE_DAY + ONE_HOUR + ONE_MINUTE + ONE_SECOND) / 1000 * 20),
    ).toMatch(/[0-9]+d [0-9]+h [0-9]+m$/);
  });
});

describe('dateFormat', () => {
  test('should return sting', () => {
    expect(typeof dateFormat(0)).toBe('string');
  });
  test('should match match date pattern "@@@@-@@-@@ @@:@@:@@"', () => {
    expect(dateFormat(0)).toMatch(/^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$/);
  });
  test('should contain UTC', () => {
    expect(dateFormat(0, true)).toMatch(/UTC/);
  });
});

describe('daysBetween', () => {
  test('should return number', () => {
    expect(typeof daysBetween(new Date(), new Date())).toBe('number');
  });
  test('should return zero for equal Dates', () => {
    expect(daysBetween(new Date(), new Date())).toEqual(0);
    expect(daysBetween(new Date(2018, 0, 1), new Date(2018, 0, 1))).toEqual(0);
  });
  test('should return 365 for dates with a year difference', () => {
    expect(daysBetween(new Date(2017, 0, 1), new Date(2018, 0, 1))).toEqual(365);
  });
});
<commit_msg>Test improvements<commit_after>import { getDuration, approximateTimeFormat, dateFormat, daysBetween } from './timeDateUtils';

const NOW = Date.now();
const ONE_SECOND = 1000;
const ONE_MINUTE = 60 * 1000;
const ONE_HOUR = 3600 * 1000;
const ONE_DAY = 24 * 3600 * 1000;

describe('getDuration', () => {
  test('should return string', () => {
    expect(typeof getDuration(NOW, NOW + ONE_HOUR)).toBe('string');
  });

  test('should return "0s" in case of the same start and end', () => {
    expect(getDuration(NOW, NOW)).toBe('0s');
  });

  test('should return "20s" in case of the time difference between start and end is 20 seconds', () => {
    expect(getDuration(NOW, NOW + ONE_SECOND * 20)).toEqual('20s');
  });
  test('should return "20m" in case of the time difference between start and end is 20 minutes', () => {
    expect(getDuration(NOW, NOW + ONE_MINUTE * 20)).toEqual('20m');
  });
  test('should return "20h" in case of the time difference between start and end is 20 hours', () => {
    expect(getDuration(NOW, NOW + ONE_HOUR * 20)).toEqual('20h');
  });
  test('should return "20d" in case of the time difference between start and end is 20 days', () => {
    expect(getDuration(NOW, NOW + ONE_DAY * 20)).toEqual('20d');
  });
  test('should return "20d 20h 20m" in case of the time difference between start and end is 20 days, 20 hours 20 minutes and 20 seconds', () => {
    expect(getDuration(NOW, NOW + (ONE_DAY + ONE_HOUR + ONE_MINUTE + ONE_SECOND) * 20)).toEqual(
      '20d 20h 20m',
    );
  });
  test('should return "20m 20s" in case of the time difference between start and end is 20 minutes and 20 seconds', () => {
    expect(getDuration(NOW, NOW + (ONE_MINUTE + ONE_SECOND) * 20)).toEqual('20m 20s');
  });
});

describe('approximateTimeFormat', () => {
  test('should return sting', () => {
    expect(typeof approximateTimeFormat(NOW + ONE_HOUR)).toBe('string');
  });

  test('should return "0s" in case of 0 timestamp (in seconds) is passed as argument', () => {
    expect(approximateTimeFormat(0)).toEqual('0s');
  });

  test('should return "20s" in case of the 20 seconds timestamp (in seconds) is passed as argument', () => {
    expect(approximateTimeFormat(ONE_SECOND / 1000 * 20)).toEqual('20s');
  });
  test('should return "20m" in case of the 20 minutes timestamp (in seconds) is passed as argument', () => {
    expect(approximateTimeFormat(ONE_MINUTE / 1000 * 20)).toEqual('20m');
  });
  test('should return "20h" in case of the 20 hours timestamp (in seconds) is passed as argument', () => {
    expect(approximateTimeFormat(ONE_HOUR / 1000 * 20)).toEqual('20h');
  });
  test('should return "20d" in case of the 20 days timestamp (in seconds) is passed as argument', () => {
    expect(approximateTimeFormat(ONE_DAY / 1000 * 20)).toEqual('20d');
  });
  test('should return "20d 20h 20m" in case of the 20 days, 20 hours 20 minutes and 20 seconds timestamp (in seconds) is passed as argument', () => {
    expect(
      approximateTimeFormat((ONE_DAY + ONE_HOUR + ONE_MINUTE + ONE_SECOND) / 1000 * 20),
    ).toEqual('20d 20h 20m');
  });
});

describe('dateFormat', () => {
  test('should return sting', () => {
    expect(typeof dateFormat(0)).toBe('string');
  });
  test('should match match date pattern "@@@@-@@-@@ @@:@@:@@"', () => {
    expect(dateFormat(0)).toMatch(/^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$/);
  });
  test('should contain UTC offset after date in case second argument is true.', () => {
    expect(dateFormat(0, true)).toMatch(
      /^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2} UTC[+-]{0,1}[0-9]{1,2}$/,
    );
  });
});

describe('daysBetween', () => {
  test('should return number', () => {
    expect(typeof daysBetween(new Date(), new Date())).toBe('number');
  });
  test('should return 0 for equal Dates', () => {
    expect(daysBetween(new Date(), new Date())).toEqual(0);
    expect(daysBetween(new Date(2018, 0, 1), new Date(2018, 0, 1))).toEqual(0);
  });
  test('should return 365 for dates with a year difference', () => {
    expect(daysBetween(new Date(2017, 0, 1), new Date(2018, 0, 1))).toEqual(365);
  });
});
<|endoftext|>


<commit_before>/*
* DependencyChecker.js
* Written by Michael Albinson 11/19/16
*
* Checks that all required dependencies have been installed.
 */

"use strict";

var log = require('../log');

const dependencies = ['mysql', 'express', 'path', 'fs', 'cookie-parser', 'body-parser', 'nodemailer', 'nodemailer-smtp-transport'];

exports.checkDependencies = function() {
	log.info("~~~~~~~~~~~~~~~~Module Check~~~~~~~~~~~~~~~~~");
	var modulesYouNeed = [];
	for (var i in dependencies) {
		if (dependencies.hasOwnProperty(i)) {
            try {
                require(dependencies[i]);
                log.info("Cool, looks like the module " + dependencies[i] + " is already installed!")
            } catch (e) {
                log.warn("Uh oh, looks like you're missing the module " + dependencies[i]);
                modulesYouNeed.push(dependencies[i]);
            }
        }
	}

	logSummary(dependencies, modulesYouNeed);
	log.info("~~~~~~~~~~~~~~~End Module Check~~~~~~~~~~~~~~~~\n\n");
};

function logSummary(dependencyList, modulesNotInstalled) {
	log.info("~~~~~~~~~~~~~~~~~~~~Summary~~~~~~~~~~~~~~~~~~~");
	log.info((dependencyList.length - modulesNotInstalled.length) + "/" +dependencyList.length + " modules installed");

	if (modulesNotInstalled.length == 0) {
		log.info("You have all the modules necesary to run this service!");
		return true;
	}

	log.warn("You still need the module(s):\n");
	for(var mods in modulesNotInstalled) {
		if (modulesNotInstalled.hasOwnProperty(mods))
			log.warn(modulesNotInstalled[mods] + "\n");
	}

	throw new Error("You can't run this service until all the modules are installed, check the previous module logs\n");
}<commit_msg>Added new dependencies for the service<commit_after>/*
* DependencyChecker.js
* Written by Michael Albinson 11/19/16
*
* Checks that all required dependencies have been installed.
 */

"use strict";

var log = require('../log');

const dependencies = ['mysql', 'express', 'path', 'fs', 'cookie-parser', 'body-parser', 'nodemailer', 'nodemailer-smtp-transport', 'algorithmia', 'natural'];

exports.checkDependencies = function() {
	log.info("~~~~~~~~~~~~~~~~Module Check~~~~~~~~~~~~~~~~~");
	var modulesYouNeed = [];
	for (var i in dependencies) {
		if (dependencies.hasOwnProperty(i)) {
            try {
                require(dependencies[i]);
                log.info("Cool, looks like the module " + dependencies[i] + " is already installed!")
            } catch (e) {
                log.warn("Uh oh, looks like you're missing the module " + dependencies[i]);
                modulesYouNeed.push(dependencies[i]);
            }
        }
	}

	logSummary(dependencies, modulesYouNeed);
	log.info("~~~~~~~~~~~~~~~End Module Check~~~~~~~~~~~~~~~~\n\n");
};

function logSummary(dependencyList, modulesNotInstalled) {
	log.info("~~~~~~~~~~~~~~~~~~~~Summary~~~~~~~~~~~~~~~~~~~");
	log.info((dependencyList.length - modulesNotInstalled.length) + "/" +dependencyList.length + " modules installed");

	if (modulesNotInstalled.length == 0) {
		log.info("You have all the modules necesary to run this service!");
		return true;
	}

	log.warn("You still need the module(s):\n");
	for(var mods in modulesNotInstalled) {
		if (modulesNotInstalled.hasOwnProperty(mods))
			log.warn(modulesNotInstalled[mods] + "\n");
	}

	throw new Error("You can't run this service until all the modules are installed, check the previous module logs\n");
}<|endoftext|>


<commit_before>'use strict';

angular
  .module('fli.home')
  .controller('home.ctrl',
  function ($rootScope, $routeParams, $scope, $mdMedia,  local, SEARCH_KEY, LOOK_KEY) {

    $rootScope.fli.route = $routeParams || {};
    $rootScope.fli.media = $mdMedia;

    $scope.searchs = local.get(SEARCH_KEY);
    $scope.looks = local.get(LOOK_KEY);

  });
<commit_msg>add menu<commit_after>'use strict';

angular
  .module('fli.home')
  .controller('home.ctrl',
  function ($rootScope, $routeParams, $scope, $mdMedia, local, SEARCH_KEY, LOOK_KEY) {

    $rootScope.fli.route = $routeParams || {};
    $rootScope.fli.media = $mdMedia;

    $scope.searchs = local.get(SEARCH_KEY);
    $scope.looks = local.get(LOOK_KEY);

  });
<|endoftext|>


<commit_before>// @flow
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import VerticalTree from '../components/VerticalTree';
import * as verticalTreeActions from '../actions/verticalTreeActions';
import * as asyncActions from '../actions/asyncActions';
import * as userCodeActions from '../actions/userCodeActions';

function mapStateToProps(state) {
  return {
    treeData: state.verticalTreeData,
    ...state.userCode,
    ...state.async,
  };
}

function mapDispatchToProps(dispatch) {
  return bindActionCreators(
    {
      ...verticalTreeActions,
      ...asyncActions,
      ...userCodeActions
    }, dispatch);
}

export default connect(mapStateToProps, mapDispatchToProps)(VerticalTree);
<commit_msg>pass usercode state as props<commit_after>// @flow
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import VerticalTree from '../components/VerticalTree';
import * as verticalTreeActions from '../actions/verticalTreeActions';
import * as asyncActions from '../actions/asyncActions';
import * as userCodeActions from '../actions/userCodeActions';

function mapStateToProps(state) {
  return {
    treeData: state.verticalTreeData,
    theme: state.userCode.theme,
    userCode: state.userCode.userCode,
    isBinaryTree: state.userCode.binary,
    ...state.async,
  };
}

function mapDispatchToProps(dispatch) {
  return bindActionCreators(
    {
      ...verticalTreeActions,
      ...asyncActions,
      ...userCodeActions
    }, dispatch);
}

export default connect(mapStateToProps, mapDispatchToProps)(VerticalTree);
<|endoftext|>


<commit_before>var args = arguments[0] || {};
var data = args.data || '{}'; //検索結果JSON
var searchCondition = args.searchCondition || '';
var headerItems = [];
var glassIcon = Ti.UI.createImageView({
	image: "/images/glass.png",
	right: 30,
	top: 25,
	height: "30%"
});
glassIcon.addEventListener("touchend", function(e){
	var searchMenu = Alloy.createController("search/searchMenu").getView();
	searchMenu.open();
});
headerItems.push(glassIcon);

if(searchCondition){
	var conditionLabel = Ti.UI.createLabel({
		top: 20,
		left: '20%',
		width: '60%',
		color: '#000000',
		text: '検索条件: ' + searchCondition
	});
	headerItems.push(conditionLabel);
}

$.mainWin.add(Alloy.Globals.createCommonHeader(headerItems));

var screenWidthActual = pixelsToDPUnits(Ti.Platform.displayCaps.platformWidth);
var screenHeightActual = pixelsToDPUnits(Ti.Platform.displayCaps.platformHeight);
var properTop = screenHeightActual / 20;

var scrollView = Ti.UI.createScrollView({
    top: 70, // this should be changed
    showVerticalScrollIndicator: true,
    showHorizontalScrollIndicator: true,
});

var parsedData = JSON.parse(data);

for (var i = 0; i < parsedData.length; i++) {
	listBooks(i, parsedData[i]);
}

// the display width comes in pixel, so convert it to dpi
function pixelsToDPUnits(pixel) {
    return (pixel / (Titanium.Platform.displayCaps.dpi / 160));
}

function limitCharNum(pt) {
    var new_post_title = '';
    for (var i = 0; i < 19; i++) {
		if (i == pt.length) break;
		new_post_title += pt[i];
		if (i == 18) new_post_title += '...';
    }
    return new_post_title;
}

function listBooks(num, book) {
    if (num % 2 == 0) {
		var txt = limitCharNum(book.post_title);
		var thumbnail = Ti.UI.createImageView({
	  	  	image: book.image_url,
	   	 	width: pixelsToDPUnits(110),
	   		height: pixelsToDPUnits(160),
	   	 	left: (screenWidthActual - (pixelsToDPUnits(110) * 2)) / 3,
	    	top: properTop,
		});
		var title = Ti.UI.createLabel({
	    	width: screenWidthActual * 30 / 100,
	    	height: '56dp',
	    	left: (screenWidthActual - (pixelsToDPUnits(110) * 2)) / 3
			- screenWidthActual * 15 / 100 + pixelsToDPUnits(55),
	    	top: properTop + pixelsToDPUnits(160),

	    	color: '#00A935',
	    	font: { fontsize: 5, fontFamily: 'Helvetica Neue' },
	    	textAlign: Ti.UI.TEXT_ALIGNMENT_CENTER,
	    	verticalAlign: Ti.UI.TEXT_VERTICAL_ALIGNMENT_TOP,
	    	text: txt,
		});
    } else {
    	var txt = limitCharNum(book.post_title);
		var thumbnail = Ti.UI.createImageView({
	    	image: book.image_url,
	    	width: '110px',
	    	height: '160px',
	    	left: ((screenWidthActual - (pixelsToDPUnits(110) * 2)) / 3) * 2
			+ pixelsToDPUnits(110),
	    	top: properTop
		});
		var title = Ti.UI.createLabel({
	    	color: '#00A935',
	    	width: screenWidthActual * 30 / 100,
	    	height: '56dp',
	    	left: ((screenWidthActual - (pixelsToDPUnits(110) * 2)) / 3) * 2
			+ pixelsToDPUnits(110) - screenWidthActual * 15 / 100
			+ pixelsToDPUnits(55),
	    	top: properTop + pixelsToDPUnits(160),
	    	font: { fontsize: 5, fontFamily: 'Helvetica Neue' },
	    	textAlign: Ti.UI.TEXT_ALIGNMENT_CENTER,
	    	verticalAlign: Ti.UI.TEXT_VERTICAL_ALIGNMENT_TOP,
	    	text: txt,
		});
		properTop += screenHeightActual / 3;
    }
	thumbnail.addEventListener('click', function(e){
		openBookPage({
			title: book.post_title,
			author: book.author,
			category: book.category[0].name,
			image_url: book.image_url,
			price: book.price,
			count: book.book_count
		});
	});
	title.addEventListener('click', function(e){
		openBookPage({
			title: book.post_title,
			author: book.author,
			category: book.category[0].name,
			image_url: book.image_url,
			price: book.price,
			count: book.book_count
		});
	});
    scrollView.add(thumbnail);
    scrollView.add(title);
    $.mainWin.add(scrollView);
}

function openBookPage(args){
	var itemWin = Alloy.createController('book', {
		title: args.title,
		image_url: args.image_url,
		author: args.author,
		category: args.category,
		price: args.price,
		count: args.count
	}).getView();
	itemWin.open();
}
<commit_msg>Fix overflow occuring the retrieved books data is too much<commit_after>var args = arguments[0] || {};
var data = args.data || '{}'; //検索結果JSON
var searchCondition = args.searchCondition || '';
var headerItems = [];
var glassIcon = Ti.UI.createImageView({
	image: "/images/glass.png",
	right: 30,
	top: 25,
	height: "30%"
});
glassIcon.addEventListener("touchend", function(e){
	var searchMenu = Alloy.createController("search/searchMenu").getView();
	searchMenu.open();
});
headerItems.push(glassIcon);

if(searchCondition){
	var conditionLabel = Ti.UI.createLabel({
		top: 20,
		left: '20%',
		width: '60%',
		color: '#000000',
		text: '検索条件: ' + searchCondition
	});
	headerItems.push(conditionLabel);
}

$.mainWin.add(Alloy.Globals.createCommonHeader(headerItems));

var screenWidthActual = pixelsToDPUnits(Ti.Platform.displayCaps.platformWidth);
var screenHeightActual = pixelsToDPUnits(Ti.Platform.displayCaps.platformHeight);
var properTop = screenHeightActual / 20;

var scrollView = Ti.UI.createScrollView({
    top: 70, // this should be changed
    showVerticalScrollIndicator: true,
    showHorizontalScrollIndicator: true,
});
$.mainWin.add(scrollView);

var parsedData = JSON.parse(data);
if(parsedData.length >= 50) {
	parsedData = parsedData.slice(0, 49);
}

for (var i = 0; i < parsedData.length; i++) {
	listBooks(i, parsedData[i]);
}

// the display width comes in pixel, so convert it to dpi
function pixelsToDPUnits(pixel) {
    return (pixel / (Titanium.Platform.displayCaps.dpi / 160));
}

function limitCharNum(pt) {
    var new_post_title = '';
    for (var i = 0; i < 19; i++) {
		if (i == pt.length) break;
		new_post_title += pt[i];
		if (i == 18) new_post_title += '...';
    }
    return new_post_title;
}

function listBooks(num, book) {
    if (num % 2 == 0) {
		var txt = limitCharNum(book.post_title);
		var thumbnail = Ti.UI.createImageView({
	  	  	image: book.image_url,
	   	 	width: pixelsToDPUnits(110),
	   		height: pixelsToDPUnits(160),
	   	 	left: (screenWidthActual - (pixelsToDPUnits(110) * 2)) / 3,
	    	top: properTop,
		});
		var title = Ti.UI.createLabel({
	    	width: screenWidthActual * 30 / 100,
	    	height: '56dp',
	    	left: (screenWidthActual - (pixelsToDPUnits(110) * 2)) / 3
			- screenWidthActual * 15 / 100 + pixelsToDPUnits(55),
	    	top: properTop + pixelsToDPUnits(160),

	    	color: '#00A935',
	    	font: { fontsize: 5, fontFamily: 'Helvetica Neue' },
	    	textAlign: Ti.UI.TEXT_ALIGNMENT_CENTER,
	    	verticalAlign: Ti.UI.TEXT_VERTICAL_ALIGNMENT_TOP,
	    	text: txt,
		});
    } else {
    	var txt = limitCharNum(book.post_title);
		var thumbnail = Ti.UI.createImageView({
	    	image: book.image_url,
	    	width: '110px',
	    	height: '160px',
	    	left: ((screenWidthActual - (pixelsToDPUnits(110) * 2)) / 3) * 2
			+ pixelsToDPUnits(110),
	    	top: properTop
		});
		var title = Ti.UI.createLabel({
	    	color: '#00A935',
	    	width: screenWidthActual * 30 / 100,
	    	height: '56dp',
	    	left: ((screenWidthActual - (pixelsToDPUnits(110) * 2)) / 3) * 2
			+ pixelsToDPUnits(110) - screenWidthActual * 15 / 100
			+ pixelsToDPUnits(55),
	    	top: properTop + pixelsToDPUnits(160),
	    	font: { fontsize: 5, fontFamily: 'Helvetica Neue' },
	    	textAlign: Ti.UI.TEXT_ALIGNMENT_CENTER,
	    	verticalAlign: Ti.UI.TEXT_VERTICAL_ALIGNMENT_TOP,
	    	text: txt,
		});
		properTop += screenHeightActual / 3;
    }
	thumbnail.addEventListener('click', function(e){
		openBookPage({
			title: book.post_title,
			author: book.author,
			category: book.category[0].name,
			image_url: book.image_url,
			price: book.price,
			count: book.book_count
		});
	});
	title.addEventListener('click', function(e){
		openBookPage({
			title: book.post_title,
			author: book.author,
			category: book.category[0].name,
			image_url: book.image_url,
			price: book.price,
			count: book.book_count
		});
	});
    scrollView.add(thumbnail);
    scrollView.add(title);
}

function openBookPage(args){
	var itemWin = Alloy.createController('book', {
		title: args.title,
		image_url: args.image_url,
		author: args.author,
		category: args.category,
		price: args.price,
		count: args.count
	}).getView();
	itemWin.open();
}
<|endoftext|>


<commit_before>'use strict';

const Promise = require('bluebird');
const User = require('../models/User');

module.exports = function() {
  const create = function(userData) {
    return new Promise((resolve, reject) => {
      const user = new User(userData);
      user.createdAt = Date.now();
      user.updatedAt = Date.now();
      user.save()
        .then((createdUser) => {
          User.findById(createdUser.id)
            .select('-__v')
            .exec()
            .then((newUser) => {
              resolve(newUser.toObject());
            })
            .catch((err) => {
              console.log(err);
              reject();
            });
        })
        .catch((err) => {
          console.log(err);
          reject();
        });
    });
  };

  const getByEmailOrUsername = function(emailOrUsername) {
    return new Promise((resolve, reject) => {
      User.findOne({
        $or: [
          { email: emailOrUsername },
          { username: emailOrUsername },
        ],
      })
        .select('-__v')
        .exec()
        .then((user) => {
          resolve(user.toObject());
        })
        .catch(reject);
    });
  };

  const getAllUsers = function(currentUser) {
    return new Promise((resolve, reject) => {
      User.find({
        fullName: { $ne: currentUser.fullName },
      }, {
        password: 0,
      })
        .select('-__v')
        .exec()
        .then((users) => {
          resolve(users);
        })
        .catch(reject);
    });
  };

  const updateHandicap = function(emailOrUsername, newHandicap) {
    return new Promise((resolve, reject) => {
      User.findOneAndUpdate({
        $or: [
          { email: emailOrUsername },
          { username: emailOrUsername },
        ],
      }, {
        handicap: newHandicap,
      })
        .select('-__v')
        .exec()
        .then(resolve(newHandicap))
        .catch((err) => {
          console.log(err);
          reject();
        });
    });
  };

  return {
    create: create,
    getByEmailOrUsername: getByEmailOrUsername,
    getAllUsers: getAllUsers,
    updateHandicap: updateHandicap,
  };
};
<commit_msg>replace updateHandicap with updateUser to allow updating multiple properties in one query<commit_after>'use strict';

const Promise = require('bluebird');
const User = require('../models/User');

module.exports = function() {
  const create = function(userData) {
    return new Promise((resolve, reject) => {
      const user = new User(userData);
      user.createdAt = Date.now();
      user.updatedAt = Date.now();
      user.save()
        .then((createdUser) => {
          User.findById(createdUser.id)
            .select('-__v')
            .exec()
            .then((newUser) => {
              resolve(newUser.toObject());
            })
            .catch((err) => {
              console.log(err);
              reject();
            });
        })
        .catch((err) => {
          console.log(err);
          reject();
        });
    });
  };

  const getByEmailOrUsername = function(emailOrUsername) {
    return new Promise((resolve, reject) => {
      User.findOne({
        $or: [
          { email: emailOrUsername },
          { username: emailOrUsername },
        ],
      })
        .select('-__v')
        .exec()
        .then((user) => {
          resolve(user.toObject());
        })
        .catch(reject);
    });
  };

  const updateUser = function(emailOrUsername, newData) {
    User.findOneAndUpdate({
      $or: [
        { email: emailOrUsername },
        { username: emailOrUsername },
      ],
    }, newData)
      .select('-__v')
      .exec()
      .then((user) => {
        console.log('updateUser function');
        console.log(user);
        resolve(user);
      })
      .catch(reject);
  };

  const getAllUsers = function(currentUser) {
    return new Promise((resolve, reject) => {
      User.find({
        fullName: { $ne: currentUser.fullName },
      }, {
        password: 0,
      })
        .select('-__v')
        .exec()
        .then((users) => {
          resolve(users);
        })
        .catch(reject);
    });
  };

  return {
    create: create,
    getByEmailOrUsername: getByEmailOrUsername,
    getAllUsers: getAllUsers,
    updateUser: updateUser,
  };
};
<|endoftext|>