<commit_before>import path from "path";
import config from "config";
import Q from "q";
import formidable from "formidable";
import nodemailer from "nodemailer";
import sendmailTransport from "nodemailer-sendmail-transport";

import NarrowsStore from "./NarrowsStore";
import UserStore from "./UserStore";
import mentionFilter from "./mention-filter";
import messageUtils from "./message-utils";
import feeds from "./feeds";
import Mailer from "./Mailer";

const store = new NarrowsStore(config.db, config.files.path);
store.connect();
const userStore = new UserStore(config.db);
userStore.connect();

const transporter = nodemailer.createTransport(sendmailTransport());
const mailer = new Mailer(store, transporter);

export function getNarrationOverview(req, res) {
    store.getNarrationOverview(req.session.userId, 5).then(narrationOverviewData => {
        res.json(narrationOverviewData);
    }).catch(err => {
        res.status(500).json({
            errorMessage: `Cannot get narration overview for ` +
                `this user: ${ err }`
        });
    });
}

export function getNarration(req, res) {
    const narrationId = parseInt(req.params.narrId, 10);

    store.getNarration(narrationId).then(narrationData => {
        return userStore.canActAs(
            req.session.userId,
            narrationData.narratorId
        ).then(() => {
            res.json(narrationData);
        });
    }).catch(err => {
        res.status(404).json({
            errorMessage: `Cannot find narration ${ narrationId } for ` +
                `this user: ${ err }`
        });
    });
}

export function getChapter(req, res) {
    const chapterId = parseInt(req.params.chptId, 10);

    store.getChapter(chapterId, { includePrivateFields: true }).then(chapterData => (
        store.getNarration(chapterData.narrationId).then(narrationData => (
            userStore.canActAs(req.session.userId, narrationData.narratorId)
        )).then(() => (
            res.json(chapterData)
        ))
    )).catch(err => {
        res.status(404).json({
            errorMessage: `Cannot find chapter ${ chapterId }: ${ err }`
        });
    });
}

export function getChapterCharacter(req, res) {
    const chapterId = parseInt(req.params.chptId, 10);
    const characterToken = req.params.charToken;

    store.getCharacterInfo(characterToken).then(charInfo => {
        return store.getChapter(chapterId).then(chapterData => {
            if (!chapterData.published && !req.session.loggedIn) {
                throw new Error("Unpublished chapter");
            }

            const participantIds = chapterData.participants.map(p => p.id);
            if (participantIds.indexOf(charInfo.id) === -1) {
                throw new Error("Character does not participate in chapter");
            }

            chapterData.character = charInfo;
            chapterData.text =
                mentionFilter.filter(chapterData.text, charInfo.id);

            store.getChapterReaction(chapterId, charInfo.id).then(reaction => {
                chapterData.reaction = reaction;
                res.json(chapterData);
            });
        });
    }).catch(err => {
        res.status(404).json({
            errorMessage: `Cannot find chapter ${ chapterId }` +
                ` with character ${ characterToken }: ${ err }`
        });
    });
}

export function getNarrationChapters(req, res) {
    const narrationId = parseInt(req.params.narrId, 10);

    Q.all([
        store.getNarrationChapters(narrationId),
        store.getNarration(narrationId)
    ]).spread((chapterListData, narrationData) => {
        return userStore.canActAs(
            req.session.userId,
            narrationData.narratorId
        ).then(() => {
            res.json({ narration: narrationData, chapters: chapterListData });
        });
    }).catch(err => {
        res.status(404).json({
            errorMessage: `Cannot find chapters for narration ${ narrationId }: ${ err }`
        });
    });
}

export function putChapter(req, res) {
    const chapterId = parseInt(req.params.chptId, 10);

    req.body.text = JSON.stringify(req.body.text);
    store.getChapter(chapterId).then(origChapter => (
        store.getNarration(origChapter.narrationId).then(narrationData => (
            userStore.canActAs(req.session.userId, narrationData.narratorId)
        )).then(() => {
            const origPublished = origChapter.published;

            return store.updateChapter(chapterId, req.body).then(chapter => {
                res.json(chapter);

                if (!origPublished && req.body.published) {
                    mailer.chapterPublished(chapter);
                }
            });
        })
    )).catch(err => {
        res.status(500).json({
            errorMessage: `There was a problem updating: ${ err }`
        });
    });
}

export function getChapterInteractions(req, res) {
    const chapterId = req.params.chptId;

    return Q.all([
        store.getChapter(chapterId, { includePrivateFields: true }),
        store.getAllChapterMessages(chapterId),
        store.getChapterReactions(chapterId)
    ]).spread((chapter, messages, reactions) => (
        store.getNarration(chapter.narrationId).then(narrationData => (
            userStore.canActAs(req.session.userId, narrationData.narratorId)
        )).then(() => (
            res.json({
                chapter: chapter,
                messageThreads: messageUtils.threadMessages(messages),
                reactions: reactions
            })
        ))
    )).catch(err => {
        res.status(500).json({
            errorMessage: `Could not get interactions: ${ err }`
        });
    });
}

export function postChapterMessages(req, res) {
    const chapterId = req.params.chptId,
          messageText = req.body.text,
          messageRecipients = req.body.recipients || [];

    return store.getChapter(chapterId).then(chapterData => (
        store.getNarration(chapterData.narrationId)
    )).then(narrationData => (
        userStore.canActAs(req.session.userId, narrationData.narratorId)
    )).then(() => (
        store.addMessage(
            chapterId,
            null,
            messageText,
            messageRecipients
        )
    )).then(() => (
        store.getAllChapterMessages(chapterId)
    )).then(messages => {
        res.json({
            messageThreads: messageUtils.threadMessages(messages)
        });

        mailer.messagePosted({chapterId: chapterId,
                              sender: {id: null, name: "Narrator"},
                              text: messageText,
                              recipients: messageRecipients});
    }).catch(err => {
        res.status(500).json({
            errorMessage: `Could not post message: ${ err }`
        });
    });
}

export function postNewChapter(req, res) {
    const narrationId = parseInt(req.params.narrId, 10);

    store.getNarration(narrationId).then(narrationData => (
        userStore.canActAs(req.session.userId, narrationData.narratorId)
    )).then(() => (
        store.createChapter(narrationId, req.body)
    )).then(chapterData => {
        res.json(chapterData);

        if (chapterData.published) {
            mailer.chapterPublished(chapterData);
        }
    }).catch(err => {
        res.status(500).json({
            errorMessage: `There was a problem: ${ err }`
        });
    });
}

export function postNarrationFiles(req, res) {
    const narrationId = parseInt(req.params.narrId, 10);

    const form = new formidable.IncomingForm();
    form.uploadDir = config.files.tmpPath;

    store.getNarration(narrationId).then(narrationData => (
        userStore.canActAs(req.session.userId, narrationData.narratorId)
    )).then(() => (
        Q.ninvoke(form, "parse", req)
    )).spread(function(fields, files) {
        var uploadedFileInfo = files.file,
            filename = path.basename(uploadedFileInfo.name),
            tmpPath = uploadedFileInfo.path;

        return store.addMediaFile(narrationId, filename, tmpPath);
    }).then(fileInfo => (
        res.json(fileInfo)
    )).catch(err => {
        console.error("Caught error???");
        res.status(500).json({
            errorMessage: `Cannot add new media file: ${ err }`
        });
    });
}

function uploadFile(req, res, type) {
    const narrationId = parseInt(req.params.narrId, 10);

    const form = new formidable.IncomingForm();
    form.uploadDir = config.files.tmpPath;

    store.getNarration(narrationId).then(narrationData => (
        userStore.canActAs(req.session.userId, narrationData.narratorId)
    )).then(() => (
        Q.ninvoke(form, "parse", req)
    )).spread(function(fields, files) {
        var uploadedFileInfo = files.file,
            filename = path.basename(uploadedFileInfo.name),
            tmpPath = uploadedFileInfo.path;

        return store.addMediaFile(narrationId, filename, tmpPath, type);
    }).then(fileInfo => {
        res.json(fileInfo);
    }).catch(err => {
        res.status(500).json({
            errorMessage: `Cannot add new media file: ${ err }`
        });
    });
}

export function postNarrationImages(req, res) {
    uploadFile(req, res, "images");
}

export function getChapterLastReactions(req, res) {
    const chapterId = parseInt(req.params.chptId, 10);

    store.getChapter(chapterId).then(chapterData => (
        store.getNarration(chapterData.narrationId)
    )).then(narrationData => (
        userStore.canActAs(req.session.userId, narrationData.narratorId)
    )).then(() => (
        store.getChapterLastReactions(chapterId)
    )).then(lastReactions => {
        res.json({ chapterId: chapterId,
                   lastReactions: lastReactions.map(reaction => (
                       { chapter: { id: reaction.chapterId,
                                    title: reaction.chapterTitle },
                         character: { id: reaction.characterId,
                                      name: reaction.characterName },
                         text: reaction.text }
                   )) });
    }).catch(err => {
        res.status(500).json({
            errorMessage: `Cannot get last reactions: ${ err }`
        });
    });
}

export function getStaticFile(req, res) {
    res.sendFile(path.join(req.params.narrId, req.params.filename),
                 { root: config.files.path });
}

export function putReactionCharacter(req, res) {
    const chapterId = req.params.chptId,
          characterToken = req.params.charToken,
          reactionText = req.body.text;

    store.getCharacterInfo(characterToken).then(({ id: characterId }) => {
        return store.updateReaction(chapterId, characterId, reactionText).then(() => {
            res.json({ chapterId, characterId, reactionText });

            mailer.reactionPosted(chapterId, characterToken, reactionText);
        });
    }).catch(err => {
        res.status(500).json({
            errorMessage: `Could not save reaction: ${ err }`
        });
    });
}

export function getMessagesCharacter(req, res) {
    const chapterId = req.params.chptId,
          characterToken = req.params.charToken;

    store.getCharacterInfo(characterToken).then(characterInfo => {
        return store.getChapterMessages(chapterId, characterInfo.id).then(messages => {
            res.json({
                messageThreads: messageUtils.threadMessages(messages),
                characterId: characterInfo.id
            });
        });
    }).catch(err => {
        res.status(500).json({
            errorMessage: `Could not get messages: ${ err }`
        });
    });
}

export function postMessageCharacter(req, res) {
    const chapterId = req.params.chptId,
          characterToken = req.params.charToken,
          messageText = req.body.text,
          messageRecipients = req.body.recipients || [];

    // TODO: Check that the character really belongs to this narration

    store.getCharacterInfo(characterToken).then(characterInfo => {
        return store.addMessage(
            chapterId,
            characterInfo.id,
            messageText,
            messageRecipients
        ).then(() => {
            return store.getChapterMessages(chapterId, characterInfo.id);
        }).then(messages => {
            res.json({
                messageThreads: messageUtils.threadMessages(messages),
                characterId: characterInfo.id
            });

            mailer.messagePosted({chapterId: chapterId,
                                  sender: characterInfo,
                                  text: messageText,
                                  recipients: messageRecipients});
        });
    }).catch(err => {
        res.status(500).json({
            errorMessage: `Could not post message: ${ err }`
        });
    });
}

export function getFeedsCharacter(req, res) {
    const characterToken = req.params.charToken;

    store.getCharacterInfo(characterToken).then(characterInfo => {
        const { id: characterId, name: characterName } = characterInfo;
        const baseUrl = req.protocol + "://" + req.get("host");

        return feeds.makeCharacterFeed(baseUrl, characterInfo, store);
    }).then(feedXml => {
        res.set("Content-Type", "application/rss+xml");
        res.send(feedXml);
    }).catch(err => {
        res.status(500).json({
            errorMessage: `Could not create feed: ${ err }`
        });
    });
}

export function putNotesCharacter(req, res) {
    const characterToken = req.params.charToken;
    const newNotes = req.body.notes;

    store.getCharacterInfo(characterToken).then(character => {
        store.saveCharacterNotes(character.id, newNotes).then(() => {
            character.notes = newNotes;
            res.json(character);
        }).catch(err => {
            res.status(500).json({
                errorMessage: `Could not save notes: ${ err }`
            });
        });
    }).catch(err => {
        res.status(500).json({
            errorMessage: `Could not save notes for non-existent ` +
                `character ${ characterToken }`
        });
    });
}

export function getCharacter(req, res) {
    const characterToken = req.params.charToken;

    return store.getCharacterInfo(characterToken).then(character => (
        store.getFullCharacterStats(character.id).then(stats => {
            res.json(stats);
        })
    )).catch(err => {
        res.status(500).json({
            errorMessage: `Could not get full character stats for ` +
                `character '${ characterToken }': ${ err }`
        });
    });
}

export function putCharacter(req, res) {
    const characterToken = req.params.charToken;
    const newProps = req.body;

    if (newProps.description) {
        newProps.description = JSON.stringify(newProps.description);
    }
    if (newProps.backstory) {
        newProps.backstory = JSON.stringify(newProps.backstory);
    }

    console.log("newProps =", JSON.stringify(newProps, null, 2));

    return store.getCharacterInfo(characterToken).then(character => (
        store.updateCharacter(character.id, newProps).then(newCharacter => {
            res.json(newCharacter);
        })
    )).catch(err => {
        res.status(500).json({
            errorMessage: `Could not update character with ` +
                `id '${ characterToken }': ${ err }`
        });
    });
}
<commit_msg>Fix chapter previews for narrators<commit_after>import path from "path";
import config from "config";
import Q from "q";
import formidable from "formidable";
import nodemailer from "nodemailer";
import sendmailTransport from "nodemailer-sendmail-transport";

import NarrowsStore from "./NarrowsStore";
import UserStore from "./UserStore";
import mentionFilter from "./mention-filter";
import messageUtils from "./message-utils";
import feeds from "./feeds";
import Mailer from "./Mailer";

const store = new NarrowsStore(config.db, config.files.path);
store.connect();
const userStore = new UserStore(config.db);
userStore.connect();

const transporter = nodemailer.createTransport(sendmailTransport());
const mailer = new Mailer(store, transporter);

export function getNarrationOverview(req, res) {
    store.getNarrationOverview(req.session.userId, 5).then(narrationOverviewData => {
        res.json(narrationOverviewData);
    }).catch(err => {
        res.status(500).json({
            errorMessage: `Cannot get narration overview for ` +
                `this user: ${ err }`
        });
    });
}

export function getNarration(req, res) {
    const narrationId = parseInt(req.params.narrId, 10);

    store.getNarration(narrationId).then(narrationData => {
        return userStore.canActAs(
            req.session.userId,
            narrationData.narratorId
        ).then(() => {
            res.json(narrationData);
        });
    }).catch(err => {
        res.status(404).json({
            errorMessage: `Cannot find narration ${ narrationId } for ` +
                `this user: ${ err }`
        });
    });
}

export function getChapter(req, res) {
    const chapterId = parseInt(req.params.chptId, 10);

    store.getChapter(chapterId, { includePrivateFields: true }).then(chapterData => (
        store.getNarration(chapterData.narrationId).then(narrationData => (
            userStore.canActAs(req.session.userId, narrationData.narratorId)
        )).then(() => (
            res.json(chapterData)
        ))
    )).catch(err => {
        res.status(404).json({
            errorMessage: `Cannot find chapter ${ chapterId }: ${ err }`
        });
    });
}

export function getChapterCharacter(req, res) {
    const chapterId = parseInt(req.params.chptId, 10);
    const characterToken = req.params.charToken;

    store.getCharacterInfo(characterToken).then(charInfo => {
        return store.getChapter(chapterId).then(chapterData => {
            if (!chapterData.published && !req.session.userId) {
                throw new Error("Unpublished chapter");
            }

            const participantIds = chapterData.participants.map(p => p.id);
            if (participantIds.indexOf(charInfo.id) === -1) {
                throw new Error("Character does not participate in chapter");
            }

            chapterData.character = charInfo;
            chapterData.text =
                mentionFilter.filter(chapterData.text, charInfo.id);

            store.getChapterReaction(chapterId, charInfo.id).then(reaction => {
                chapterData.reaction = reaction;
                res.json(chapterData);
            });
        });
    }).catch(err => {
        res.status(404).json({
            errorMessage: `Cannot find chapter ${ chapterId }` +
                ` with character ${ characterToken }: ${ err }`
        });
    });
}

export function getNarrationChapters(req, res) {
    const narrationId = parseInt(req.params.narrId, 10);

    Q.all([
        store.getNarrationChapters(narrationId),
        store.getNarration(narrationId)
    ]).spread((chapterListData, narrationData) => {
        return userStore.canActAs(
            req.session.userId,
            narrationData.narratorId
        ).then(() => {
            res.json({ narration: narrationData, chapters: chapterListData });
        });
    }).catch(err => {
        res.status(404).json({
            errorMessage: `Cannot find chapters for narration ${ narrationId }: ${ err }`
        });
    });
}

export function putChapter(req, res) {
    const chapterId = parseInt(req.params.chptId, 10);

    req.body.text = JSON.stringify(req.body.text);
    store.getChapter(chapterId).then(origChapter => (
        store.getNarration(origChapter.narrationId).then(narrationData => (
            userStore.canActAs(req.session.userId, narrationData.narratorId)
        )).then(() => {
            const origPublished = origChapter.published;

            return store.updateChapter(chapterId, req.body).then(chapter => {
                res.json(chapter);

                if (!origPublished && req.body.published) {
                    mailer.chapterPublished(chapter);
                }
            });
        })
    )).catch(err => {
        res.status(500).json({
            errorMessage: `There was a problem updating: ${ err }`
        });
    });
}

export function getChapterInteractions(req, res) {
    const chapterId = req.params.chptId;

    return Q.all([
        store.getChapter(chapterId, { includePrivateFields: true }),
        store.getAllChapterMessages(chapterId),
        store.getChapterReactions(chapterId)
    ]).spread((chapter, messages, reactions) => (
        store.getNarration(chapter.narrationId).then(narrationData => (
            userStore.canActAs(req.session.userId, narrationData.narratorId)
        )).then(() => (
            res.json({
                chapter: chapter,
                messageThreads: messageUtils.threadMessages(messages),
                reactions: reactions
            })
        ))
    )).catch(err => {
        res.status(500).json({
            errorMessage: `Could not get interactions: ${ err }`
        });
    });
}

export function postChapterMessages(req, res) {
    const chapterId = req.params.chptId,
          messageText = req.body.text,
          messageRecipients = req.body.recipients || [];

    return store.getChapter(chapterId).then(chapterData => (
        store.getNarration(chapterData.narrationId)
    )).then(narrationData => (
        userStore.canActAs(req.session.userId, narrationData.narratorId)
    )).then(() => (
        store.addMessage(
            chapterId,
            null,
            messageText,
            messageRecipients
        )
    )).then(() => (
        store.getAllChapterMessages(chapterId)
    )).then(messages => {
        res.json({
            messageThreads: messageUtils.threadMessages(messages)
        });

        mailer.messagePosted({chapterId: chapterId,
                              sender: {id: null, name: "Narrator"},
                              text: messageText,
                              recipients: messageRecipients});
    }).catch(err => {
        res.status(500).json({
            errorMessage: `Could not post message: ${ err }`
        });
    });
}

export function postNewChapter(req, res) {
    const narrationId = parseInt(req.params.narrId, 10);

    store.getNarration(narrationId).then(narrationData => (
        userStore.canActAs(req.session.userId, narrationData.narratorId)
    )).then(() => (
        store.createChapter(narrationId, req.body)
    )).then(chapterData => {
        res.json(chapterData);

        if (chapterData.published) {
            mailer.chapterPublished(chapterData);
        }
    }).catch(err => {
        res.status(500).json({
            errorMessage: `There was a problem: ${ err }`
        });
    });
}

export function postNarrationFiles(req, res) {
    const narrationId = parseInt(req.params.narrId, 10);

    const form = new formidable.IncomingForm();
    form.uploadDir = config.files.tmpPath;

    store.getNarration(narrationId).then(narrationData => (
        userStore.canActAs(req.session.userId, narrationData.narratorId)
    )).then(() => (
        Q.ninvoke(form, "parse", req)
    )).spread(function(fields, files) {
        var uploadedFileInfo = files.file,
            filename = path.basename(uploadedFileInfo.name),
            tmpPath = uploadedFileInfo.path;

        return store.addMediaFile(narrationId, filename, tmpPath);
    }).then(fileInfo => (
        res.json(fileInfo)
    )).catch(err => {
        console.error("Caught error???");
        res.status(500).json({
            errorMessage: `Cannot add new media file: ${ err }`
        });
    });
}

function uploadFile(req, res, type) {
    const narrationId = parseInt(req.params.narrId, 10);

    const form = new formidable.IncomingForm();
    form.uploadDir = config.files.tmpPath;

    store.getNarration(narrationId).then(narrationData => (
        userStore.canActAs(req.session.userId, narrationData.narratorId)
    )).then(() => (
        Q.ninvoke(form, "parse", req)
    )).spread(function(fields, files) {
        var uploadedFileInfo = files.file,
            filename = path.basename(uploadedFileInfo.name),
            tmpPath = uploadedFileInfo.path;

        return store.addMediaFile(narrationId, filename, tmpPath, type);
    }).then(fileInfo => {
        res.json(fileInfo);
    }).catch(err => {
        res.status(500).json({
            errorMessage: `Cannot add new media file: ${ err }`
        });
    });
}

export function postNarrationImages(req, res) {
    uploadFile(req, res, "images");
}

export function getChapterLastReactions(req, res) {
    const chapterId = parseInt(req.params.chptId, 10);

    store.getChapter(chapterId).then(chapterData => (
        store.getNarration(chapterData.narrationId)
    )).then(narrationData => (
        userStore.canActAs(req.session.userId, narrationData.narratorId)
    )).then(() => (
        store.getChapterLastReactions(chapterId)
    )).then(lastReactions => {
        res.json({ chapterId: chapterId,
                   lastReactions: lastReactions.map(reaction => (
                       { chapter: { id: reaction.chapterId,
                                    title: reaction.chapterTitle },
                         character: { id: reaction.characterId,
                                      name: reaction.characterName },
                         text: reaction.text }
                   )) });
    }).catch(err => {
        res.status(500).json({
            errorMessage: `Cannot get last reactions: ${ err }`
        });
    });
}

export function getStaticFile(req, res) {
    res.sendFile(path.join(req.params.narrId, req.params.filename),
                 { root: config.files.path });
}

export function putReactionCharacter(req, res) {
    const chapterId = req.params.chptId,
          characterToken = req.params.charToken,
          reactionText = req.body.text;

    store.getCharacterInfo(characterToken).then(({ id: characterId }) => {
        return store.updateReaction(chapterId, characterId, reactionText).then(() => {
            res.json({ chapterId, characterId, reactionText });

            mailer.reactionPosted(chapterId, characterToken, reactionText);
        });
    }).catch(err => {
        res.status(500).json({
            errorMessage: `Could not save reaction: ${ err }`
        });
    });
}

export function getMessagesCharacter(req, res) {
    const chapterId = req.params.chptId,
          characterToken = req.params.charToken;

    store.getCharacterInfo(characterToken).then(characterInfo => {
        return store.getChapterMessages(chapterId, characterInfo.id).then(messages => {
            res.json({
                messageThreads: messageUtils.threadMessages(messages),
                characterId: characterInfo.id
            });
        });
    }).catch(err => {
        res.status(500).json({
            errorMessage: `Could not get messages: ${ err }`
        });
    });
}

export function postMessageCharacter(req, res) {
    const chapterId = req.params.chptId,
          characterToken = req.params.charToken,
          messageText = req.body.text,
          messageRecipients = req.body.recipients || [];

    // TODO: Check that the character really belongs to this narration

    store.getCharacterInfo(characterToken).then(characterInfo => {
        return store.addMessage(
            chapterId,
            characterInfo.id,
            messageText,
            messageRecipients
        ).then(() => {
            return store.getChapterMessages(chapterId, characterInfo.id);
        }).then(messages => {
            res.json({
                messageThreads: messageUtils.threadMessages(messages),
                characterId: characterInfo.id
            });

            mailer.messagePosted({chapterId: chapterId,
                                  sender: characterInfo,
                                  text: messageText,
                                  recipients: messageRecipients});
        });
    }).catch(err => {
        res.status(500).json({
            errorMessage: `Could not post message: ${ err }`
        });
    });
}

export function getFeedsCharacter(req, res) {
    const characterToken = req.params.charToken;

    store.getCharacterInfo(characterToken).then(characterInfo => {
        const { id: characterId, name: characterName } = characterInfo;
        const baseUrl = req.protocol + "://" + req.get("host");

        return feeds.makeCharacterFeed(baseUrl, characterInfo, store);
    }).then(feedXml => {
        res.set("Content-Type", "application/rss+xml");
        res.send(feedXml);
    }).catch(err => {
        res.status(500).json({
            errorMessage: `Could not create feed: ${ err }`
        });
    });
}

export function putNotesCharacter(req, res) {
    const characterToken = req.params.charToken;
    const newNotes = req.body.notes;

    store.getCharacterInfo(characterToken).then(character => {
        store.saveCharacterNotes(character.id, newNotes).then(() => {
            character.notes = newNotes;
            res.json(character);
        }).catch(err => {
            res.status(500).json({
                errorMessage: `Could not save notes: ${ err }`
            });
        });
    }).catch(err => {
        res.status(500).json({
            errorMessage: `Could not save notes for non-existent ` +
                `character ${ characterToken }`
        });
    });
}

export function getCharacter(req, res) {
    const characterToken = req.params.charToken;

    return store.getCharacterInfo(characterToken).then(character => (
        store.getFullCharacterStats(character.id).then(stats => {
            res.json(stats);
        })
    )).catch(err => {
        res.status(500).json({
            errorMessage: `Could not get full character stats for ` +
                `character '${ characterToken }': ${ err }`
        });
    });
}

export function putCharacter(req, res) {
    const characterToken = req.params.charToken;
    const newProps = req.body;

    if (newProps.description) {
        newProps.description = JSON.stringify(newProps.description);
    }
    if (newProps.backstory) {
        newProps.backstory = JSON.stringify(newProps.backstory);
    }

    console.log("newProps =", JSON.stringify(newProps, null, 2));

    return store.getCharacterInfo(characterToken).then(character => (
        store.updateCharacter(character.id, newProps).then(newCharacter => {
            res.json(newCharacter);
        })
    )).catch(err => {
        res.status(500).json({
            errorMessage: `Could not update character with ` +
                `id '${ characterToken }': ${ err }`
        });
    });
}
<|endoftext|>


<commit_before>/**
 * @license
 * Copyright 2019 The FOAM Authors. All Rights Reserved.
 * http://www.apache.org/licenses/LICENSE-2.0
 */

foam.CLASS({
  package: 'foam.u2.detail',
  name: 'SectionedDetailPropertyView',
  extends: 'foam.u2.View',

  documentation: `
    View for one property of a SectionedDetailView.
  `,

  css: `
    ^card-label {
      font-size: 16px;
      font-weight: bold;
    }
    ^validation-container {
      margin-top: 4px;
      color: #d9170e;
    }
    ^helper-icon {
      width: 20px;
      height: 20px;
    }
    ^error-icon {
      width: 16px;
      height: 16px;
    }
    ^error .foam-u2-TextField {
      background-color: #fbedec;
      border: solid 1px #d9170e;
      font-size: 12px;
    }
    ^error .foam-u2-tag-TextArea {
      background-color: #fbedec;
      border: solid 1px #d9170e;
      font-size: 12px;
    }
    ^error .foam-u2-tag-Select {
      background-color: #fbedec;
      border: solid 1px #d9170e;
      font-size: 12px;
    }
    ^error .foam-u2-view-date-DateTimePicker .date-display-box {
      background-color: #fbedec;
      border: solid 1px #d9170e;
      font-size: 12px;
    }

    /*
      For the following inputs below, we are planning
      encode these changes in the actual foam files
    */
    ^ .foam-u2-CheckBox-label {
      margin-left: 12px;
    }

    ^ .foam-u2-view-RadioView label {
      margin-left: 12px;
    }

    ^ .foam-u2-TextField {
      width: 100%;
      padding: 10px 8px;
      font-size: 14px;
    }

    ^ .foam-u2-CheckBox {
    }

    ^ .foam-u2-view-date-DateTimePicker {
      cursor: pointer;
    }

    ^ .foam-u2-tag-Select {
      width: 100%;
      font-size: 14px;
      border: solid 1px #8e9090;
      border-radius: 3px;
      font-weight: 400;
      padding: 10px 8px;
      box-shadow: none;
      background: #ffffff url('images/dropdown-icon.svg') no-repeat 99% 50%;
      -webkit-appearance: none;
      cursor: pointer;
    }

    ^ .foam-u2-view-RichChoiceView {
      display: flex;
    }

    ^ .foam-u2-view-RichChoiceView-chevron {
      content: '▾';
      padding-left: 0px;
      font-size: 16px;
      color: #8D9090;
    }

    ^ .foam-u2-view-RichChoiceView-selection-view {
      padding: 2px 12px 0px 8px;
      width: 100%;
      border-radius: 3px;
      border: solid 1px #8e9090;
      background-color: #ffffff;
      box-sizing: border-box;
      -webkit-appearance: none;
      cursor: pointer;
      font-size: 14px;
    }

    ^ .foam-u2-view-RichChoiceView .search {
      padding: 8px 16px;
      font-size: 14px;
      border-bottom: 1px solid #f4f4f9;
    }

    ^ .foam-u2-detail-SectionedDetailPropertyView .property-filter {
      font-size: 14px;
      padding-left: 16px;
    }

    ^ .foam-u2-view-RichChoiceView .search input {
      border-bottom: none;
    }

    ^ .foam-u2-view-RichChoiceView .search img {
      top: 8px;
    }

    ^ .foam-u2-view-RichChoiceView-heading {
      border-bottom: 1px solid #f4f4f9;
      line-height: 24px;
      font-size: 14px;
      color: #333;
      font-weight: 900;
      padding: 6px 16px;
    }

    ^ .foam-u2-IntView {
      width: 100%;
      padding: 10px 8px;
      font-size: 14px;
    }

    ^ .DefaultRowView-row {
      background: white;
      padding: 8px 16px;
      font-size: 12px;
      color: #424242;
    }

    ^ .DefaultRowView-row:hover {
      background: #f4f4f9;
      cursor: pointer;
    }
  `,

  requires: [
    'foam.u2.layout.Rows',
    'foam.u2.layout.Row',
    'foam.u2.layout.Cols',
    'foam.u2.layout.Col'
  ],

  properties: [
    'prop',
  ],

  methods: [
    function initE() {
      var self = this;
      this.SUPER();
      this
        .show(this.prop.createVisibilityFor(this.data$)
          .map(function(m) {
            return m != foam.u2.Visibility.HIDDEN;
          }
        ))
        .addClass(this.myClass())
        .start(self.Rows, { defaultChildConfig: { padding: '8px 0'} })
          .add(this.slot(function(data, prop) {

            var errorSlot = prop.validateObj ?
              data.slot(prop.validateObj) :
              foam.core.ConstantSlot.create({ value: null });

            return self.E()
              .start(self.Rows, { defaultChildConfig:  { lineHeight: '2' } })
                .start().add(prop.label$).addClass(this.myClass('card-label')).end()
                .start(self.Cols, { contentJustification: foam.u2.layout.ContentJustification.START, defaultChildConfig: { margin: '0 16px 0 0' } })
                  .start(self.Col, { flex: 1 }).add(prop).enableClass(this.myClass('error'), errorSlot).end()
                  .callIf(prop.help, function() {
                    this.start({class: 'foam.u2.tag.Image', data: 'images/question-icon.svg'})
                      .addClass(this.myClass('helper-icon'))
                      .attrs({ title: prop.help })
                    .end();
                  })
                .end()
                .start(self.Cols, {
                  contentJustification: foam.u2.layout.ContentJustification.START,
                  itemAlignment: foam.u2.layout.ItemAlignment.CENTER, defaultChildConfig: { margin: '0 8px 0 0' }
                }).addClass(this.myClass('validation-container')).show(errorSlot)
                  .start({class: 'foam.u2.tag.Image', data: 'images/inline-error-icon.svg'})
                    .addClass(this.myClass('error-icon'))
                  .end()
                  .add(errorSlot.map((s) => {
                    return self.E().add(s);
                  }))
                .end()
              .end();
          }));
    }
  ]
});
<commit_msg>Added in the checkbox styling<commit_after>/**
 * @license
 * Copyright 2019 The FOAM Authors. All Rights Reserved.
 * http://www.apache.org/licenses/LICENSE-2.0
 */

foam.CLASS({
  package: 'foam.u2.detail',
  name: 'SectionedDetailPropertyView',
  extends: 'foam.u2.View',

  documentation: `
    View for one property of a SectionedDetailView.
  `,

  css: `
    ^card-label {
      font-size: 16px;
      font-weight: bold;
    }
    ^validation-container {
      margin-top: 4px;
      color: #d9170e;
    }
    ^helper-icon {
      width: 20px;
      height: 20px;
    }
    ^error-icon {
      width: 16px;
      height: 16px;
    }
    ^error .foam-u2-TextField {
      background-color: #fbedec;
      border: solid 1px #d9170e;
      font-size: 12px;
    }
    ^error .foam-u2-tag-TextArea {
      background-color: #fbedec;
      border: solid 1px #d9170e;
      font-size: 12px;
    }
    ^error .foam-u2-tag-Select {
      background-color: #fbedec;
      border: solid 1px #d9170e;
      font-size: 12px;
    }
    ^error .foam-u2-view-date-DateTimePicker .date-display-box {
      background-color: #fbedec;
      border: solid 1px #d9170e;
      font-size: 12px;
    }

    /*
      !IMPORTANT!
      For the following inputs below, we are planning
      encode these changes in the actual foam files
    */

    ^ .foam-u2-view-RadioView label {
      margin-left: 12px;
    }

    ^ .foam-u2-TextField {
      width: 100%;
      padding: 10px 8px;
      font-size: 14px;
    }

    ^ .foam-u2-view-date-DateTimePicker {
      cursor: pointer;
    }

    ^ .foam-u2-tag-Select {
      width: 100%;
      font-size: 14px;
      border: solid 1px #8e9090;
      border-radius: 3px;
      font-weight: 400;
      padding: 10px 8px;
      box-shadow: none;
      background: #ffffff url('images/dropdown-icon.svg') no-repeat 99% 50%;
      -webkit-appearance: none;
      cursor: pointer;
    }

    ^ .foam-u2-view-RichChoiceView {
      display: flex;
    }

    ^ .foam-u2-view-RichChoiceView-chevron {
      content: '▾';
      padding-left: 0px;
      font-size: 16px;
      color: #8D9090;
    }

    ^ .foam-u2-view-RichChoiceView-selection-view {
      padding: 2px 12px 0px 8px;
      width: 100%;
      border-radius: 3px;
      border: solid 1px #8e9090;
      background-color: #ffffff;
      box-sizing: border-box;
      -webkit-appearance: none;
      cursor: pointer;
      font-size: 14px;
    }

    ^ .foam-u2-view-RichChoiceView .search {
      padding: 8px 16px;
      font-size: 14px;
      border-bottom: 1px solid #f4f4f9;
    }

    ^ .foam-u2-detail-SectionedDetailPropertyView .property-filter {
      font-size: 14px;
      padding-left: 16px;
    }

    ^ .foam-u2-view-RichChoiceView .search input {
      border-bottom: none;
    }

    ^ .foam-u2-view-RichChoiceView .search img {
      top: 8px;
    }

    ^ .foam-u2-view-RichChoiceView-heading {
      border-bottom: 1px solid #f4f4f9;
      line-height: 24px;
      font-size: 14px;
      color: #333;
      font-weight: 900;
      padding: 6px 16px;
    }

    ^ .foam-u2-IntView {
      width: 100%;
      padding: 10px 8px;
      font-size: 14px;
    }

    ^ .DefaultRowView-row {
      background: white;
      padding: 8px 16px;
      font-size: 12px;
      color: #424242;
    }

    ^ .DefaultRowView-row:hover {
      background: #f4f4f9;
      cursor: pointer;
    }

    ^ .foam-u2-CheckBox {
      -webkit-appearance: none;
      border-radius: 2px;
      border: solid 1px #8e9090;
      box-sizing: border-box;
      display: inline-block;
      fill: rgba(0, 0, 0, 0);

      vertical-align: middle;

      height: 16px;
      width: 16px;

      opacity: 1;

      transition: background-color 140ms, border-color 140ms;
    }

    ^ .foam-u2-CheckBox-label {
      margin-left: 12px;
      vertical-align: middle;
    }

    ^ .foam-u2-CheckBox:checked {
      background-color: #604aff;
      fill: white;
      border: solid 1px #604aff;
    }

    ^ .foam-u2-CheckBox:checked:after {
      content: url(data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2048%2048%22%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2215%22%20height%3D%2215%22%20version%3D%221.1%22%3E%0A%20%20%20%3Cpath%20fill%3D%22white%22%20stroke-width%3D%223%22%20d%3D%22M18%2032.34L9.66%2024l-2.83%202.83L18%2038l24-24-2.83-2.83z%22/%3E%0A%3C/svg%3E);
    }
  `,

  requires: [
    'foam.u2.layout.Rows',
    'foam.u2.layout.Row',
    'foam.u2.layout.Cols',
    'foam.u2.layout.Col'
  ],

  properties: [
    'prop',
  ],

  methods: [
    function initE() {
      var self = this;
      this.SUPER();
      this
        .show(this.prop.createVisibilityFor(this.data$)
          .map(function(m) {
            return m != foam.u2.Visibility.HIDDEN;
          }
        ))
        .addClass(this.myClass())
        .start(self.Rows, { defaultChildConfig: { padding: '8px 0'} })
          .add(this.slot(function(data, prop) {

            var errorSlot = prop.validateObj ?
              data.slot(prop.validateObj) :
              foam.core.ConstantSlot.create({ value: null });

            return self.E()
              .start(self.Rows, { defaultChildConfig:  { lineHeight: '2' } })
                .start().add(prop.label$).addClass(this.myClass('card-label')).end()
                .start(self.Cols, { contentJustification: foam.u2.layout.ContentJustification.START, defaultChildConfig: { margin: '0 16px 0 0' } })
                  .start(self.Col, { flex: 1 }).add(prop).enableClass(this.myClass('error'), errorSlot).end()
                  .callIf(prop.help, function() {
                    this.start({class: 'foam.u2.tag.Image', data: 'images/question-icon.svg'})
                      .addClass(this.myClass('helper-icon'))
                      .attrs({ title: prop.help })
                    .end();
                  })
                .end()
                .start(self.Cols, {
                  contentJustification: foam.u2.layout.ContentJustification.START,
                  itemAlignment: foam.u2.layout.ItemAlignment.CENTER, defaultChildConfig: { margin: '0 8px 0 0' }
                }).addClass(this.myClass('validation-container')).show(errorSlot)
                  .start({class: 'foam.u2.tag.Image', data: 'images/inline-error-icon.svg'})
                    .addClass(this.myClass('error-icon'))
                  .end()
                  .add(errorSlot.map((s) => {
                    return self.E().add(s);
                  }))
                .end()
              .end();
          }));
    }
  ]
});
<|endoftext|>


<commit_before>var _ = require('lodash')
var add = require('./add')
var count = require('./count')

function courses(coursesTaken, creditsNeeded) {
	// Students must take the equivalent of 35 St. Olaf credits through a
	// combination of full-credit and fractional-credit courses.

	// Note: For numerical purposes, the term "course" means a full (1.00)
	// course credit, as distinguished from fractional course credits, unless
	// otherwise noted.

	var creditsTaken = _.reduce(_.pluck(coursesTaken, 'credits'), add)

	return creditsTaken >= creditsNeeded
}

function ensureLimitedOffCampusCoursesDuringFinalYear(student) {
	// Used by the residency checker to ensure that only the allowed amount of
	// off-campus courses are taken during the final year

	var finalYear = _.max(student.fabrications, 'year')
	var finalYearFabrications = _.filter(student.fabrications, {year: finalYear})
	var finalYearCourses = _.filter(student.courses, {year: finalYear})

	return (
		_.isEmpty(finalYearFabrications) ||
		_.reduce(finalYearFabrications, 'credits') <= 3
	)
}

function residency(student) {
	// Seventeen of the 35 St. Olaf credits required for graduation must be
	// taken through St. Olaf College. This may include St. Olaf-sponsored
	// off-campus programs.

	// The senior residency requirement consists of registration through St.
	// Olaf either (a) during the last two semesters and Interim preceding the
	// conferring of the degree or (b) for at least nine of the last 12 full-
	// course credits. For instance, seniors may spend one of their last terms
	// at another institution only if a maximum of three credits are applied
	// to the St. Olaf degree. A senior may spend the final semester on a St.
	// Olaf-sponsored off-campus program. See, however, PARTICIPATION IN
	// COMMENCEMENT.

	// `fabrications` is a list of courses that the student has manually
	// entered, and which did not come from the course catalog. I'm using it
	// as a list of off-campus courses.

	return (
		_.size(student.courses) - _.size(student.fabrications) >= 17 &&
		ensureLimitedOffCampusCoursesDuringFinalYear(student)
	)
}

function interim(courses) {
	// At least three of the required 35 St. Olaf credits must be earned in
	// three separate January full-credit (1.0) Interims. An Interim may be
	// taken on campus, through a St. Olaf off-campus Interim program, or
	// through an approved Interim exchange with another 4-1-4 college.
	// Transfer students admitted with at least sophomore standing must
	// complete two Interims. After having successfully completed two
	// Interims, senior participators may satisfy the third Interim
	// requirement by means of a summer course taken during a St. Olaf summer
	// session after the commencement in which the student participates.

	return _.size(_.filter(courses, {term: 2})) >= 3
}

function gpa(student) {
	// An average grade of C (2.00 on a 4.00 system) for all courses taken for
	// the usual letter grades. See GRADE POINT AVERAGE for details.

	// NOTE: Currently, we don't store grades for courses, so we'll just
	// assume the best.

	return true
}

function onlyTwoHundredLevelCourses(courses) {
	return _.filter(courses, function(course) {
		return course.level >= 200
	})
}

function courseLevel(courses) {
	// Satisfactory completion of 18 credits above level I, courses numbered
	// 200 and higher.

	return _.size(onlyTwoHundredLevelCourses(courses)) >= 18
}

function gradedCourses(student) {
	// A minimum of 24 credits out of 35 must be taken graded through courses
	// taken from a St. Olaf professor.

	// A sliding scale is used if not all work is taken at St. Olaf or if
	// credit is earned through special programs such as Advanced Placement.
	// Sophomore transfer students need a minimum of 18 graded credits and
	// junior transfer students a minimum of 13 graded credits. Details are
	// available from the registrar.

	// All students should be aware of the restrictions on S/U and P/N grades.

	// Students studying on St. Olaf off-campus programs should refer to
	// GRADED AND UNGRADED COURSES in the Academic Regulations section of this
	// catalog for details about how this graduation requirement is affected
	// by off-campus programs.

	// From the [S/U] section: [quote]
	// The intent of the S/U option is to allow students to explore areas in
	// which they have an interest, without the grade counting in their grade
	// point average.

	// All graded courses are open to the S/U option as distinguished from P/N
	// (pass/no pass) courses where the ungraded option is mandatory. Neither
	// an S nor a U figure into the grade point average. An S is awarded for a
	// grade of C- or higher; a U is assigned for any grade of D+ or lower. A
	// student earning a grade of D+ to F in a course taken S/U receives a U,
	// which carries no course credit. A U cannot be assigned another type of
	// grade at a later date.

	// Students should exercise caution in choosing to take courses on an S/U
	// basis. Some graduate schools, for example, assume that a grade of S
	// replaces a C or C-.
	// [/quote]

	// NOTE: Because we don't store grades, we canot verify them. Same goes
	// for S/U courses.

	return _.size(student.courses) - _.size(student.fabrications) >= 24
}

function major(studies, courses) {
	// One completed major is required for graduation. Depending on the
	// department or interdisciplinary program, the number of courses required
	// for a major ranges from eight to twelve courses, with some departments
	// requiring course work in other departments. Departments and
	// interdisciplinary programs may require comprehensive examinations or
	// special research projects. (See Majors, Concentrations, and Areas of
	// Emphasis.) Consult the department or interdisciplinary program listing
	// in this catalog for specific major requirements. Students must declare
	// one major no later than the time of registration for Interim and spring
	// semester of their junior year by filling out a form available at the
	// Office of the Registrar and Academic Advising. Students are allowed to
	// change this declaration or add a second major at a later date. Only
	// this catalog defines the specific requirements for each departmental or
	// interdisciplinary major.

	// Other regulations are:

	// Of the credits counting toward the minimum requirements for a major, a
	// total of six (6.00) must be completed with a grade of C or higher.
	// NOTE: Ignored, because we don't store grades.

	// Only one full-course equivalent (1.00-credit course) taken S/U may
	// count toward the minimum requirements for a major. Some departments
	// have more stringent regulations.
	// NOTE: Ignored, because we don't store s/u status.

	// At least 50 percent of the minimum major must be taken through St.
	// Olaf. Students should consult the registrar and the appropriate
	// department chair or program director about counting toward a major
	// courses taken at other colleges. In addition to the registrar, the
	// chair must sign the student’s transfer of credit form (available from
	// the Office of the Registrar and Academic Advising) if work from other
	// institutions is accepted in advance. Likewise, courses taken through
	// St. Olaf off-campus programs must be approved by the chair or director
	// and the off-campus program advisor in advance if credit toward a major
	// is sought.
	// NOTE: Ignored, because this doesn't know about major requirements.

	return _.size(_.filter(studies, {type: 'major'}) >= 1
}

var hasDepartment = _.curry(function(dept, course) {
	// _.curry allows the function to be called with arguments multiple times.
	return _.contains(course.depts, dept)
})

var twentyOneCreditsAndBeyond = _.curry(function(courses, major) {
	// Takes the courses *outside* of the major department, and counts them.

	// _.curry allows the function to be called with arguments multiple times.
	// also, remember: reject !== filter.
	return _.size(_.reject(courses, hasDepartment(major.abbr))) >= 21
})

function beyondTheMajor(studies, courses) {
	// While the maximum course credits counting toward a major in any one
	// department may vary, 21 total full-course credits must be completed
	// outside of the SIS "department" code of the major. The 21 total credits
	// include Education Department courses attending the major. In order for
	// a student to be certified in a second or third major, 21 credits also
	// must be taken outside of the SIS "department" code of each of those
	// majors as well. If a student has a double major, courses taken in one
	// major count toward the 21 credits outside of the other major. Credits
	// outside the major department or program include full- (1.00) credit
	// courses plus partial - (.25, .50, .75) credit courses.

	return _.every(_.filter(studies, {type: 'major'}), twentyOneCreditsAndBeyond(courses))
}

function finalTwoYearsInResidence(student) {
	// "The final two years of coursework in pursuit of the degrees must be
	// spent in residence."

	var years = _.uniq(_.pluck(student.fabrications, 'year'))

	if (_.size(years) >= 2) {
		var sortedYears = _.sortBy(years).reverse()
		var finalYear = years[0]
		var secondFinalYear = years[1]
		var finalYearFabrications = _.filter(student.fabrications, {year: finalYear})
		var secondFinalYearFabrications = _.filter(student.fabrications, {year: finalYear})

		if ( !_.every(
			[_.isEmpty(finalYearFabrications), _.isEmpty(secondFinalYearFabrications)]
		) ) {
			return false
		}

		return true
	}

	return false
}

function onlyFullCreditCourses(course) {
	return course.credits >= 1.0
}

function seventeenOlafCourses(student) {
	// "17 of the last 20 full-course credits must be earned through St. Olaf."
	if (_.size(student.courses) < 20) {
		return false
	}

	var fullCreditCourses = _.filter(student.courses, onlyFullCreditCourses)

	// Put the most recent courses at the front
	var sortedFullCreditCourses = _.sortBy(fullCreditCourses, 'term').reverse()

	// Get just the most recent 20 courses
	var lastTwentyFullCreditCourses = _.first(sortedFullCreditCourses, 20)

	// Reject all of the fabricated courses
	var notFabricatedFullCreditCourses = _.reject(lastTwentyFullCreditCourses, { alteration: 'fabricated' })

	if (_.size(notFabricatedFullCreditCourses) < 17) {
		return false
	}

	return true
}

function artsAndMusicDoubleMajor(student) {
	// 	Students must meet the application requirements for both the Bachelor
	// 	of Arts and Bachelor of Music degree programs.

	// [...]

	// Students pursuing the double-degree option may choose any of the five
	// Bachelor of Music graduation majors (Church Music, Elective Studies, Music
	// Education, Performance, and Theory/Composition) and any of the Bachelor of
	// Arts graduation majors except Music.

	// Students shall meet the requirements for the B.A. graduation major and B.M.
	// graduation major, as well as the general education requirements and general
	// graduation requirements for the two degrees. The final two years of
	// coursework in pursuit of the degrees must be spent in residence. 17 of the
	// last 20 full-course credits must be earned through St. Olaf. Some students
	// may require up to 43 total credits in order to complete all requirements
	// for both degrees.

	// [...]

	// A double-degree student may elect to graduate in two different semesters,
	// one with each degree, but must meet all requirements for the degree and the
	// graduation major within that degree before the diploma for that degree will
	// be awarded.

	var degrees = _.filter(studies, {type: 'degree'})
	if (_.size(degrees) === 1) {
		// there's only one degree, so we don't care.
		return true
	}

	var majors = _.filter(studies, {type: 'major'})
	if (_.find(degrees, {abbr: 'B.A.'}) && _.find(majors, {abbr: 'MUSIC'})) {
		// there's a double-ba-bm trying to major in Music -- no.
		return false
	}

	// "The final two years of coursework in pursuit of the degrees must be
	// spent in residence."
	if (!finalTwoYearsInResidence(student)) {
		return false
	}

	// "17 of the last 20 full-course credits must be earned through St. Olaf."
	if (!seventeenOlafCourses(student)) {
		return false
	}

	return true
}

module.exports = function(student) {
	// Requirements taken from
	// http://www.stolaf.edu/catalog/1314/academiclife/ba-gen-grad-requirements.html
	var requirements = {
		courses: courses(student.courses, student.creditsNeeded),
		residency: residency(student),
		interim: interim(student.courses),
		gpa: gpa(student),
		courseLevel: courseLevel(student.courses),
		gradedCourses: gradedCourses(student),
		major: major(student.studies, student.courses),
		beyondTheMajor: beyondTheMajor(student.studies, student.courses),
		artsAndMusicDoubleMajor: artsAndMusicDoubleMajor(student)
	}
	return _.every(requirements)
}
<commit_msg>Allow the student to fulfill the alternative satisfaction for interim courses<commit_after>var _ = require('lodash')
var add = require('./add')
var count = require('./count')

function courses(coursesTaken, creditsNeeded) {
	// Students must take the equivalent of 35 St. Olaf credits through a
	// combination of full-credit and fractional-credit courses.

	// Note: For numerical purposes, the term "course" means a full (1.00)
	// course credit, as distinguished from fractional course credits, unless
	// otherwise noted.

	var creditsTaken = _.reduce(_.pluck(coursesTaken, 'credits'), add)

	return creditsTaken >= creditsNeeded
}

function ensureLimitedOffCampusCoursesDuringFinalYear(student) {
	// Used by the residency checker to ensure that only the allowed amount of
	// off-campus courses are taken during the final year

	var finalYear = _.max(student.fabrications, 'year')
	var finalYearFabrications = _.filter(student.fabrications, {year: finalYear})
	var finalYearCourses = _.filter(student.courses, {year: finalYear})

	return (
		_.isEmpty(finalYearFabrications) ||
		_.reduce(finalYearFabrications, 'credits') <= 3
	)
}

function residency(student) {
	// Seventeen of the 35 St. Olaf credits required for graduation must be
	// taken through St. Olaf College. This may include St. Olaf-sponsored
	// off-campus programs.

	// The senior residency requirement consists of registration through St.
	// Olaf either (a) during the last two semesters and Interim preceding the
	// conferring of the degree or (b) for at least nine of the last 12 full-
	// course credits. For instance, seniors may spend one of their last terms
	// at another institution only if a maximum of three credits are applied
	// to the St. Olaf degree. A senior may spend the final semester on a St.
	// Olaf-sponsored off-campus program. See, however, PARTICIPATION IN
	// COMMENCEMENT.

	// `fabrications` is a list of courses that the student has manually
	// entered, and which did not come from the course catalog. I'm using it
	// as a list of off-campus courses.

	return (
		_.size(student.courses) - _.size(student.fabrications) >= 17 &&
		ensureLimitedOffCampusCoursesDuringFinalYear(student)
	)
}

function onlyFullCreditCourses(course) {
	return course.credits >= 1.0
}

function onlyFullCreditInterimCourses(course) {
	return (course.semester === 2 && onlyFullCreditCourses(course))
}

function onlySummerSessionCourses(course) {
	return course.semester === 4 || course.semester === 5
}

function onlyFullCreditSummerSessionCourses(course) {
	return (onlySummerSessionCourses(course) && onlyFullCreditCourses(course))
}

function interim(courses) {
	// At least three of the required 35 St. Olaf credits must be earned in
	// three separate January full-credit (1.0) Interims. An Interim may be
	// taken on campus, through a St. Olaf off-campus Interim program, or
	// through an approved Interim exchange with another 4-1-4 college.
	// Transfer students admitted with at least sophomore standing must
	// complete two Interims. After having successfully completed two
	// Interims, senior participators may satisfy the third Interim
	// requirement by means of a summer course taken during a St. Olaf summer
	// session after the commencement in which the student participates.

	var interimCourses = _.filter(courses, onlyFullCreditInterimCourses)
	var interimCourseCount = _.size(interimCourses)

	// "After having successfully completed two Interims, senior participators
	// may satisfy the third Interim requirement by means of a summer course
	// taken during a St. Olaf summer session after the commencement in which
	// the student participates."
	var years = _.uniq(_.pluck(student.fabrications, 'year'))
	var finalYear = _.max(years)

	var summerSessionCourses = _.filter(courses, onlyFullCreditSummerSessionCourses)
	var finalSummerSessionCourses = _.filter(summerSessionCourses, {year: finalYear})
	var finalSummerSessionCourseCount = _.size(finalSummerSessionCourses)

	return (
		interimCourseCount >= 3 ||
		(interimCourseCount >= 2 && finalSummerSessionCourseCount >= 1)
	)
}

function gpa(student) {
	// An average grade of C (2.00 on a 4.00 system) for all courses taken for
	// the usual letter grades. See GRADE POINT AVERAGE for details.

	// NOTE: Currently, we don't store grades for courses, so we'll just
	// assume the best.

	return true
}

function onlyTwoHundredLevelCourses(courses) {
	return _.filter(courses, function(course) {
		return course.level >= 200
	})
}

function courseLevel(courses) {
	// Satisfactory completion of 18 credits above level I, courses numbered
	// 200 and higher.

	return _.size(onlyTwoHundredLevelCourses(courses)) >= 18
}

function gradedCourses(student) {
	// A minimum of 24 credits out of 35 must be taken graded through courses
	// taken from a St. Olaf professor.

	// A sliding scale is used if not all work is taken at St. Olaf or if
	// credit is earned through special programs such as Advanced Placement.
	// Sophomore transfer students need a minimum of 18 graded credits and
	// junior transfer students a minimum of 13 graded credits. Details are
	// available from the registrar.

	// All students should be aware of the restrictions on S/U and P/N grades.

	// Students studying on St. Olaf off-campus programs should refer to
	// GRADED AND UNGRADED COURSES in the Academic Regulations section of this
	// catalog for details about how this graduation requirement is affected
	// by off-campus programs.

	// From the [S/U] section: [quote]
	// The intent of the S/U option is to allow students to explore areas in
	// which they have an interest, without the grade counting in their grade
	// point average.

	// All graded courses are open to the S/U option as distinguished from P/N
	// (pass/no pass) courses where the ungraded option is mandatory. Neither
	// an S nor a U figure into the grade point average. An S is awarded for a
	// grade of C- or higher; a U is assigned for any grade of D+ or lower. A
	// student earning a grade of D+ to F in a course taken S/U receives a U,
	// which carries no course credit. A U cannot be assigned another type of
	// grade at a later date.

	// Students should exercise caution in choosing to take courses on an S/U
	// basis. Some graduate schools, for example, assume that a grade of S
	// replaces a C or C-.
	// [/quote]

	// NOTE: Because we don't store grades, we canot verify them. Same goes
	// for S/U courses.

	return _.size(student.courses) - _.size(student.fabrications) >= 24
}

function major(studies, courses) {
	// One completed major is required for graduation. Depending on the
	// department or interdisciplinary program, the number of courses required
	// for a major ranges from eight to twelve courses, with some departments
	// requiring course work in other departments. Departments and
	// interdisciplinary programs may require comprehensive examinations or
	// special research projects. (See Majors, Concentrations, and Areas of
	// Emphasis.) Consult the department or interdisciplinary program listing
	// in this catalog for specific major requirements. Students must declare
	// one major no later than the time of registration for Interim and spring
	// semester of their junior year by filling out a form available at the
	// Office of the Registrar and Academic Advising. Students are allowed to
	// change this declaration or add a second major at a later date. Only
	// this catalog defines the specific requirements for each departmental or
	// interdisciplinary major.

	// Other regulations are:

	// Of the credits counting toward the minimum requirements for a major, a
	// total of six (6.00) must be completed with a grade of C or higher.
	// NOTE: Ignored, because we don't store grades.

	// Only one full-course equivalent (1.00-credit course) taken S/U may
	// count toward the minimum requirements for a major. Some departments
	// have more stringent regulations.
	// NOTE: Ignored, because we don't store s/u status.

	// At least 50 percent of the minimum major must be taken through St.
	// Olaf. Students should consult the registrar and the appropriate
	// department chair or program director about counting toward a major
	// courses taken at other colleges. In addition to the registrar, the
	// chair must sign the student’s transfer of credit form (available from
	// the Office of the Registrar and Academic Advising) if work from other
	// institutions is accepted in advance. Likewise, courses taken through
	// St. Olaf off-campus programs must be approved by the chair or director
	// and the off-campus program advisor in advance if credit toward a major
	// is sought.
	// NOTE: Ignored, because this doesn't know about major requirements.

	return _.size(_.filter(studies, {type: 'major'}) >= 1
}

var hasDepartment = _.curry(function(dept, course) {
	// _.curry allows the function to be called with arguments multiple times.
	return _.contains(course.depts, dept)
})

var twentyOneCreditsAndBeyond = _.curry(function(courses, major) {
	// Takes the courses *outside* of the major department, and counts them.

	// _.curry allows the function to be called with arguments multiple times.
	// also, remember: reject !== filter.
	return _.size(_.reject(courses, hasDepartment(major.abbr))) >= 21
})

function beyondTheMajor(studies, courses) {
	// While the maximum course credits counting toward a major in any one
	// department may vary, 21 total full-course credits must be completed
	// outside of the SIS "department" code of the major. The 21 total credits
	// include Education Department courses attending the major. In order for
	// a student to be certified in a second or third major, 21 credits also
	// must be taken outside of the SIS "department" code of each of those
	// majors as well. If a student has a double major, courses taken in one
	// major count toward the 21 credits outside of the other major. Credits
	// outside the major department or program include full- (1.00) credit
	// courses plus partial - (.25, .50, .75) credit courses.

	return _.every(_.filter(studies, {type: 'major'}), twentyOneCreditsAndBeyond(courses))
}

function finalTwoYearsInResidence(student) {
	// "The final two years of coursework in pursuit of the degrees must be
	// spent in residence."

	var years = _.uniq(_.pluck(student.fabrications, 'year'))

	if (_.size(years) >= 2) {
		var sortedYears = _.sortBy(years).reverse()
		var finalYear = years[0]
		var secondFinalYear = years[1]
		var finalYearFabrications = _.filter(student.fabrications, {year: finalYear})
		var secondFinalYearFabrications = _.filter(student.fabrications, {year: finalYear})

		if ( !_.every(
			[_.isEmpty(finalYearFabrications), _.isEmpty(secondFinalYearFabrications)]
		) ) {
			return false
		}

		return true
	}

	return false
}

function seventeenOlafCourses(student) {
	// "17 of the last 20 full-course credits must be earned through St. Olaf."
	if (_.size(student.courses) < 20) {
		return false
	}

	var fullCreditCourses = _.filter(student.courses, onlyFullCreditCourses)

	// Put the most recent courses at the front
	var sortedFullCreditCourses = _.sortBy(fullCreditCourses, 'term').reverse()

	// Get just the most recent 20 courses
	var lastTwentyFullCreditCourses = _.first(sortedFullCreditCourses, 20)

	// Reject all of the fabricated courses
	var notFabricatedFullCreditCourses = _.reject(lastTwentyFullCreditCourses, { alteration: 'fabricated' })

	if (_.size(notFabricatedFullCreditCourses) < 17) {
		return false
	}

	return true
}

function artsAndMusicDoubleMajor(student) {
	// 	Students must meet the application requirements for both the Bachelor
	// 	of Arts and Bachelor of Music degree programs.

	// [...]

	// Students pursuing the double-degree option may choose any of the five
	// Bachelor of Music graduation majors (Church Music, Elective Studies, Music
	// Education, Performance, and Theory/Composition) and any of the Bachelor of
	// Arts graduation majors except Music.

	// Students shall meet the requirements for the B.A. graduation major and B.M.
	// graduation major, as well as the general education requirements and general
	// graduation requirements for the two degrees. The final two years of
	// coursework in pursuit of the degrees must be spent in residence. 17 of the
	// last 20 full-course credits must be earned through St. Olaf. Some students
	// may require up to 43 total credits in order to complete all requirements
	// for both degrees.

	// [...]

	// A double-degree student may elect to graduate in two different semesters,
	// one with each degree, but must meet all requirements for the degree and the
	// graduation major within that degree before the diploma for that degree will
	// be awarded.

	var degrees = _.filter(studies, {type: 'degree'})
	if (_.size(degrees) === 1) {
		// there's only one degree, so we don't care.
		return true
	}

	var majors = _.filter(studies, {type: 'major'})
	if (_.find(degrees, {abbr: 'B.A.'}) && _.find(majors, {abbr: 'MUSIC'})) {
		// there's a double-ba-bm trying to major in Music -- no.
		return false
	}

	// "The final two years of coursework in pursuit of the degrees must be
	// spent in residence."
	if (!finalTwoYearsInResidence(student)) {
		return false
	}

	// "17 of the last 20 full-course credits must be earned through St. Olaf."
	if (!seventeenOlafCourses(student)) {
		return false
	}

	return true
}

module.exports = function(student) {
	// Requirements taken from
	// http://www.stolaf.edu/catalog/1314/academiclife/ba-gen-grad-requirements.html
	var requirements = {
		courses: courses(student.courses, student.creditsNeeded),
		residency: residency(student),
		interim: interim(student.courses),
		gpa: gpa(student),
		courseLevel: courseLevel(student.courses),
		gradedCourses: gradedCourses(student),
		major: major(student.studies, student.courses),
		beyondTheMajor: beyondTheMajor(student.studies, student.courses),
		artsAndMusicDoubleMajor: artsAndMusicDoubleMajor(student)
	}
	return _.every(requirements)
}
<|endoftext|>


<commit_before>'use strict';

const http = require('http');
const https = require('https');
const App = require('./app').App;

http.globalAgent.maxSockets = 2;
https.globalAgent.maxSockets = 2;

const app = new App();
app.run();
<commit_msg>Increase http socket numbers.<commit_after>'use strict';

const http = require('http');
const https = require('https');
const App = require('./app').App;

http.globalAgent.maxSockets = 6;
https.globalAgent.maxSockets = 6;

const app = new App();
app.run();
<|endoftext|>


<commit_before>var expect = require('chai').expect;
var cytoscape = require('../src/test.js', cytoscape);

describe('Algorithms', function(){
  describe('eles.aStar()', function(){

    var cy;
    var nodes;
    var edges;

    beforeEach(function(done) {
      cytoscape({
        elements: {
          nodes: [
            { data: { id: '1-1'} },
            { data: { id: '1-2'} },
            { data: { id: '1-3'} },
            { data: { id: '1-4'} },
            { data: { id: '1-5'} },
            { data: { id: '1-6'} },

            { data: { id: '2-1'} },
            { data: { id: '2-2'} },
            { data: { id: '2-3'} },
            { data: { id: '2-4'} },
            { data: { id: '2-5'} },
            { data: { id: '2-6'} },

            { data: { id: '3-1'} },
            { data: { id: '3-2'} },
            { data: { id: '3-3'} },
            { data: { id: '3-4'} },
            { data: { id: '3-5'} },
            { data: { id: '3-6'} },

            { data: { id: '4-1'} },
            { data: { id: '4-2'} },
            { data: { id: '4-3'} },
            { data: { id: '4-4'} },
            { data: { id: '4-5'} },
            { data: { id: '4-6'} },

            { data: { id: '5-1'} },
            { data: { id: '5-2'} },
            { data: { id: '5-3'} },
            { data: { id: '5-4'} },
            { data: { id: '5-5'} },
            { data: { id: '5-6'} },

            { data: { id: '6-1'} },
            { data: { id: '6-2'} },
            { data: { id: '6-3'} },
            { data: { id: '6-4'} },
            { data: { id: '6-5'} },
            { data: { id: '6-6'} }
          ],

          edges: [
            { data: { source: '6-1', target: '5-1', weight: 1.0 } },
            { data: { source: '5-1', target: '4-1', weight: 1.0 } },
            { data: { source: '4-1', target: '3-1', weight: 1.0 } },
            { data: { source: '3-1', target: '2-1', weight: 1.0 } },
            { data: { source: '2-1', target: '1-1', weight: 1.0 } },
            { data: { source: '6-2', target: '5-2', weight: 1.0 } },
            { data: { source: '5-2', target: '4-2', weight: 1.0 } },
            { data: { source: '6-3', target: '5-3', weight: 1.0 } },
            { data: { source: '5-3', target: '4-3', weight: 1.0 } },
            { data: { source: '4-3', target: '3-3', weight: 1.0 } },
            { data: { source: '4-4', target: '3-4', weight: 1.0 } },
            { data: { source: '6-6', target: '5-6', weight: 1.0 } },
            { data: { source: '5-6', target: '4-6', weight: 1.0 } },
            { data: { source: '4-6', target: '3-6', weight: 1.0 } },
            { data: { source: '3-6', target: '2-6', weight: 1.0 } },
            { data: { source: '2-6', target: '1-6', weight: 1.0 } },

            { data: { source: '6-1', target: '6-2', weight: 1.0 } },
            { data: { source: '6-2', target: '6-3', weight: 1.0 } },
            { data: { source: '6-3', target: '6-4', weight: 1.0 } },
            { data: { source: '6-4', target: '6-5', weight: 1.0 } },
            { data: { source: '6-5', target: '6-6', weight: 1.0 } },
            { data: { source: '5-1', target: '5-2', weight: 1.0 } },
            { data: { source: '5-2', target: '5-3', weight: 1.0 } },
            { data: { source: '4-1', target: '4-2', weight: 1.0 } },
            { data: { source: '4-2', target: '4-3', weight: 1.0 } },
            { data: { source: '4-3', target: '4-4', weight: 1.0 } },
            { data: { source: '3-3', target: '3-4', weight: 1.0 } },
            { data: { source: '1-1', target: '1-2', weight: 1.0 } },
            { data: { source: '1-2', target: '1-3', weight: 1.0 } },
            { data: { source: '1-3', target: '1-4', weight: 1.0 } },
            { data: { source: '1-4', target: '1-5', weight: 1.0 } },
            { data: { source: '1-5', target: '1-6', weight: 1.0 } },
          ]
        },

        ready: function(){
          cy = this;

          // nodes
          nodes = ["dummy"];
          for (var i = 1; i <= 6; i++) {
            var tempArray = ["dummy"];
            for (var j = 1; j <= 6; j++) {
              tempArray.push(cy.$('#' + i + '-' + j));
            }
            nodes.push(tempArray);
          }

          done();
        }
      });
    });

    function eles() {
      var col = cy.collection();

      for (var i = 0; i < arguments.length; i++) {
        var ele = arguments[i];
        col = col.add(ele);
      }

      return col;
    };

    // Computes euclidean distance between 2 nodes
    function euclid(node1, node2) {
      var n1 = node1.id().split("-");
      var n2 = node2.id().split("-");
      return Math.sqrt( Math.pow(n1[0] - n2[0], 2) + Math.pow(n1[1] - n2[1], 2));
    };

    function ele2id( ele ){
      return ele.id();
    }

    function isNode( ele ){
      return ele.isNode();
    }

    it('eles.aStar(): directed, null heuristic, unweighted', function(){
      var options = {root: nodes[6][1],
               goal: nodes[1][5],
               directed: true,
               heuristic: function(a){return 0;}
              };
      var res = cy.elements().aStar(options);
      expect(res.found).to.equal(true);
      expect(res.distance).to.equal(9);
      expect(res.path.stdFilter(isNode).map(ele2id)).to.deep.equal(["6-1", "5-1", "4-1", "3-1", "2-1", "1-1", "1-2", "1-3", "1-4", "1-5"]);
    });

    it('eles.aStar(): directed, heuristic, unweighted', function(){
      var options = {root: nodes[6][1],
               goal: nodes[2][6],
               directed: true,
               heuristic: function(node) {return euclid(node, nodes[2][6]);}
              };
      var res = cy.elements().aStar(options);
      expect(res.found).to.equal(true);
      expect(res.distance).to.equal(9);
      expect(res.path.stdFilter(isNode).map(ele2id)).to.deep.equal(["6-1", "6-2", "6-3", "6-4", "6-5", "6-6", "5-6", "4-6", "3-6", "2-6"]);
    });

    it('eles.aStar(): undirected, heuristic vs null heuristic (performance), ', function(){
      var options1 = {root: nodes[3][3],
              goal: nodes[1][5],
              directed: false,
              heuristic: function(node) {return euclid(node, nodes[1][5]);}
              };
      var res1 = cy.elements().aStar(options1);
      expect(res1.found).to.equal(true);
      expect(res1.distance).to.equal(10);
      expect(res1.path.stdFilter(isNode).map(ele2id)).to.deep.equal(["3-3", "4-3", "4-2", "4-1", "3-1", "2-1", "1-1", "1-2", "1-3", "1-4", "1-5"]);

      var options2 = {root: nodes[3][3],
              goal: nodes[1][5],
              directed: false,
              heuristic: function(a){return 0;}
              };
      var res2 = cy.elements().aStar(options2);
      expect(res2.found).to.equal(true);
      expect(res2.distance).to.equal(10);
      expect(res2.path.stdFilter(isNode).map(ele2id)).to.deep.equal(["3-3", "4-3", "4-2", "4-1", "3-1", "2-1", "1-1", "1-2", "1-3", "1-4", "1-5"]);

      // Performance with heuristic should be better than without it
      expect(res1.steps).to.be.below(res2.steps);
    });

    function isContinuous(edges) {
      for (var i = 0; i < edges.length -1; ++i) {
        var edge = edges[i];
        var nextEdge = edges[i+1];
        if (! areConnected(edge, nextEdge)) {
          return false;
        }
      }
      return true;
    }

    function areConnected(edgeA, edgeB) {
      return edgeA.target() === edgeB.source() ||
             edgeA.source() === edgeB.target() ||
             edgeA.target() === edgeB.target() ||
             edgeA.source() === edgeB.source();
    }

    it('eles.aStar(): edges must form a continuous path', function(){
      var options = {root: nodes[6][1],
                      goal: nodes[2][6],
                      directed: true,
                      heuristic: function(node) {return euclid(node, nodes[2][6]);}
                    };
      var res = cy.elements().aStar(options);
      expect(isContinuous(res.path.edges())).to.equal(true);
    });

    function source(edge) {
      return edge.source();
    }

    function target(edge) {
      return edge.target();
    }

    it('eles.aStar(): the set of nodes touched by path.edges must equal the set of path.nodes', function(){
      let options = {root: nodes[6][1],
                      goal: nodes[2][6],
                      directed: true,
                      heuristic: function(node) {return euclid(node, nodes[2][6]);}
                    };
      let res = cy.elements().aStar(options);
      let sourcesAndTargetsOfEdges = new Set(res.path.edges().toArray().map(source).concat(res.path.edges().toArray().map(target)).map(ele2id));
      let setOfNodes = new Set(res.path.nodes().toArray().map(ele2id));
      expect(sourcesAndTargetsOfEdges).to.have.keys([...setOfNodes]);
    });
  });
});
<commit_msg>Add test for A*-Dijkstra consistency #2880<commit_after>var expect = require('chai').expect;
var cytoscape = require('../src/test.js', cytoscape);

describe('Algorithms', function(){
  describe('eles.aStar()', function(){

    var cy;
    var nodes;
    var edges;

    beforeEach(function(done) {
      cytoscape({
        elements: {
          nodes: [
            { data: { id: '1-1'} },
            { data: { id: '1-2'} },
            { data: { id: '1-3'} },
            { data: { id: '1-4'} },
            { data: { id: '1-5'} },
            { data: { id: '1-6'} },

            { data: { id: '2-1'} },
            { data: { id: '2-2'} },
            { data: { id: '2-3'} },
            { data: { id: '2-4'} },
            { data: { id: '2-5'} },
            { data: { id: '2-6'} },

            { data: { id: '3-1'} },
            { data: { id: '3-2'} },
            { data: { id: '3-3'} },
            { data: { id: '3-4'} },
            { data: { id: '3-5'} },
            { data: { id: '3-6'} },

            { data: { id: '4-1'} },
            { data: { id: '4-2'} },
            { data: { id: '4-3'} },
            { data: { id: '4-4'} },
            { data: { id: '4-5'} },
            { data: { id: '4-6'} },

            { data: { id: '5-1'} },
            { data: { id: '5-2'} },
            { data: { id: '5-3'} },
            { data: { id: '5-4'} },
            { data: { id: '5-5'} },
            { data: { id: '5-6'} },

            { data: { id: '6-1'} },
            { data: { id: '6-2'} },
            { data: { id: '6-3'} },
            { data: { id: '6-4'} },
            { data: { id: '6-5'} },
            { data: { id: '6-6'} }
          ],

          edges: [
            { data: { source: '6-1', target: '5-1', weight: 1.0 } },
            { data: { source: '5-1', target: '4-1', weight: 1.0 } },
            { data: { source: '4-1', target: '3-1', weight: 1.0 } },
            { data: { source: '3-1', target: '2-1', weight: 1.0 } },
            { data: { source: '2-1', target: '1-1', weight: 1.0 } },
            { data: { source: '6-2', target: '5-2', weight: 1.0 } },
            { data: { source: '5-2', target: '4-2', weight: 1.0 } },
            { data: { source: '6-3', target: '5-3', weight: 1.0 } },
            { data: { source: '5-3', target: '4-3', weight: 1.0 } },
            { data: { source: '4-3', target: '3-3', weight: 1.0 } },
            { data: { source: '4-4', target: '3-4', weight: 1.0 } },
            { data: { source: '6-6', target: '5-6', weight: 1.0 } },
            { data: { source: '5-6', target: '4-6', weight: 1.0 } },
            { data: { source: '4-6', target: '3-6', weight: 1.0 } },
            { data: { source: '3-6', target: '2-6', weight: 1.0 } },
            { data: { source: '2-6', target: '1-6', weight: 1.0 } },

            { data: { source: '6-1', target: '6-2', weight: 1.0 } },
            { data: { source: '6-2', target: '6-3', weight: 1.0 } },
            { data: { source: '6-3', target: '6-4', weight: 1.0 } },
            { data: { source: '6-4', target: '6-5', weight: 1.0 } },
            { data: { source: '6-5', target: '6-6', weight: 1.0 } },
            { data: { source: '5-1', target: '5-2', weight: 1.0 } },
            { data: { source: '5-2', target: '5-3', weight: 1.0 } },
            { data: { source: '4-1', target: '4-2', weight: 1.0 } },
            { data: { source: '4-2', target: '4-3', weight: 1.0 } },
            { data: { source: '4-3', target: '4-4', weight: 1.0 } },
            { data: { source: '3-3', target: '3-4', weight: 1.0 } },
            { data: { source: '1-1', target: '1-2', weight: 1.0 } },
            { data: { source: '1-2', target: '1-3', weight: 1.0 } },
            { data: { source: '1-3', target: '1-4', weight: 1.0 } },
            { data: { source: '1-4', target: '1-5', weight: 1.0 } },
            { data: { source: '1-5', target: '1-6', weight: 1.0 } },
          ]
        },

        ready: function(){
          cy = this;

          // nodes
          nodes = ["dummy"];
          for (var i = 1; i <= 6; i++) {
            var tempArray = ["dummy"];
            for (var j = 1; j <= 6; j++) {
              tempArray.push(cy.$('#' + i + '-' + j));
            }
            nodes.push(tempArray);
          }

          done();
        }
      });
    });

    function eles() {
      var col = cy.collection();

      for (var i = 0; i < arguments.length; i++) {
        var ele = arguments[i];
        col = col.add(ele);
      }

      return col;
    };

    // Computes euclidean distance between 2 nodes
    function euclid(node1, node2) {
      var n1 = node1.id().split("-");
      var n2 = node2.id().split("-");
      return Math.sqrt( Math.pow(n1[0] - n2[0], 2) + Math.pow(n1[1] - n2[1], 2));
    };

    function ele2id( ele ){
      return ele.id();
    }

    function isNode( ele ){
      return ele.isNode();
    }

    it('eles.aStar(): directed, null heuristic, unweighted', function(){
      var options = {root: nodes[6][1],
               goal: nodes[1][5],
               directed: true,
               heuristic: function(a){return 0;}
              };
      var res = cy.elements().aStar(options);
      expect(res.found).to.equal(true);
      expect(res.distance).to.equal(9);
      expect(res.path.stdFilter(isNode).map(ele2id)).to.deep.equal(["6-1", "5-1", "4-1", "3-1", "2-1", "1-1", "1-2", "1-3", "1-4", "1-5"]);
    });

    it('eles.aStar(): directed, heuristic, unweighted', function(){
      var options = {root: nodes[6][1],
               goal: nodes[2][6],
               directed: true,
               heuristic: function(node) {return euclid(node, nodes[2][6]);}
              };
      var res = cy.elements().aStar(options);
      expect(res.found).to.equal(true);
      expect(res.distance).to.equal(9);
      expect(res.path.stdFilter(isNode).map(ele2id)).to.deep.equal(["6-1", "6-2", "6-3", "6-4", "6-5", "6-6", "5-6", "4-6", "3-6", "2-6"]);
    });

    it('eles.aStar(): undirected, heuristic vs null heuristic (performance), ', function(){
      var options1 = {root: nodes[3][3],
              goal: nodes[1][5],
              directed: false,
              heuristic: function(node) {return euclid(node, nodes[1][5]);}
              };
      var res1 = cy.elements().aStar(options1);
      expect(res1.found).to.equal(true);
      expect(res1.distance).to.equal(10);
      expect(res1.path.stdFilter(isNode).map(ele2id)).to.deep.equal(["3-3", "4-3", "4-2", "4-1", "3-1", "2-1", "1-1", "1-2", "1-3", "1-4", "1-5"]);

      var options2 = {root: nodes[3][3],
              goal: nodes[1][5],
              directed: false,
              heuristic: function(a){return 0;}
              };
      var res2 = cy.elements().aStar(options2);
      expect(res2.found).to.equal(true);
      expect(res2.distance).to.equal(10);
      expect(res2.path.stdFilter(isNode).map(ele2id)).to.deep.equal(["3-3", "4-3", "4-2", "4-1", "3-1", "2-1", "1-1", "1-2", "1-3", "1-4", "1-5"]);

      // Performance with heuristic should be better than without it
      expect(res1.steps).to.be.below(res2.steps);
    });

    function isContinuous(edges) {
      for (var i = 0; i < edges.length -1; ++i) {
        var edge = edges[i];
        var nextEdge = edges[i+1];
        if (! areConnected(edge, nextEdge)) {
          return false;
        }
      }
      return true;
    }

    function areConnected(edgeA, edgeB) {
      return edgeA.target() === edgeB.source() ||
             edgeA.source() === edgeB.target() ||
             edgeA.target() === edgeB.target() ||
             edgeA.source() === edgeB.source();
    }

    it('eles.aStar(): edges must form a continuous path', function(){
      var options = {root: nodes[6][1],
                      goal: nodes[2][6],
                      directed: true,
                      heuristic: function(node) {return euclid(node, nodes[2][6]);}
                    };
      var res = cy.elements().aStar(options);
      expect(isContinuous(res.path.edges())).to.equal(true);
    });

    function source(edge) {
      return edge.source();
    }

    function target(edge) {
      return edge.target();
    }

    it('eles.aStar(): the set of nodes touched by path.edges must equal the set of path.nodes', function(){
      let options = {root: nodes[6][1],
                      goal: nodes[2][6],
                      directed: true,
                      heuristic: function(node) {return euclid(node, nodes[2][6]);}
                    };
      let res = cy.elements().aStar(options);
      let sourcesAndTargetsOfEdges = new Set(res.path.edges().toArray().map(source).concat(res.path.edges().toArray().map(target)).map(ele2id));
      let setOfNodes = new Set(res.path.nodes().toArray().map(ele2id));
      expect(sourcesAndTargetsOfEdges).to.have.keys([...setOfNodes]);
    });

    it('eles.aStar(): directed, null heuristic, unweighted: same as dijkstra', function(){
      var options = {root: nodes[6][1],
               goal: nodes[1][5],
               directed: true,
               heuristic: function(a){return 0;}
              };
      var res = cy.elements().aStar(options);
      expect(res.found).to.equal(true);
      var dist = res.distance;
      expect(res.path.stdFilter(isNode).map(ele2id)).to.deep.equal(["6-1", "5-1", "4-1", "3-1", "2-1", "1-1", "1-2", "1-3", "1-4", "1-5"]);

      var resD = cy.elements().dijkstra({ root: nodes[6][1], directed: true });
      expect(resD.distanceTo(nodes[1][5])).to.equal(dist);
      expect(resD.pathTo(nodes[1][5]).stdFilter(isNode).map(ele2id)).to.deep.equal(["6-1", "5-1", "4-1", "3-1", "2-1", "1-1", "1-2", "1-3", "1-4", "1-5"]);
    });

    it('eles.aStar(): undirected, null heuristic, unweighted: same as dijkstra', function(){
      var options = {root: nodes[6][1],
               goal: nodes[1][5],
               directed: false,
               heuristic: function(a){return 0;}
              };
      var res = cy.elements().aStar(options);
      expect(res.found).to.equal(true);
      var dist = res.distance;
      var path = res.path.stdFilter(isNode).map(ele2id);

      var resD = cy.elements().dijkstra({ root: nodes[6][1], directed: true });
      expect(resD.distanceTo(nodes[1][5])).to.equal(dist);
      expect(resD.pathTo(nodes[1][5]).stdFilter(isNode).map(ele2id)).to.deep.equal(path);
    });
  });
});
<|endoftext|>


<commit_before>function encrypt(text, pass) {
  //console.log('pass:' + pass + ' encrypt IN:' + text);
  var key = Sha256.hash(pass);
  var encrypted = Aes.Ctr.encrypt(text, key, 256);
  //console.log('encrypt OUT:' + encrypted);
  return encrypted;
}

function decrypt (text, pass) {
  //console.log('pass:' + pass + ' decrypt IN:' + text);
  var key = Sha256.hash(pass);
  var decrypted = Aes.Ctr.decrypt(text, key, 256);
  //console.log('decrypt OUT:' + decrypted);
  return decrypted;
}

function passwordsMatch () {
  if( $('#password').val() == $('#password2').val() ) {

    $('#passGroup').removeClass("has-error");

    return true;
  }

  $('#passGroup').addClass("has-error");

  return false;
}

function pageEncrypt() {
  if ( passwordsMatch() ) {
    $('#message').val( encrypt( $('#message').val(), $('#password').val() ) );
    $('#message').select();
  }
}

function pageDecrypt () {
  if( passwordsMatch() ) {
    $('#message').val( decrypt( $('#message').val(), $('#password').val() ) );
    $('#message').select();
  }
}

$( function () {

  $('#encrypt').click( pageEncrypt );
  $('#decrypt').click( pageDecrypt );

  console.log('Page loaded');
});

<commit_msg>Update app.js<commit_after>function encrypt(text, pass) {
  //console.log('pass:' + pass + ' encrypt IN:' + text);
  var key = Sha256.hash(pass);
  var encrypted = Aes.Ctr.encrypt(text, key, 256);
  //console.log('encrypt OUT:' + encrypted);
  return encrypted;
}

function decrypt (text, pass) {
  //console.log('pass:' + pass + ' decrypt IN:' + text);
  var key = Sha256.hash(pass);
  var decrypted = Aes.Ctr.decrypt(text, key, 256);
  //console.log('decrypt OUT:' + decrypted);
  return decrypted;
}

function passwordsMatch () {
  if( $('#password').val() == $('#password2').val() ) {

    $('#passGroup').removeClass("has-error");
    $('#passwordError').addClass("hidden");

    return true;
  }

  $('#passGroup').addClass("has-error");
  $('#passwordError').addClass("show");

  return false;
}

function pageEncrypt() {
  if ( passwordsMatch() ) {
    $('#message').val( encrypt( $('#message').val(), $('#password').val() ) );
    $('#message').select();
  }
}

function pageDecrypt () {
  if( passwordsMatch() ) {
    $('#message').val( decrypt( $('#message').val(), $('#password').val() ) );
    $('#message').select();
  }
}

$( function () {

  $('#encrypt').click( pageEncrypt );
  $('#decrypt').click( pageDecrypt );

  console.log('Page loaded');
});

<|endoftext|>


<commit_before>/* exported
        registerConsole
*/
/* global
        sql_endpoint
*/

// FIXME: consolidate the exported functions into a single exported variable
var registerConsole = function (console, exitCallback) {
    console.click(function() {
        var sel = window.getSelection().toString();
        if (!sel) {
            $(this).find(".input").focus();
        }
    });
    var history = [];
    var history_point = 0;
    console.keydown(function(e) {
        if (e.which == 38) {
            history_point += 1;
            if (history_point < history.length) {
                $(this).find(".input").val(history[history_point]);
            }
            else {
                history_point = history.length - 1;
            }
            e.preventDefault();
        }
        if (e.which == 40) {
            history_point -= 1;
            if (history_point >= 0 && history.length > 0) {
                $(this).find(".input").val(history[history_point]);
            }
            else {
                $(this).find(".input").val("");
                history_point = -1;
            }
            e.preventDefault();
        }
    });
    console.find(".input").keydown(function(e) {
        var console = $(this).closest(".sql-console");
        if (e.ctrlKey && e.keyCode == 67 && !window.getSelection().toString()) {
            console.find(".output").append(console.find(".ps").text() + $(this).val() + "^C\n");
            $(this).val("");
            console.scrollTop(console[0].scrollHeight);
            e.preventDefault();
        }
    });
    console.find(".input").keypress(function(e) {
        var console = $(this).closest(".sql-console");
        if (e.which == 13) {
            var val = $(this).val();
            if (val) {
                if (val == "quit" || val == "exit" || val == "\\q" || val == ".q") {
                    if (exitCallback) {
                        exitCallback();
                        return;
                    }
                }
                else if ($.trim(val.toLowerCase()) == "\\! clear") {
                    console.find(".output").text("");
                }
                else {
                    console.find(".output").append(console.find(".ps").text() + val + "\n");
                    console.find("table").hide();
                    $.post(sql_endpoint, {"sql": val}, function(data) {
                        console.find(".output").append($("
").text(data).html());
                    }).error(function() {
                        console.find(".output").append("Server Error\n\n");
                    }).always(function() {
                        console.find("table").show();
                        console.find(".input").focus();
                        console.scrollTop(console[0].scrollHeight);
                    });
                    history.unshift(val);
                    history_point = -1;
                }
                $(this).val("");
            }
            else {
                console.find(".output").append(console.find(".ps").text() + "\n");
            }
            console.scrollTop(console[0].scrollHeight);
        }
    });
};
<commit_msg>Delete Extraneous Comment<commit_after>/* exported
        registerConsole
*/
/* global
        sql_endpoint
*/

var registerConsole = function (console, exitCallback) {
    console.click(function() {
        var sel = window.getSelection().toString();
        if (!sel) {
            $(this).find(".input").focus();
        }
    });
    var history = [];
    var history_point = 0;
    console.keydown(function(e) {
        if (e.which == 38) {
            history_point += 1;
            if (history_point < history.length) {
                $(this).find(".input").val(history[history_point]);
            }
            else {
                history_point = history.length - 1;
            }
            e.preventDefault();
        }
        if (e.which == 40) {
            history_point -= 1;
            if (history_point >= 0 && history.length > 0) {
                $(this).find(".input").val(history[history_point]);
            }
            else {
                $(this).find(".input").val("");
                history_point = -1;
            }
            e.preventDefault();
        }
    });
    console.find(".input").keydown(function(e) {
        var console = $(this).closest(".sql-console");
        if (e.ctrlKey && e.keyCode == 67 && !window.getSelection().toString()) {
            console.find(".output").append(console.find(".ps").text() + $(this).val() + "^C\n");
            $(this).val("");
            console.scrollTop(console[0].scrollHeight);
            e.preventDefault();
        }
    });
    console.find(".input").keypress(function(e) {
        var console = $(this).closest(".sql-console");
        if (e.which == 13) {
            var val = $(this).val();
            if (val) {
                if (val == "quit" || val == "exit" || val == "\\q" || val == ".q") {
                    if (exitCallback) {
                        exitCallback();
                        return;
                    }
                }
                else if ($.trim(val.toLowerCase()) == "\\! clear") {
                    console.find(".output").text("");
                }
                else {
                    console.find(".output").append(console.find(".ps").text() + val + "\n");
                    console.find("table").hide();
                    $.post(sql_endpoint, {"sql": val}, function(data) {
                        console.find(".output").append($("
").text(data).html());
                    }).error(function() {
                        console.find(".output").append("Server Error\n\n");
                    }).always(function() {
                        console.find("table").show();
                        console.find(".input").focus();
                        console.scrollTop(console[0].scrollHeight);
                    });
                    history.unshift(val);
                    history_point = -1;
                }
                $(this).val("");
            }
            else {
                console.find(".output").append(console.find(".ps").text() + "\n");
            }
            console.scrollTop(console[0].scrollHeight);
        }
    });
};
<|endoftext|>