<commit_before>/* global sauce, jQuery */

sauce.ns('sync', ns => {
    'use strict';

    const controllers = new Map();
    const L = sauce.locale;
    const H = sauce.locale.human;

    function setupSyncController($btn, id) {
        let statusTimeout;
        let syncError;
        const $status = $btn.find('.sauce-sync-status');
        if (!controllers.has(id)) {
            controllers.set(id, new sauce.hist.SyncController(id));
        }
        const controller = controllers.get(id);

        function setStatus(msg, options={}) {
            clearTimeout(statusTimeout);
            $status.html(msg);
            if (options.timeout) {
                statusTimeout = setTimeout(() => $status.empty(), options.timeout);
            }
        }

        controller.addEventListener('active', ev => {
            const active = ev.data;
            $btn.toggleClass('sync-active', active);
            if (active) {
                syncError = null;
                setStatus('Starting sync...');
            } else if (!syncError) {
                setStatus('Sync completed', {timeout: 5000});
            }
        });
        controller.addEventListener('error', ev => {
            setStatus(`Sync error: ${ev.data.error}`);
        });
        controller.addEventListener('progress', async ev => {
            const counts = ev.data.counts;
            const synced = counts.processed;
            const total = counts.total - counts.unavailable - counts.unprocessable;
            setStatus(`${synced.toLocaleString()}/${total.toLocaleString()}`);
        });
        controller.addEventListener('enable', ev => {
            $btn.addClass('enabled');
            setStatus('Sync enabled', {timeout: 5000});
        });
        controller.addEventListener('disable', ev => {
            $btn.removeClass('enabled sync-active');
            setStatus('Sync disabled', {timeout: 5000});
        });
        controller.isActiveSync().then(x => $btn.toggleClass('sync-active', x));
    }

    async function createSyncButton(id, athleteData, options={}) {
        if (athleteData && !athleteData.name) {
            throw new TypeError('athleteData.name is required');
        }
        await sauce.proxy.connected;
        await sauce.locale.init();
        if (options.syncController) {
            controllers.set(id, options.syncController);
        }
        const tpl = await sauce.template.getTemplate('sync-button.html');
        const $btn = jQuery(await tpl({status: !options.noStatus}));
        const athlete = await sauce.hist.getAthlete(id);
        $btn.toggleClass('enabled', !!(athlete && athlete.sync));
        setupSyncController($btn, id);
        $btn.on('click', async () => {
            if (!athlete && !(await sauce.hist.getAthlete(id))) {
                await sauce.hist.addAthlete({id, ...athleteData});
            }
            await activitySyncDialog(id, controllers.get(id));
            sauce.report.event('AthleteSync', 'ui-button', 'show-dialog');
        });
        return $btn;
    }

    async function activitySyncDialog(athleteId, syncController) {
        const locale = await L.getMessagesObject([
            'total', 'imported', 'unavailable', 'processed', 'unprocessable', 'activities',
            'delayed_until', 'title', 'remaining',
        ], 'sync_control_panel');
        let athlete = await sauce.hist.getAthlete(athleteId);
        const {FTPHistoryView, WeightHistoryView} = await sauce.getModule('/src/site/data-views.mjs');
        const tpl = await sauce.template.getTemplate('sync-control-panel.html', 'sync_control_panel');
        const hrZonesTpl = await sauce.template.getTemplate('sync-control-panel-hr-zones.html',
            'sync_control_panel');
        const initiallyEnabled = !!athlete.sync;
        const $modal = sauce.modal({
            title: `${locale.title} - ${athlete.name}`,
            icon: await sauce.images.asText('fa/sync-alt-duotone.svg'),
            dialogClass: 'sauce-sync-athlete-dialog',
            body: await tpl({
                athlete,
                enabled: initiallyEnabled
            }),
            flex: true,
            width: '60em',
            autoDestroy: true,
            autoOpen: false,
            closeOnMobileBack: true,
            extraButtons: [{
                text: 'Import Data',
                click: ev => {
                    sauce.report.event('AthleteSync', 'ui-button', 'import');
                    const btn = ev.currentTarget;
                    const input = document.createElement('input');
                    input.type = 'file';
                    input.accept = '.sbin';
                    input.multiple = true;
                    input.style.display = 'none';
                    input.addEventListener('change', async ev => {
                        const jobs = await sauce.getModule('/src/common/jscoop/jobs.js');
                        const importingQueue = new jobs.UnorderedWorkQueue({maxPending: 12});
                        const files = input.files;
                        const dataEx = new sauce.hist.DataExchange();
                        const origText = btn.textContent;
                        btn.classList.add('sauce-loading', 'disabled');
                        try {
                            let fileNum = 0;
                            for (const f of files) {
                                fileNum++;
                                const fileDesc = files.length > 1 ?
                                    `file ${fileNum} of ${files.length}` : 'file';
                                btn.textContent = `Reading ${fileDesc}...`;
                                const ab = await sauce.blobToArrayBuffer(f);
                                const batch = sauce.decodeBundle(ab);
                                const stride = 250;  // ~10MB
                                for (let i = 0; i < batch.length; i += stride) {
                                    importingQueue.put(dataEx.import(batch.slice(i, i + stride)));
                                }
                                let i = 1;
                                for await (const x of importingQueue) {
                                    void x;
                                    const progress = Math.min(i++ * stride, batch.length);
                                    btn.textContent = `Importing ${fileDesc}: ${H.number(progress / batch.length * 100)}%`;
                                }
                            }
                            await dataEx.flush();
                        } finally {
                            btn.textContent = origText;
                            btn.classList.remove('sauce-loading', 'disabled');
                        }
                    });
                    document.body.appendChild(input);
                    input.click();
                    input.remove();
                }
            }, {
                text: 'Export Data',
                click: async ev => {
                    sauce.report.event('AthleteSync', 'ui-button', 'export');
                    let bigBundle;
                    let page = 1;
                    const mem = navigator.deviceMemory || 4;
                    const date = (new Date()).toISOString().replace(/[-T:]/g, '_').split('.')[0];
                    const dl = () => {
                        sauce.downloadBlob(new Blob([bigBundle]), `${athlete.name}-${date}-${page++}.sbin`);
                        bigBundle = null;
                    };
                    const btn = ev.currentTarget;
                    const origText = btn.textContent;
                    btn.classList.add('sauce-loading', 'disabled');
                    const MB = 1024 * 1024;
                    const GB = 1024 * MB;
                    try {
                        const dataEx = new sauce.hist.DataExchange(athlete.id);
                        dataEx.addEventListener('data', async ev => {
                            const bundle = sauce.encodeBundle(ev.data);
                            bigBundle = bigBundle ? sauce.concatBundles(bigBundle, bundle) : bundle;
                            btn.textContent = `Creating file ${page}: ${H.number(bigBundle.byteLength / MB)}MB`;
                            if (bigBundle.byteLength > Math.min(mem * 0.25, 2) * GB) {
                                dl();
                            }
                        });
                        await dataEx.export();
                        dl();
                    } finally {
                        btn.textContent = origText;
                        btn.classList.remove('sauce-loading', 'disabled');
                    }
                }
            }]
        });
        const ftpHistView = new FTPHistoryView({
            athlete,
            el: $modal.find('.entry.history.ftp')
        });
        const weightHistView = new WeightHistoryView({
            athlete,
            el: $modal.find('.entry.history.weight')
        });

        async function updateHRZones() {
            $modal.find('.hr-zones').html(await hrZonesTpl({athlete}));
        }

        const rendering = [ftpHistView.render(), weightHistView.render(), updateHRZones()];

        async function updateSyncCounts(counts) {
            counts = counts || await sauce.hist.activityCounts(athlete.id);
            const total = counts.total - counts.unavailable;
            const synced = counts.processed;
            const title =
                `${locale.total}: ${counts.total}\n` +
                `${locale.imported}: ${counts.imported}\n` +
                `${locale.unavailable}: ${counts.unavailable}\n` +
                `${locale.remaining}: ${counts.total - counts.unavailable - counts.imported}\n` +
                `${locale.processed}: ${counts.processed}\n` +
                `${locale.unprocessable}: ${counts.unprocessable}\n`;
            const $synced = $modal.find('.entry.synced');
            $synced.attr('title', title);
            $synced.find('progress').attr('value', synced / total);
            if (synced === total) {
                $synced.find('.text').html(`100% | ${synced.toLocaleString()} ${locale.activities}`);
            } else {
                $synced.find('.text').html(`${synced.toLocaleString()} of ${total.toLocaleString()} ${locale.activities}`);
            }
        }

        async function updateSyncTimes() {
            const [lastSync, nextSync] = await Promise.all([
                syncController.lastSync(),
                syncController.nextSync()
            ]);
            $modal.find('.entry.last-sync value').text(lastSync ? H.datetime(lastSync) : '');
            $modal.find('.entry.next-sync value').text(nextSync ? H.datetime(nextSync) : '');
        }

        let rateLimiterInterval;
        async function setActive(active) {
            if (active) {
                $modal.addClass('sync-active');
                $modal.removeClass('has-error');
                $modal.find('.entry.status value').text('Running...');
                $modal.find('.entry.last-sync value').empty();
                $modal.find('.entry.next-sync value').empty();
                $modal.find('.entry.synced progress').removeAttr('value');
                $modal.find('.entry.synced .text').empty();
                clearInterval(rateLimiterInterval);
                rateLimiterInterval = setInterval(async () => {
                    const resumes = await syncController.rateLimiterResumes();
                    if (resumes && resumes - Date.now() > 10000) {
                        const resumesLocale = H.datetime(resumes, {concise: true});
                        $modal.find('.entry.status value').text(`${locale.delayed_until}: ${resumesLocale}`);
                    }
                }, 2000);
            } else {
                clearInterval(rateLimiterInterval);
                $modal.removeClass('sync-active');
                $modal.find('.entry.status value').text('Idle');
                athlete = await sauce.hist.getAthlete(athleteId);
                await Promise.all([updateSyncCounts(), updateSyncTimes(), updateHRZones()]);
            }
        }

        const listeners = {
            active: ev => void setActive(ev.data),
            error: async ev => {
                $modal.addClass('has-error');
                $modal.find('.entry.status value').text(ev.data.error);
            },
            progress: ev => void updateSyncCounts(ev.data.counts),
            enable: ev => void ($modal.find('input[name="enable"]')[0].checked = true),
            disable: ev => void ($modal.find('input[name="enable"]')[0].checked = false),
        };
        for (const [event, cb] of Object.entries(listeners)) {
            syncController.addEventListener(event, cb);
        }

        function onPromoKeyDown(ev) {
            if (ev.key === 'Escape') {
                ev.preventDefault();
                ev.stopPropagation();
                removeEventListener('keydown', onPromoKeyDown, {capture: true});
                $modal.find('.perf-promo').removeClass('expanded');
            }
        }

        $modal.on('input', 'input[name="enable"]', async ev => {
            const enabled = ev.currentTarget.checked;
            if (enabled) {
                await Promise.all([updateSyncCounts(), updateSyncTimes()]);
                await sauce.hist.enableAthlete(athlete.id);
            } else {
                await sauce.hist.disableAthlete(athlete.id);
            }
            $modal.toggleClass('sync-disabled', !enabled);
            sauce.report.event('AthleteSync', 'ui-button', enabled ? 'enable' : 'disable');
        });
        $modal.on('click', '.perf-promo .btn.enable', async ev => {
            $modal.find('input[name="enable"]').click();
        });
        $modal.on('click', '.perf-promo .nav-left', ev => {
            const $promo = jQuery(ev.currentTarget.closest('.perf-promo'));
            const $selected = $promo.find('.selected');
            $selected.removeClass('selected').prev().addClass('selected');
            const isFirst = $promo.find('.nav-placement .selected').is(':first-child');
            const isLast = $promo.find('.nav-placement .selected').is(':last-child');
            $promo.find('.nav-left').toggleClass('hidden', isFirst);
            $promo.find('.nav-right').toggleClass('hidden', isLast);
        });
        $modal.on('click', '.perf-promo .nav-right', ev => {
            const $promo = jQuery(ev.currentTarget.closest('.perf-promo'));
            const $selected = $promo.find('.selected');
            $selected.removeClass('selected').next().addClass('selected');
            const isFirst = $promo.find('.nav-placement .selected').is(':first-child');
            const isLast = $promo.find('.nav-placement .selected').is(':last-child');
            $promo.find('.nav-left').toggleClass('hidden', isFirst);
            $promo.find('.nav-right').toggleClass('hidden', isLast);
        });
        $modal.on('click', '.perf-promo .slides img', ev => {
            const $el = $modal.find('.perf-promo').toggleClass('expanded');
            if ($el.hasClass('expanded')) {
                addEventListener('keydown', onPromoKeyDown, {capture: true});
            } else {
                removeEventListener('keydown', onPromoKeyDown, {capture: true});
            }
        });
        $modal.on('click', '.perf-promo .btn.zoom-out', ev => {
            $modal.find('.perf-promo').removeClass('expanded');
            removeEventListener('keydown', onPromoKeyDown, {capture: true});
        });
        $modal.on('click', '.sync-stop.btn', ev => {
            $modal.removeClass('sync-active');
            syncController.cancel();
            sauce.report.event('AthleteSync', 'ui-button', 'stop');
        });
        $modal.on('click', '.sync-recompute.btn', async ev => {
            $modal.addClass('sync-active');
            $modal.find('.entry.synced progress').removeAttr('value');  // make it indeterminate
            $modal.find('.entry.synced .text').empty();
            await sauce.hist.invalidateAthleteSyncState(athlete.id, 'local');
            sauce.report.event('AthleteSync', 'ui-button', 'recompute');
        });
        $modal.on('click', '.sync-hr-zones.btn', async ev => {
            $modal.addClass('sync-active');
            $modal.find('.entry.synced progress').removeAttr('value');  // make it indeterminate
            $modal.find('.entry.synced .text').empty();
            await sauce.hist.updateAthlete(athlete.id, {hrZonesTS: null});
            athlete.hrZonesTS = null;
            await updateHRZones();
            await sauce.hist.invalidateAthleteSyncState(athlete.id, 'local', 'athlete-settings');
            sauce.report.event('AthleteSync', 'ui-button', 'hr-zones');
        });
        $modal.on('click', '.sync-start.btn', async ev => {
            $modal.addClass('sync-active');
            await sauce.hist.syncAthlete(athlete.id);
            sauce.report.event('AthleteSync', 'ui-button', 'start');
        });
        $modal.on('dialogclose', () => {
            for (const [event, cb] of Object.entries(listeners)) {
                syncController.removeEventListener(event, cb);
            }
        });
        if (initiallyEnabled) {
            await Promise.all([updateSyncCounts(), updateSyncTimes()]);
            if (await syncController.isActiveSync()) {
                setActive(true);
            } else {
                $modal.find('.entry.status value').text('Idle');
            }
        } else {
            $modal.addClass('sync-disabled');
        }
        await Promise.all(rendering);
        $modal.dialog('open');
        return $modal;
    }

    return {
        createSyncButton,
        activitySyncDialog,
    };
});
<commit_msg>have to use lower mem limit on import/export files<commit_after>/* global sauce, jQuery */

sauce.ns('sync', ns => {
    'use strict';

    const controllers = new Map();
    const L = sauce.locale;
    const H = sauce.locale.human;

    function setupSyncController($btn, id) {
        let statusTimeout;
        let syncError;
        const $status = $btn.find('.sauce-sync-status');
        if (!controllers.has(id)) {
            controllers.set(id, new sauce.hist.SyncController(id));
        }
        const controller = controllers.get(id);

        function setStatus(msg, options={}) {
            clearTimeout(statusTimeout);
            $status.html(msg);
            if (options.timeout) {
                statusTimeout = setTimeout(() => $status.empty(), options.timeout);
            }
        }

        controller.addEventListener('active', ev => {
            const active = ev.data;
            $btn.toggleClass('sync-active', active);
            if (active) {
                syncError = null;
                setStatus('Starting sync...');
            } else if (!syncError) {
                setStatus('Sync completed', {timeout: 5000});
            }
        });
        controller.addEventListener('error', ev => {
            setStatus(`Sync error: ${ev.data.error}`);
        });
        controller.addEventListener('progress', async ev => {
            const counts = ev.data.counts;
            const synced = counts.processed;
            const total = counts.total - counts.unavailable - counts.unprocessable;
            setStatus(`${synced.toLocaleString()}/${total.toLocaleString()}`);
        });
        controller.addEventListener('enable', ev => {
            $btn.addClass('enabled');
            setStatus('Sync enabled', {timeout: 5000});
        });
        controller.addEventListener('disable', ev => {
            $btn.removeClass('enabled sync-active');
            setStatus('Sync disabled', {timeout: 5000});
        });
        controller.isActiveSync().then(x => $btn.toggleClass('sync-active', x));
    }

    async function createSyncButton(id, athleteData, options={}) {
        if (athleteData && !athleteData.name) {
            throw new TypeError('athleteData.name is required');
        }
        await sauce.proxy.connected;
        await sauce.locale.init();
        if (options.syncController) {
            controllers.set(id, options.syncController);
        }
        const tpl = await sauce.template.getTemplate('sync-button.html');
        const $btn = jQuery(await tpl({status: !options.noStatus}));
        const athlete = await sauce.hist.getAthlete(id);
        $btn.toggleClass('enabled', !!(athlete && athlete.sync));
        setupSyncController($btn, id);
        $btn.on('click', async () => {
            if (!athlete && !(await sauce.hist.getAthlete(id))) {
                await sauce.hist.addAthlete({id, ...athleteData});
            }
            await activitySyncDialog(id, controllers.get(id));
            sauce.report.event('AthleteSync', 'ui-button', 'show-dialog');
        });
        return $btn;
    }

    async function activitySyncDialog(athleteId, syncController) {
        const locale = await L.getMessagesObject([
            'total', 'imported', 'unavailable', 'processed', 'unprocessable', 'activities',
            'delayed_until', 'title', 'remaining',
        ], 'sync_control_panel');
        let athlete = await sauce.hist.getAthlete(athleteId);
        const {FTPHistoryView, WeightHistoryView} = await sauce.getModule('/src/site/data-views.mjs');
        const tpl = await sauce.template.getTemplate('sync-control-panel.html', 'sync_control_panel');
        const hrZonesTpl = await sauce.template.getTemplate('sync-control-panel-hr-zones.html',
            'sync_control_panel');
        const initiallyEnabled = !!athlete.sync;
        const $modal = sauce.modal({
            title: `${locale.title} - ${athlete.name}`,
            icon: await sauce.images.asText('fa/sync-alt-duotone.svg'),
            dialogClass: 'sauce-sync-athlete-dialog',
            body: await tpl({
                athlete,
                enabled: initiallyEnabled
            }),
            flex: true,
            width: '60em',
            autoDestroy: true,
            autoOpen: false,
            closeOnMobileBack: true,
            extraButtons: [{
                text: 'Import Data',
                click: ev => {
                    sauce.report.event('AthleteSync', 'ui-button', 'import');
                    const btn = ev.currentTarget;
                    const input = document.createElement('input');
                    input.type = 'file';
                    input.accept = '.sbin';
                    input.multiple = true;
                    input.style.display = 'none';
                    input.addEventListener('change', async ev => {
                        const jobs = await sauce.getModule('/src/common/jscoop/jobs.js');
                        const importingQueue = new jobs.UnorderedWorkQueue({maxPending: 12});
                        const files = input.files;
                        const dataEx = new sauce.hist.DataExchange();
                        const origText = btn.textContent;
                        btn.classList.add('sauce-loading', 'disabled');
                        try {
                            let fileNum = 0;
                            for (const f of files) {
                                fileNum++;
                                const fileDesc = files.length > 1 ?
                                    `file ${fileNum} of ${files.length}` : 'file';
                                btn.textContent = `Reading ${fileDesc}...`;
                                const ab = await sauce.blobToArrayBuffer(f);
                                const batch = sauce.decodeBundle(ab);
                                const stride = 250;  // ~10MB
                                for (let i = 0; i < batch.length; i += stride) {
                                    importingQueue.put(dataEx.import(batch.slice(i, i + stride)));
                                }
                                let i = 1;
                                for await (const x of importingQueue) {
                                    void x;
                                    const progress = Math.min(i++ * stride, batch.length);
                                    btn.textContent = `Importing ${fileDesc}: ${H.number(progress / batch.length * 100)}%`;
                                }
                            }
                            await dataEx.flush();
                        } finally {
                            btn.textContent = origText;
                            btn.classList.remove('sauce-loading', 'disabled');
                        }
                    });
                    document.body.appendChild(input);
                    input.click();
                    input.remove();
                }
            }, {
                text: 'Export Data',
                click: async ev => {
                    sauce.report.event('AthleteSync', 'ui-button', 'export');
                    let bigBundle;
                    let page = 1;
                    const mem = navigator.deviceMemory || 4;
                    const date = (new Date()).toISOString().replace(/[-T:]/g, '_').split('.')[0];
                    const dl = () => {
                        sauce.downloadBlob(new Blob([bigBundle]), `${athlete.name}-${date}-${page++}.sbin`);
                        bigBundle = null;
                    };
                    const btn = ev.currentTarget;
                    const origText = btn.textContent;
                    btn.classList.add('sauce-loading', 'disabled');
                    const MB = 1024 * 1024;
                    const GB = 1024 * MB;
                    try {
                        const dataEx = new sauce.hist.DataExchange(athlete.id);
                        dataEx.addEventListener('data', async ev => {
                            const bundle = sauce.encodeBundle(ev.data);
                            bigBundle = bigBundle ? sauce.concatBundles(bigBundle, bundle) : bundle;
                            btn.textContent = `Creating file ${page}: ${H.number(bigBundle.byteLength / MB)}MB`;
                            if (bigBundle.byteLength > Math.min(mem * 0.25, 1) * GB) {
                                dl();
                            }
                        });
                        await dataEx.export();
                        dl();
                    } finally {
                        btn.textContent = origText;
                        btn.classList.remove('sauce-loading', 'disabled');
                    }
                }
            }]
        });
        const ftpHistView = new FTPHistoryView({
            athlete,
            el: $modal.find('.entry.history.ftp')
        });
        const weightHistView = new WeightHistoryView({
            athlete,
            el: $modal.find('.entry.history.weight')
        });

        async function updateHRZones() {
            $modal.find('.hr-zones').html(await hrZonesTpl({athlete}));
        }

        const rendering = [ftpHistView.render(), weightHistView.render(), updateHRZones()];

        async function updateSyncCounts(counts) {
            counts = counts || await sauce.hist.activityCounts(athlete.id);
            const total = counts.total - counts.unavailable;
            const synced = counts.processed;
            const title =
                `${locale.total}: ${counts.total}\n` +
                `${locale.imported}: ${counts.imported}\n` +
                `${locale.unavailable}: ${counts.unavailable}\n` +
                `${locale.remaining}: ${counts.total - counts.unavailable - counts.imported}\n` +
                `${locale.processed}: ${counts.processed}\n` +
                `${locale.unprocessable}: ${counts.unprocessable}\n`;
            const $synced = $modal.find('.entry.synced');
            $synced.attr('title', title);
            $synced.find('progress').attr('value', synced / total);
            if (synced === total) {
                $synced.find('.text').html(`100% | ${synced.toLocaleString()} ${locale.activities}`);
            } else {
                $synced.find('.text').html(`${synced.toLocaleString()} of ${total.toLocaleString()} ${locale.activities}`);
            }
        }

        async function updateSyncTimes() {
            const [lastSync, nextSync] = await Promise.all([
                syncController.lastSync(),
                syncController.nextSync()
            ]);
            $modal.find('.entry.last-sync value').text(lastSync ? H.datetime(lastSync) : '');
            $modal.find('.entry.next-sync value').text(nextSync ? H.datetime(nextSync) : '');
        }

        let rateLimiterInterval;
        async function setActive(active) {
            if (active) {
                $modal.addClass('sync-active');
                $modal.removeClass('has-error');
                $modal.find('.entry.status value').text('Running...');
                $modal.find('.entry.last-sync value').empty();
                $modal.find('.entry.next-sync value').empty();
                $modal.find('.entry.synced progress').removeAttr('value');
                $modal.find('.entry.synced .text').empty();
                clearInterval(rateLimiterInterval);
                rateLimiterInterval = setInterval(async () => {
                    const resumes = await syncController.rateLimiterResumes();
                    if (resumes && resumes - Date.now() > 10000) {
                        const resumesLocale = H.datetime(resumes, {concise: true});
                        $modal.find('.entry.status value').text(`${locale.delayed_until}: ${resumesLocale}`);
                    }
                }, 2000);
            } else {
                clearInterval(rateLimiterInterval);
                $modal.removeClass('sync-active');
                $modal.find('.entry.status value').text('Idle');
                athlete = await sauce.hist.getAthlete(athleteId);
                await Promise.all([updateSyncCounts(), updateSyncTimes(), updateHRZones()]);
            }
        }

        const listeners = {
            active: ev => void setActive(ev.data),
            error: async ev => {
                $modal.addClass('has-error');
                $modal.find('.entry.status value').text(ev.data.error);
            },
            progress: ev => void updateSyncCounts(ev.data.counts),
            enable: ev => void ($modal.find('input[name="enable"]')[0].checked = true),
            disable: ev => void ($modal.find('input[name="enable"]')[0].checked = false),
        };
        for (const [event, cb] of Object.entries(listeners)) {
            syncController.addEventListener(event, cb);
        }

        function onPromoKeyDown(ev) {
            if (ev.key === 'Escape') {
                ev.preventDefault();
                ev.stopPropagation();
                removeEventListener('keydown', onPromoKeyDown, {capture: true});
                $modal.find('.perf-promo').removeClass('expanded');
            }
        }

        $modal.on('input', 'input[name="enable"]', async ev => {
            const enabled = ev.currentTarget.checked;
            if (enabled) {
                await Promise.all([updateSyncCounts(), updateSyncTimes()]);
                await sauce.hist.enableAthlete(athlete.id);
            } else {
                await sauce.hist.disableAthlete(athlete.id);
            }
            $modal.toggleClass('sync-disabled', !enabled);
            sauce.report.event('AthleteSync', 'ui-button', enabled ? 'enable' : 'disable');
        });
        $modal.on('click', '.perf-promo .btn.enable', async ev => {
            $modal.find('input[name="enable"]').click();
        });
        $modal.on('click', '.perf-promo .nav-left', ev => {
            const $promo = jQuery(ev.currentTarget.closest('.perf-promo'));
            const $selected = $promo.find('.selected');
            $selected.removeClass('selected').prev().addClass('selected');
            const isFirst = $promo.find('.nav-placement .selected').is(':first-child');
            const isLast = $promo.find('.nav-placement .selected').is(':last-child');
            $promo.find('.nav-left').toggleClass('hidden', isFirst);
            $promo.find('.nav-right').toggleClass('hidden', isLast);
        });
        $modal.on('click', '.perf-promo .nav-right', ev => {
            const $promo = jQuery(ev.currentTarget.closest('.perf-promo'));
            const $selected = $promo.find('.selected');
            $selected.removeClass('selected').next().addClass('selected');
            const isFirst = $promo.find('.nav-placement .selected').is(':first-child');
            const isLast = $promo.find('.nav-placement .selected').is(':last-child');
            $promo.find('.nav-left').toggleClass('hidden', isFirst);
            $promo.find('.nav-right').toggleClass('hidden', isLast);
        });
        $modal.on('click', '.perf-promo .slides img', ev => {
            const $el = $modal.find('.perf-promo').toggleClass('expanded');
            if ($el.hasClass('expanded')) {
                addEventListener('keydown', onPromoKeyDown, {capture: true});
            } else {
                removeEventListener('keydown', onPromoKeyDown, {capture: true});
            }
        });
        $modal.on('click', '.perf-promo .btn.zoom-out', ev => {
            $modal.find('.perf-promo').removeClass('expanded');
            removeEventListener('keydown', onPromoKeyDown, {capture: true});
        });
        $modal.on('click', '.sync-stop.btn', ev => {
            $modal.removeClass('sync-active');
            syncController.cancel();
            sauce.report.event('AthleteSync', 'ui-button', 'stop');
        });
        $modal.on('click', '.sync-recompute.btn', async ev => {
            $modal.addClass('sync-active');
            $modal.find('.entry.synced progress').removeAttr('value');  // make it indeterminate
            $modal.find('.entry.synced .text').empty();
            await sauce.hist.invalidateAthleteSyncState(athlete.id, 'local');
            sauce.report.event('AthleteSync', 'ui-button', 'recompute');
        });
        $modal.on('click', '.sync-hr-zones.btn', async ev => {
            $modal.addClass('sync-active');
            $modal.find('.entry.synced progress').removeAttr('value');  // make it indeterminate
            $modal.find('.entry.synced .text').empty();
            await sauce.hist.updateAthlete(athlete.id, {hrZonesTS: null});
            athlete.hrZonesTS = null;
            await updateHRZones();
            await sauce.hist.invalidateAthleteSyncState(athlete.id, 'local', 'athlete-settings');
            sauce.report.event('AthleteSync', 'ui-button', 'hr-zones');
        });
        $modal.on('click', '.sync-start.btn', async ev => {
            $modal.addClass('sync-active');
            await sauce.hist.syncAthlete(athlete.id);
            sauce.report.event('AthleteSync', 'ui-button', 'start');
        });
        $modal.on('dialogclose', () => {
            for (const [event, cb] of Object.entries(listeners)) {
                syncController.removeEventListener(event, cb);
            }
        });
        if (initiallyEnabled) {
            await Promise.all([updateSyncCounts(), updateSyncTimes()]);
            if (await syncController.isActiveSync()) {
                setActive(true);
            } else {
                $modal.find('.entry.status value').text('Idle');
            }
        } else {
            $modal.addClass('sync-disabled');
        }
        await Promise.all(rendering);
        $modal.dialog('open');
        return $modal;
    }

    return {
        createSyncButton,
        activitySyncDialog,
    };
});
<|endoftext|>


<commit_before>import schema from 'schema:@sanity/base/schema'
import inputResolver from 'function:@sanity/form-builder/input-resolver'
import ValidationList from 'component:@sanity/form-builder/validation-list'
import {
  createFormBuilder,
  Schema
} from 'role:@sanity/form-builder'

const compiledSchema = Schema.compile(schema)

export default createFormBuilder({
  schema: compiledSchema,
  resolveInputComponent: inputResolver,
  resolveValidationComponent: () => ValidationList
})
<commit_msg>Don't import form builder from role<commit_after>import schema from 'schema:@sanity/base/schema'
import inputResolver from 'function:@sanity/form-builder/input-resolver'
import ValidationList from 'component:@sanity/form-builder/validation-list'

import {
  createFormBuilder,
  Schema
} from '../index'

const compiledSchema = Schema.compile(schema)

export default createFormBuilder({
  schema: compiledSchema,
  resolveInputComponent: inputResolver,
  resolveValidationComponent: () => ValidationList
})
<|endoftext|>


<commit_before>var $ = require('jquery');
var cdb = require('cartodb.js');
var UserSettingsView = require('../public_common/user_settings_view');
var UserTourView = require('../public_common/user_tour_view');
var UserIndustriesView = require('../public_common/user_industries_view');
var UserResourcesView = require('../public_common/user_resources_view');
var UserDiscoverView = require('../public_common/user_discover_view');
var FavMapView = require('../public_dashboard/fav_map_view');
var Feed = require('./view');
var ScrollableHeader = require('../common/views/scrollable_header');

/**
 * Entry point for the user feed, bootstraps all dependency models and application.
 */
$(function() {

  cdb.init(function() {

    cdb.templates.namespace = 'cartodb/';
    cdb.config.set(window.config);
    cdb.config.set('url_prefix', window.base_url);

    var userTourView = new UserTourView({
      el: $('.js-user-tour')
    });

    var userIndustriesView = new UserIndustriesView({
      el: $('.js-user-industries')
    });

    var userResourcesView = new UserResourcesView({
      el: $('.js-user-resources')
    });

    var userDiscoverView = new UserDiscoverView({
      el: $('.js-user-discover'),
    });

    var scrollableHeader = new ScrollableHeader({
      el: $('.js-Navmenu'),
      anchorPoint: 520
    });

    var authenticatedUser = new cdb.open.AuthenticatedUser();

    $(document.body).bind('click', function() {
      cdb.god.trigger('closeDialogs');
    });

    authenticatedUser.bind('change', function() {
      if (authenticatedUser.get('username')) {
        var user = new cdb.admin.User(authenticatedUser.attributes);
        var userSettingsView = new UserSettingsView({
          el: $('.js-user-settings'),
          model: user
        });
        userSettingsView.render();

        var userDashboardUrl = user.viewUrl().dashboard();
        $('.js-user-info .UserAvatar-img').wrap($('<a>', {
          href: userDashboardUrl
        }));

        $('.js-login').hide();
        $('.js-learn').show();
      }
    });

    var favMapView = new FavMapView(window.favMapViewAttrs);
    favMapView.render();

    var feed = new Feed({
      el: $('.js-feed'),
      authenticatedUser: authenticatedUser
    });

    feed.render();
    authenticatedUser.fetch();
  });
});
<commit_msg>Update entry.js<commit_after>var $ = require('jquery');
var cdb = require('cartodb.js');
var UserSettingsView = require('../public_common/user_settings_view');
var UserTourView = require('../public_common/user_tour_view');
var UserIndustriesView = require('../public_common/user_industries_view');
var UserResourcesView = require('../public_common/user_resources_view');
var UserDiscoverView = require('../public_common/user_discover_view');
var FavMapView = require('../public_dashboard/fav_map_view');
var Feed = require('./view');
var ScrollableHeader = require('../common/views/scrollable_header');

/**
 * Entry point for the user feed, bootstraps all dependency models and application.
 */
$(function() {

  cdb.init(function() {

    cdb.templates.namespace = 'cartodb/';
    cdb.config.set(window.config);
    cdb.config.set('url_prefix', window.base_url);

    var userTourView = new UserTourView({
      el: $('.js-user-tour')
    });

    var userIndustriesView = new UserIndustriesView({
      el: $('.js-user-industries')
    });

    var userResourcesView = new UserResourcesView({
      el: $('.js-user-resources')
    });

    var userDiscoverView = new UserDiscoverView({
      el: $('.js-user-discover')
    });

    var scrollableHeader = new ScrollableHeader({
      el: $('.js-Navmenu'),
      anchorPoint: 520
    });

    var authenticatedUser = new cdb.open.AuthenticatedUser();

    $(document.body).bind('click', function() {
      cdb.god.trigger('closeDialogs');
    });

    authenticatedUser.bind('change', function() {
      if (authenticatedUser.get('username')) {
        var user = new cdb.admin.User(authenticatedUser.attributes);
        var userSettingsView = new UserSettingsView({
          el: $('.js-user-settings'),
          model: user
        });
        userSettingsView.render();

        var userDashboardUrl = user.viewUrl().dashboard();
        $('.js-user-info .UserAvatar-img').wrap($('<a>', {
          href: userDashboardUrl
        }));

        $('.js-login').hide();
        $('.js-learn').show();
      }
    });

    var favMapView = new FavMapView(window.favMapViewAttrs);
    favMapView.render();

    var feed = new Feed({
      el: $('.js-feed'),
      authenticatedUser: authenticatedUser
    });

    feed.render();
    authenticatedUser.fetch();
  });
});
<|endoftext|>


<commit_before>"use strict";
/*
 Created by lx on 2016/11/28
 Name: Zihao Zhao
 */

const mongoCollections = require("../config/mongoCollections");
const blog = mongoCollections.blog;
const uuid = require('node-uuid');

/*
 The blog data structure
 5.Blog
  {
    "_id": "",
    "title": "",
    "content": "",
    "createTime": "",
    "mainImage": "",
    "conclusions": "",
    "type": "",
    "tag": [],
    "userId": "",
    "siteId": []
  }
 */

let exportedMethods = {
    getAllCities() {
        return blog().then((blogCollection) => {
            return blogCollection.find({}).toArray();
        });
    },

    getBlogById(id) {
        if (!id) return Promise.reject ("You must provide an id.");

        return blog().then((blogCollection) => {
            return blogCollection.findOne({ _id: id }).then((blog) => {
                if (!blog) return Promise.reject (`blog with id: ${id} is not found.`);

                return blog;
            });
        });
    },

    getBlogByTitle(name) {
        if (!name) return Promise.reject ("You must provide a blog title.");

        return blog().then((blogCollection) => {
            return blogCollection.find({ title: title }).toArray().then((blogList) => {
                if (!blogList) return Promise.reject (`blog title ${name} is not found.`);
                return blogList;
            });
        });
    },

    getBlogByUserId(userId) {
        if (!userId) return Promise.reject ("You must provide a userId.");

        return blog().then((blogCollection) => {
            return blogCollection.find({ "userId": userId }).toArray().then((blogList) => {
                if (!blogList) return Promise.reject (`blog with userId of ${userId} is not found.`);
                return blogList;
            });
        });
    },

    getBlogByType(type) {
        if (!type) return Promise.reject ("You must provide a type.");

        return blog().then((blogCollection) => {
            return blogCollection.find({ type: type }).toArray().then((blogList) => {
                if (!blogList) return Promise.reject (`blog with type of ${province} is not found.`);
                return blogList;
            });
        });
    },

    addBlog(title, content, createTime, mainImage, conclusions, type, tag, userId, siteId) {
        // check title and content
        if (!title) return Promise.reject ("You must provide a title of the blog.");
        if (!content) return Promise.reject("You must provide content of the blog.")
        return blog().then((blogCollection) => {
            let newblog = {
                _id: uuid.v4(),
                title: title,
                content: content,
                createTime: createTime,
                mainImage: mainImage,
                conclusions: conclusions,
                type: type,
                tag: tag,
                userId: userId,
                siteId: siteId
            };

            return blogCollection.insertOne(newblog).then((newInsertInformation) => {
                return newInsertInformation.insertedId;
            }).then((newId) => {
                return this.getblogById(newId);
            });
        });
    },

    addTagIfNotExists(id, newTag) {
        if (!id) return Promise.reject ("You must provide a blog id.");
        if (!newTag) return Promise.reject("You must provide a tag.");
        return blog().then((blogCollection) => {
            return blogCollection.update({ _id: id }, { $addToSet: { "tag": newTag } }).then((result) => {
                return this.getblogById(id);
            });
        });
    },

    removeblog(id) {
        if (!id) return Promise.reject ("You must provide an id.");

        return blog().then((blogCollection) => {
            return blogCollection.removeOne({ _id: id }).then((deletionInfo) => {
                if (deletionInfo.deletedCount === 0) {
                    throw (`Could not delete blog with id of ${id}.`);
                } else {
                    return {"message":"remove success!"};
                 }
            });
        });
    },

    removeblogTag(id, TagToRemove) {
        if (!id) return Promise.reject("You must provide a blog id.");
        if (!TagToRemove) return Promise.reject("You must provide tag to remove.");

        return blog().then((blogCollection) => {
            return blogCollection.update({ _id: id }, { $pullAll: { "tag": TagToRemove } }).then((result) => {
                return this.getBlogById(id);
            });
        });
    },

    updateblog(id, updatedblog) {
        if (!id) return Promise.reject ("You must provide an id.");

        return blog().then((blogCollection) => {
            let updatedblogData = {};
            // title
            if (updatedblog.title) {
                updatedblogData.title = updatedblog.title;
            }
            // content
            if (updatedblog.content) {
                updatedblogData.content = updatedblog.content;
            }
            // createTime
            if (updatedblog.createTime) {
                updatedblogData.createTime = updatedblog.createTime;
            }
            // mainImage
            if (updatedblog.mainImage) {
                updatedblogData.mainImage = updatedblog.mainImage;
            }
            // conclusions
            if (updatedblog.conclusions) {
                updatedblogData.conclusions = updatedblog.conclusions;
            }
            // type
            if (updatedblog.type) {
                updatedblogData.type = updatedblog.type;
            }
            // tag
            if (updatedblog.tag) {
                updatedblogData.tag = updatedblog.tag;
            }
            // siteId
            if (updatedblog.siteId) {
                updatedblogData.siteId = updatedblog.siteId;
            }
            let updateCommand = {
                $set: updatedblogData
            };

            return blogCollection.updateOne({ _id: id }, updateCommand).then((result) => {
                return this.getBlogById(id);
            });
        });
    }
}

module.exports = exportedMethods;
<commit_msg>update #1<commit_after>"use strict";
/*
 Created by lx on 2016/11/28
 Name: Zihao Zhao
 */

const mongoCollections = require("../config/mongoCollections");
const blog = mongoCollections.blog;
const uuid = require('node-uuid');

/*
 The blog data structure
 5.Blog
  {
    "_id": "",
    "title": "",
    "content": "",
    "createTime": "",
    "mainImage": "",
    "conclusions": "",
    "type": "",
    "tag": [],
    "userId": "",
    "siteId": []
  }
 */

let exportedMethods = {
    getAllBlogs() {
        return blog().then((blogCollection) => {
            return blogCollection.find({}).toArray();
        });
    },

    getBlogById(id) {
        if (!id) return Promise.reject ("You must provide an id.");

        return blog().then((blogCollection) => {
            return blogCollection.findOne({ _id: id }).then((blog) => {
                if (!blog) return Promise.reject (`blog with id: ${id} is not found.`);

                return blog;
            });
        });
    },

    getBlogByTitle(name) {
        if (!name) return Promise.reject ("You must provide a blog title.");

        return blog().then((blogCollection) => {
            return blogCollection.find({ title: title }).toArray().then((blogList) => {
                if (!blogList) return Promise.reject (`blog title ${name} is not found.`);
                return blogList;
            });
        });
    },

    getBlogByUserId(userId) {
        if (!userId) return Promise.reject ("You must provide a userId.");

        return blog().then((blogCollection) => {
            return blogCollection.find({ "userId": userId }).toArray().then((blogList) => {
                if (!blogList) return Promise.reject (`blog with userId of ${userId} is not found.`);
                return blogList;
            });
        });
    },

    getBlogByType(type) {
        if (!type) return Promise.reject ("You must provide a type.");

        return blog().then((blogCollection) => {
            return blogCollection.find({ type: type }).toArray().then((blogList) => {
                if (!blogList) return Promise.reject (`blog with type of ${province} is not found.`);
                return blogList;
            });
        });
    },

    addBlog(title, content, createTime, mainImage, conclusions, type, tag, userId, siteId) {
        // check title and content
        if (!title) return Promise.reject ("You must provide a title of the blog.");
        if (!content) return Promise.reject("You must provide content of the blog.")
        return blog().then((blogCollection) => {
            let newblog = {
                _id: uuid.v4(),
                title: title,
                content: content,
                createTime: createTime,
                mainImage: mainImage,
                conclusions: conclusions,
                type: type,
                tag: tag,
                userId: userId,
                siteId: siteId
            };

            return blogCollection.insertOne(newblog).then((newInsertInformation) => {
                return newInsertInformation.insertedId;
            }).then((newId) => {
                return this.getblogById(newId);
            });
        });
    },

    addTagIfNotExists(id, newTag) {
        if (!id) return Promise.reject ("You must provide a blog id.");
        if (!newTag) return Promise.reject("You must provide a tag.");
        return blog().then((blogCollection) => {
            return blogCollection.update({ _id: id }, { $addToSet: { "tag": newTag } }).then((result) => {
                return this.getblogById(id);
            });
        });
    },

    removeblog(id) {
        if (!id) return Promise.reject ("You must provide an id.");

        return blog().then((blogCollection) => {
            return blogCollection.removeOne({ _id: id }).then((deletionInfo) => {
                if (deletionInfo.deletedCount === 0) {
                    throw (`Could not delete blog with id of ${id}.`);
                } else {
                    return {"message":"remove success!"};
                 }
            });
        });
    },

    removeblogTag(id, TagToRemove) {
        if (!id) return Promise.reject("You must provide a blog id.");
        if (!TagToRemove) return Promise.reject("You must provide tag to remove.");

        return blog().then((blogCollection) => {
            return blogCollection.update({ _id: id }, { $pullAll: { "tag": TagToRemove } }).then((result) => {
                return this.getBlogById(id);
            });
        });
    },

    updateblog(id, updatedblog) {
        if (!id) return Promise.reject ("You must provide an id.");

        return blog().then((blogCollection) => {
            let updatedblogData = {};
            // title
            if (updatedblog.title) {
                updatedblogData.title = updatedblog.title;
            }
            // content
            if (updatedblog.content) {
                updatedblogData.content = updatedblog.content;
            }
            // createTime
            if (updatedblog.createTime) {
                updatedblogData.createTime = updatedblog.createTime;
            }
            // mainImage
            if (updatedblog.mainImage) {
                updatedblogData.mainImage = updatedblog.mainImage;
            }
            // conclusions
            if (updatedblog.conclusions) {
                updatedblogData.conclusions = updatedblog.conclusions;
            }
            // type
            if (updatedblog.type) {
                updatedblogData.type = updatedblog.type;
            }
            // tag
            if (updatedblog.tag) {
                updatedblogData.tag = updatedblog.tag;
            }
            // siteId
            if (updatedblog.siteId) {
                updatedblogData.siteId = updatedblog.siteId;
            }
            let updateCommand = {
                $set: updatedblogData
            };

            return blogCollection.updateOne({ _id: id }, updateCommand).then((result) => {
                return this.getBlogById(id);
            });
        });
    }
}

module.exports = exportedMethods;
<|endoftext|>


<commit_before>// Copyright 2018, University of Colorado Boulder

//TODO https://github.com/phetsims/phet-io/issues/1371, phetioInherit doesn't support ES6 class
/**
 * IO type for Quadratic
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define( require => {
  'use strict';

  // modules
  const graphingQuadratics = require( 'GRAPHING_QUADRATICS/graphingQuadratics' );
  const Quadratic = require( 'GRAPHING_QUADRATICS/common/model/Quadratic' );

  // ifphetio
  const assertInstanceOf = require( 'ifphetio!PHET_IO/assertInstanceOf' );
  const ObjectIO = require( 'ifphetio!PHET_IO/types/ObjectIO' );
  const phetioInherit = require( 'ifphetio!PHET_IO/phetioInherit' );

  /**
   * @param {Quadratic} quadratic
   * @param {string} phetioID
   * @constructor
   */
  function QuadraticIO( quadratic, phetioID ) {
    assert && assertInstanceOf( quadratic, Quadratic );
    ObjectIO.call( this, quadratic, phetioID );
  }

  graphingQuadratics.register( 'QuadraticIO', QuadraticIO );

  //TODO return fails when running without phetio, see https://github.com/phetsims/phet-io/issues/1370
  phetioInherit( ObjectIO, 'QuadraticIO', QuadraticIO, {}, {

    /**
     * Encodes the state of a Quadratic instance.
     * @param {Quadratic} quadratic
     * @returns {*}
     * @public
     */
    toStateObject: function( quadratic ) {
      assert && assertInstanceOf( quadratic, Quadratic );
      return quadratic.toStateObject();
    },

    /**
     * Decodes state into a Quadratic instance.
     * @param {*} object
     * @returns {Quadratic}
     * @public
     */
    fromStateObject: function( object ) {
      return Quadratic.fromStateObject( object );
    }
  } );

  return QuadraticIO;
} );<commit_msg>TODO<commit_after>// Copyright 2018, University of Colorado Boulder

//TODO https://github.com/phetsims/phet-io/issues/1371, phetioInherit doesn't support ES6 class
/**
 * IO type for Quadratic
 *
 * @author Chris Malley (PixelZoom, Inc.)
 */
define( require => {
  'use strict';

  // modules
  const graphingQuadratics = require( 'GRAPHING_QUADRATICS/graphingQuadratics' );
  const Quadratic = require( 'GRAPHING_QUADRATICS/common/model/Quadratic' );

  // ifphetio
  const assertInstanceOf = require( 'ifphetio!PHET_IO/assertInstanceOf' );
  const ObjectIO = require( 'ifphetio!PHET_IO/types/ObjectIO' );
  const phetioInherit = require( 'ifphetio!PHET_IO/phetioInherit' );

  /**
   * @param {Quadratic} quadratic
   * @param {string} phetioID
   * @constructor
   */
  function QuadraticIO( quadratic, phetioID ) {
    assert && assertInstanceOf( quadratic, Quadratic );
    ObjectIO.call( this, quadratic, phetioID );
  }

  graphingQuadratics.register( 'QuadraticIO', QuadraticIO );

  //TODO https://github.com/phetsims/phet-io/issues/1370, return phetioInherit fails when running without phetio
  phetioInherit( ObjectIO, 'QuadraticIO', QuadraticIO, {}, {

    /**
     * Encodes the state of a Quadratic instance.
     * @param {Quadratic} quadratic
     * @returns {*}
     * @public
     */
    toStateObject: function( quadratic ) {
      assert && assertInstanceOf( quadratic, Quadratic );
      return quadratic.toStateObject();
    },

    /**
     * Decodes state into a Quadratic instance.
     * @param {*} object
     * @returns {Quadratic}
     * @public
     */
    fromStateObject: function( object ) {
      return Quadratic.fromStateObject( object );
    }
  } );

  return QuadraticIO;
} );<|endoftext|>


<commit_before>// Description:
//   Search torrents from torrentproject.se
//
//  Dependencies:
//    cheerio
//    cloudscraper
//
// Commands:
//   hubot torrent <query>
//
// Author:
//   @jorgeepunan

var cheerio = require('cheerio');
var cloudscraper = require('cloudscraper');

module.exports = function(robot) {
  robot.respond(/torrent (.*)/i, function(msg) {

    msg.send('Esperando respuesta de Torrent Project... :clock830:');

    var busqueda = msg.match[1];
    var domain = 'https://torrentproject.se/';
    var url = domain + '?t=' + busqueda.split(' ').join('+');

    cloudscraper.get(url, function(error, response, body) {
      if (error) {
        console.log('Ocurrió un error :(');
      } else {
        var $ = cheerio.load(body);
        var resultados = [];

        $('#similarfiles > div:not(.gac_bb)').each(function() {
          var title = $(this).find('a').text();
          var link = $(this).find('a').attr('href');

          resultados.push( title + ' | ' + domain + link );
        });

        if(resultados.length > 0) {
          var limiteResultados = (resultados.length > 4) ? 3 : resultados.length;
          var plural = resultados.length > 1 ? ['n','s'] : ['',''];
          msg.send('Se ha'+plural[0]+' encontrado '+ resultados.length + ' resultado'+plural[1]);
          for (var i=0; i < limiteResultados; i++) {
            var conteo = i + 1;
            msg.send(conteo + ': ' + resultados[i]);
          }
          if(resultados.length > limiteResultados) {
            msg.send('Otros resultados en: '+ url);
          }
        } else {
          msg.send('No se han encontrado resultados sobre '+ busqueda);
        }

      }
    });

  });

};
<commit_msg>mejora en torres project<commit_after>// Description:
//   Search torrents from torrentproject.se
//
//  Dependencies:
//    cheerio
//    cloudscraper
//
// Commands:
//   hubot torrent <query>
//
// Author:
//   @jorgeepunan

var cheerio = require('cheerio');
var cloudscraper = require('cloudscraper');

module.exports = function(robot) {
  robot.respond(/torrent (.*)/i, function(msg) {

    msg.send('Esperando respuesta de Torrent Project... :clock830:');

    var busqueda = msg.match[1];
    var domain = 'https://torrentproject.se/';
    var url = domain + '?t=' + busqueda.split(' ').join('+');

    cloudscraper.get(url, function(error, response, body) {
      if (error) {
        robot.emit('error', error, msg);
      } else {
        var $ = cheerio.load(body);
        var resultados = [];

        $('#similarfiles > div:not(.gac_bb)').each(function() {
          var title = $(this).find('a').text();
          var link = $(this).find('a').attr('href');

          resultados.push(`<${domain}${link}|${title}>`);
        });

        if(resultados.length > 0) {
          var limiteResultados = (resultados.length > 4) ? 3 : resultados.length;
          var plural = resultados.length > 1 ? ['n','s'] : ['',''];
          var resume = 'Se ha'+plural[0]+' encontrado '+ resultados.length + ' resultado'+plural[1];
          var links = resultados
            .slice(0, limiteResultados)
            .map((result, index) => `${index + 1}: ${result}`)
            .join('\n');
          var more = resultados.length > limiteResultados ? `\n<${url}|Ver más resultados>` : '';
          var text = `${resume}\n${links}${more}`;
          if (robot.adapter.constructor.name === 'SlackBot') {
            var options = {unfurl_links: false, as_user: true};
            robot.adapter.client.web.chat.postMessage(msg.message.room, text, options);
          } else {
            msg.send(text);
          }
        } else {
          msg.send('No se han encontrado resultados sobre '+ busqueda);
        }

      }
    });

  });

};<|endoftext|>


<commit_before>var camera, scene, renderer, mesh;

function init () {
  var tunnel = new THREE.BoxGeometry( 600, 1200, 3000 );

  var material = new THREE.MeshBasicMaterial({
    color: 0xaffff,
    opacity: 0.5,
    wireframe: true,
    transparent: true
  })

  var tunnelMesh = new THREE.Mesh( tunnel, material );
  tunnelMesh.position.y = 300;
  tunnelMesh.position.z = -1200;

  //

  renderer = new THREE.WebGLRenderer({
    alpha: true
  });
  renderer.setPixelRatio( window.devicePixelRatio );
  renderer.setSize( window.innerWidth, window.innerHeight );
  document.body.appendChild( renderer.domElement );

  //

  camera = new THREE.PerspectiveCamera( 70, window.innerWidth /
    window.innerHeight, 1, 4000 );

  camera.position.y = 400;
  camera.position.z = 100;

  //

  scene = new THREE.Scene();
  scene.add( tunnelMesh );

  window.addEventListener( 'resize', onWindowResize, false );
  document.addEventListener( 'keyup', onKeyUp, false);

  startObstacles();
  addVideoFeed();
}

function startObstacles () {
  setInterval(function(){
    var geometry = new THREE.BoxGeometry( 100, 100, 100 );

    var material = new THREE.MeshBasicMaterial({
      color: 0xaff00,
      wireframe: true,
      wireframeLinewidth: 2
    });

    mesh = new THREE.Mesh( geometry, material );
    mesh.position.y = 400;
    mesh.position.z = -1500;

    var position = {
        x: mesh.position.x,
        y: mesh.position.y,
        z: mesh.position.z
      },
      target = {
        x: 0,
        y: 0,
        z: 3000
      };

    scene.add( mesh );

    doTween(position, target, mesh, TWEEN.Easing.Circular.Out, 100000);

  }, Math.random() * 100 + 3000);
}

function onWindowResize() {

  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();

  renderer.setSize( window.innerWidth, window.innerHeight );

}

function animate() {

  requestAnimationFrame( animate );

  TWEEN.update();

  renderer.render( scene, camera );

}

var cameraMaxY = 400,
  cameraMinY = 200,
  cameraMaxX = 300,
  cameraMinX = -100;

function onKeyUp (event) {
  var increment = 100,
    position = {
      x: camera.position.x,
      y: camera.position.y,
      z: camera.position.z
    },
    target = {
      x: 0,
      y: 0,
      z: 0
    },
    tween;

  event.preventDefault();

  switch(event.keyCode) {
    case 38: //Up
      if (camera.position.y < cameraMaxY) {
        target.y = cameraMaxY;
        target.z = camera.position.z;

        doTween(position, target, camera, TWEEN.Easing.Circular.Out, 100);
      }
      break;

    case 40: //Down
      if (camera.position.y > cameraMinY) {
        target.y = cameraMinY;
        target.z = camera.position.z;

        doTween(position, target, camera, TWEEN.Easing.Circular.Out, 100);
      }
      break;

    // case 39: //Right
    //   break;

    // case 37:// left
    //   break;
  }

}

function doTween (position, target, obj, easing, time) {
  var tween = new TWEEN.Tween(position).to(target, time);

  tween.onUpdate(function(){
    obj.position.x = position.x;
    obj.position.y = position.y;
    obj.position.z = position.z;
  });

  tween.easing(easing);

  tween.start();
}

function videoSuccess (stream) {
  video.src = stream;
  video.play();
}

function videoError (err) {
  console.log(err);
}

function sourceSelected (videoSource) {
  var constraints = {
    audio: {
      optional: [{sourceId: audioSource}]
    },
    video: {
      optional: [{sourceId: videoSource}]
    }
  };

  navigator.getUserMedia(constraints, videoSuccess, videoError);
}

function addVideoFeed () {
  MediaStreamTrack.getSources(function(sourceInfos) {
    // var audioSource = null;
    var videoSource = null;

    for (var i = 0; i != sourceInfos.length; ++i) {
      var sourceInfo = sourceInfos[i];
      // if (sourceInfo.kind === 'audio') {
      //   console.log(sourceInfo.id, sourceInfo.label || 'microphone');
      //
      //   audioSource = sourceInfo.id;
      // } else
      if (sourceInfo.kind === 'video') {
        console.log(sourceInfo.id, sourceInfo.label || 'camera');

        videoSource = sourceInfo.id;
      } else {
        console.log('Some other kind of source: ', sourceInfo);
      }
    }

    // sourceSelected(audioSource, videoSource);
    sourceSelected(videoSource);
  });
    // maybe fall back to this
    // console.log("add video");
    //
    // // Grab elements, create settings, etc.
    // var video = document.getElementById("video"),
    //   videoObj = { "video": true },
    //   errBack = function(error) {
    //     console.log("Video capture error: ", error.code);
    //   };
    //
    // // Put video listeners into place
    // if (navigator.getUserMedia) { // Standard
    //   navigator.getUserMedia(videoObj, function(stream) {
    //     video.src = stream;
    //     video.play();
    //   }, errBack);
    // } else if (navigator.webkitGetUserMedia) { // WebKit-prefixed
    //   console.log("get video");
    //   navigator.webkitGetUserMedia(videoObj, function(stream){
    //     console.log("navigator video");
    //     video.src = window.webkitURL.createObjectURL(stream);
    //     video.play();
    //
    //     console.log(video);
    //   }, errBack);
    // }
    // else if (navigator.mozGetUserMedia) { // Firefox-prefixed
    //   navigator.mozGetUserMedia(videoObj, function(stream){
    //     video.src = window.URL.createObjectURL(stream);
    //     video.play();
    //   }, errBack);
    // }

}

init();
animate();
<commit_msg>removing audio<commit_after>var camera, scene, renderer, mesh;

function init () {
  var tunnel = new THREE.BoxGeometry( 600, 1200, 3000 );

  var material = new THREE.MeshBasicMaterial({
    color: 0xaffff,
    opacity: 0.5,
    wireframe: true,
    transparent: true
  })

  var tunnelMesh = new THREE.Mesh( tunnel, material );
  tunnelMesh.position.y = 300;
  tunnelMesh.position.z = -1200;

  //

  renderer = new THREE.WebGLRenderer({
    alpha: true
  });
  renderer.setPixelRatio( window.devicePixelRatio );
  renderer.setSize( window.innerWidth, window.innerHeight );
  document.body.appendChild( renderer.domElement );

  //

  camera = new THREE.PerspectiveCamera( 70, window.innerWidth /
    window.innerHeight, 1, 4000 );

  camera.position.y = 400;
  camera.position.z = 100;

  //

  scene = new THREE.Scene();
  scene.add( tunnelMesh );

  window.addEventListener( 'resize', onWindowResize, false );
  document.addEventListener( 'keyup', onKeyUp, false);

  startObstacles();
  addVideoFeed();
}

function startObstacles () {
  setInterval(function(){
    var geometry = new THREE.BoxGeometry( 100, 100, 100 );

    var material = new THREE.MeshBasicMaterial({
      color: 0xaff00,
      wireframe: true,
      wireframeLinewidth: 2
    });

    mesh = new THREE.Mesh( geometry, material );
    mesh.position.y = 400;
    mesh.position.z = -1500;

    var position = {
        x: mesh.position.x,
        y: mesh.position.y,
        z: mesh.position.z
      },
      target = {
        x: 0,
        y: 0,
        z: 3000
      };

    scene.add( mesh );

    doTween(position, target, mesh, TWEEN.Easing.Circular.Out, 100000);

  }, Math.random() * 100 + 3000);
}

function onWindowResize() {

  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();

  renderer.setSize( window.innerWidth, window.innerHeight );

}

function animate() {

  requestAnimationFrame( animate );

  TWEEN.update();

  renderer.render( scene, camera );

}

var cameraMaxY = 400,
  cameraMinY = 200,
  cameraMaxX = 300,
  cameraMinX = -100;

function onKeyUp (event) {
  var increment = 100,
    position = {
      x: camera.position.x,
      y: camera.position.y,
      z: camera.position.z
    },
    target = {
      x: 0,
      y: 0,
      z: 0
    },
    tween;

  event.preventDefault();

  switch(event.keyCode) {
    case 38: //Up
      if (camera.position.y < cameraMaxY) {
        target.y = cameraMaxY;
        target.z = camera.position.z;

        doTween(position, target, camera, TWEEN.Easing.Circular.Out, 100);
      }
      break;

    case 40: //Down
      if (camera.position.y > cameraMinY) {
        target.y = cameraMinY;
        target.z = camera.position.z;

        doTween(position, target, camera, TWEEN.Easing.Circular.Out, 100);
      }
      break;

    // case 39: //Right
    //   break;

    // case 37:// left
    //   break;
  }

}

function doTween (position, target, obj, easing, time) {
  var tween = new TWEEN.Tween(position).to(target, time);

  tween.onUpdate(function(){
    obj.position.x = position.x;
    obj.position.y = position.y;
    obj.position.z = position.z;
  });

  tween.easing(easing);

  tween.start();
}

function videoSuccess (stream) {
  video.src = stream;
  video.play();
}

function videoError (err) {
  console.log(err);
}

function sourceSelected (videoSource) {
  var constraints = {
    // audio: {
    //   optional: [{sourceId: audioSource}]
    // },
    video: {
      optional: [{sourceId: videoSource}]
    }
  };

  navigator.getUserMedia(constraints, videoSuccess, videoError);
}

function addVideoFeed () {
  MediaStreamTrack.getSources(function(sourceInfos) {
    // var audioSource = null;
    var videoSource = null;

    for (var i = 0; i != sourceInfos.length; ++i) {
      var sourceInfo = sourceInfos[i];
      // if (sourceInfo.kind === 'audio') {
      //   console.log(sourceInfo.id, sourceInfo.label || 'microphone');
      //
      //   audioSource = sourceInfo.id;
      // } else
      if (sourceInfo.kind === 'video') {
        console.log(sourceInfo.id, sourceInfo.label || 'camera');

        videoSource = sourceInfo.id;
      } else {
        console.log('Some other kind of source: ', sourceInfo);
      }
    }

    // sourceSelected(audioSource, videoSource);
    sourceSelected(videoSource);
  });
    // maybe fall back to this
    // console.log("add video");
    //
    // // Grab elements, create settings, etc.
    // var video = document.getElementById("video"),
    //   videoObj = { "video": true },
    //   errBack = function(error) {
    //     console.log("Video capture error: ", error.code);
    //   };
    //
    // // Put video listeners into place
    // if (navigator.getUserMedia) { // Standard
    //   navigator.getUserMedia(videoObj, function(stream) {
    //     video.src = stream;
    //     video.play();
    //   }, errBack);
    // } else if (navigator.webkitGetUserMedia) { // WebKit-prefixed
    //   console.log("get video");
    //   navigator.webkitGetUserMedia(videoObj, function(stream){
    //     console.log("navigator video");
    //     video.src = window.webkitURL.createObjectURL(stream);
    //     video.play();
    //
    //     console.log(video);
    //   }, errBack);
    // }
    // else if (navigator.mozGetUserMedia) { // Firefox-prefixed
    //   navigator.mozGetUserMedia(videoObj, function(stream){
    //     video.src = window.URL.createObjectURL(stream);
    //     video.play();
    //   }, errBack);
    // }

}

init();
animate();
<|endoftext|>


<commit_before>var path = require('path');
var mkdirp = require('mkdirp');
var fs = require('fs');
var _ = require('lodash');
const execSync = require('child_process').execSync;

var Logger = require( __configs + "loggingConfig");
var Errors = require(__components + "Errors/errors");

module.exports =  {

   /* Helper functions */
    getYearMonthDayStr:function (){
        var dateObj = new Date();
        var arr = [ dateObj.getFullYear(), dateObj.getMonth() + 1, dateObj.getDate()];
        return arr.join('-');
    },

    replaceExt: function( filename, newExt ) {
      var ext = path.extname(filename);
      return filename.replace(ext, newExt);
    },

    getExt: function ( filename ) {
        if(!filename)
            return "";

        var extId = filename.lastIndexOf(".");
        return (extId >= 0 ? filename.substr(extId+1) : "");
    },

    // This checks zip, tar and tz. (Only tested zip)
    isZip: function (filename) {
        var ext = path.extname(filename);
        return  ext == ".zip" || ext == ".tar" || ext == '.gz';
    },
    // This check .odt, .docx and .doc  files
    isDoc: function ( filename ) {

        var ext = path.extname(filename);
        return  ext == ".doc" || ext == ".docx" || ext == '.odt';
    },

    isSrcExt: function( filename ) {
        var ext = ['c', "cpp", "cc"];
        return _.includes(ext, this.getExt( filename) );
    },

    validateProgrammingFiles: function( filesInfo ) {

        // Check only one file for zip submission
        var countZip = 0, countSrc = 0;
        for( var i = 0; i < filesInfo.length; i++) {
            if( this.isZip(filesInfo[i].filename) )
                countZip++;
            else if( this.isSrcExt(filesInfo[i].filename) )
                countSrc++;
        }

        if( countZip > 1 || (countZip == 1 && filesInfo.length > 1) ) {
            return Errors.cLogErr("Invalid submission, please submit one file for zip files.");
        }

        if( countZip == 0 && countSrc == 0 )
            return Errors.cLogErr("Invalid submission, please submit one source file.");
        return {};
    },

    validateWritingFiles: function( filesInfo ) {
        //TODO Nothing for now
        return {};
    },

    // This funciton mimicks what would happen if students submitted via a zip file.
    // Place everything in another folder, could eventually even move certain files
    // too appropriate directories ( if needed )
    createProgrammingProject: function( filesInfo, options ) {

        if(filesInfo.length >= 1) {

            options = options || {'dir':'/unzipped'};
            var directory = filesInfo[0].destination;
            var zipDir = path.join( directory, options['dir']);
            var folderCreated = mkdirp.sync( zipDir );
            Errors.cl(folderCreated);

            if( folderCreated ) {
                try {
                    for( var i = 0; i < filesInfo.length; i++ ) {
                        var call ="mv " + filesInfo[i].filename + " " + zipDir
                        execSync( call );
                    }

                    // These will point to the directory and be handled later.
                    // Destroy the original array and replace with a single instance to directory.
                    filesInfo = [ {
                        'filename': zipDir,
                        'originalname': zipDir,
                        'destination': directory,
                        'path': 'directory',
                    }];
                    return filesInfo;
                }
                catch(e) {
                    return Errors.cErr("Cannot copy source files provided");
                }
            }
            else {
                Errors.cl( zipDir );
                return Errors.cErr("Can not create programming create folder");
            }
        }
    },

    /* Wrapper for all file types handling
        Function should take files array (containing basic info for each file)
        Check that unzipping and conversions can function
        update file names to appropritate names (doc changes to text)
        otherwise leave file information unchanged.
        Note: You can/should only handle the fileInfo once.
    */
    handleFileTypes: function( req, res ) {

          // Get files names to be inserted
        var uploadedFiles = this.getFileNames( req.files );

        if( !uploadedFiles || uploadedFiles.length == 0 )
        {
            var e = Errors.cLogErr( "Unable to process uploaded files" );
            return e;
        }

        var toolType = req.session.toolSelect;
        if( toolType == "Programming") {
            var validate = this.validateProgrammingFiles( uploadedFiles );
            if( Errors.hasErr(validate) ) {
                Errors.logErr(validate);
                return validate;
            }

            var fileInfo = uploadedFiles[0];
            var res = this.handleZipFile( fileInfo );
            if( Errors.hasErr(res) ) {
                // Could not handl zipped format.
                Errors.logErr(res);
                return res;
            }
            else if( _.has(res,'res') ) {
                // Get the directory information
                 fileInfo = res.res;
            }
            else {
                uploadedFiles = this.createProgrammingProject(uploadedFiles);
            }
        }
        else if( toolType == "Writing") {
            var validate = this.validateWritingFiles( uploadedFiles );
            if( Errors.hasErr(validate) ) {
                Errors.logErr(res);
                return validate;
            }
            for( var i = 0; i < uploadedFiles.length;i++) {
                var fileInfo = uploadedFiles[i];
                res = this.handleDocxFile( fileInfo );
                if( Errors.logErr(res)) {
                    // TODO: we need to stop
                    Errors.logErr(res);
                    return res;
                }
                else if(_.has(res,'res')  ){
                    //  file was handled
                    fileInfo = res.res;
                }
            }
        }
        return uploadedFiles;
    },

    separateFiles: function( files, fileTypes) {
        var res = {}
        for( var i = 0; i < files.length; i++ ) {
            var ext = this.getExt( files[i] );
            if( _.includes(fileTypes, ext) ){
                if(_.includes(res, ext) ) {
                    res[ext].push( files[i] )
                }
                else
                    res[ext] = [ files[i] ] ;
            }
        }
        return res;
    },

    findFilesSync: function( startDir ) {

        if( !fs.lstatSync(startDir).isDirectory() ) {
            return Errors.cLogErr("Directory:" + startDir + " does not exists");
        }

        var arr = [];
        var files = fs.readdirSync(startDir);
        for( var i = 0; i < files.length; i++ ) {
            var filename = path.join( startDir, files[i] );
            var stat = fs.lstatSync(filename);
            if( stat.isDirectory() ) {
                arr = arr.concat( this.findFilesSync( filename ) );
            }
            else
                arr.push( filename );
        }
        return arr;
    },

    handleZipFile: function ( fileInfo )
    {
        var res = {};
        var zipfile = fileInfo.filename;
        if( zipfile && this.isZip(zipfile) ){

            var zipDir = path.join( path.dirname(zipfile), '/unzipped');
            try {
                var unarc = this.unarchieve( zipfile, {'dir':zipDir} );
                if( Errors.ifErrLog(unarc) ) {
                    return unarc;
                }
                else {
                    var files = this.findFilesSync(zipDir);
                    var fileGroups = _.groupBy(res, this.getExt);

                    if( fileGroups ){
                        var isValidProject = this.validateProjectStructure(fileGroups);
                        if( Errors.ifErrLog(isValidProject) )
                            return isValidProject

                        // These will not point to the proper directory.
                        fileInfo.filename = zipDir;
                        fileInfo.originalname = zipDir;
                        res = { res: zipDir };
                    }
                }
            }
            catch(err) {
                var emsg =  "Unable to extract : " + fileInfo.originalname;
                Logger.error(err.name + ': ' + err.message);
                res.err = { msg:emsg };
            }
        }
        return res;
    },

    // This function takes the isDoc type and creates a .txt file with the same name
    // and in the same folder.
    // (TODO) Error is logged but not handled.
    handleDocxFile: function ( fileInfo )
    {
        var res = {};
        var filename = fileInfo.filename;
        if( filename && this.isDoc(filename) )
        {
            var childProcess = require('child_process');
            var outDir = path.dirname(filename);
            var args =  [ "--headless", "--convert-to","txt:Text", filename, '--outdir', outDir ];

            // This call soffice (Libra to handle conversion)
            var convertToTxt = childProcess.spawnSync('soffice', args );

            if(convertToTxt.error) {
                var e = Errors.cLogErr("Unable to convert document file: " + fileInfo.originalname);
                return e;
            }
            else {
                // Make the text file the reference file instead of .doc
                fileInfo.filename = this.replaceExt(fileInfo.filename, ".txt");
                fileInfo.originalname = this.replaceExt(fileInfo.originalname, ".txt");
                res = { res: fileInfo };
            }
        }
        return res;
    },

    /* Function will un-archieve a zip or gz file
        By Default: will create a new folder unzipped and extract to it.
    */
    unarchieve: function ( file, options ) {
        options = options || {};
        options.dir = options.dir || "./unzipped";
        var res = {}
        try {
            var call = "";
            var folderCreated = false;
            var ext = this.getExt(file );

            if( ext == 'gz') {
                folderCreated = mkdirp.sync( options.dir );
                options.params = options.params || "-xzf";
                call = "tar " + options.params + " " + file + " -C " + options.dir;
            }
            else if( ext == 'zip')  {
                folderCreated = true;
                options.params = options.params || " -q ";
                call ="unzip " + options.params + file + " -d " + options.dir;
            }
            if( folderCreated ) {
                execSync( call );
            }
            else {
                throw Errors.cLogErr("Unable to unzip project folder.");
            }
        }
        catch ( e ) {
            var e = Errors.cErrLog("Unable to extract files: " + file);
            return e;
        }

        return {};
    },

    // Minor validation of the project, including checking for a makefile, *c and *.h files
    validateProjectStructure: function ( zipDir, groupedFiles ) {

        var res = {};
        if( groupedFiles ) {
            var noExts = _.get(groupedFiles, "");
            var makeFile = path.join(zipDir,'Makefile');
            var hasMakeFile = _.includes(noExts, makeFile) || _.includes(noExts, _.lowerCase(makeFile) );

            if( !noExts || !hasMakeFile ) {
                return Errors.cLogErr("Unable to identify project makefile, please ensure file you've included a makefile at the top level of your project.");
            }

            var cFiles = _.get(groupedFiles,"c");
            var hFiles = _.get(groupedFiles,'h');

            if( !cFiles ) {
                return Errors.cLogErr("Unable to locate source (.c) files in project.");
            }

            if( !hFiles )  {
                return Errors.cLogErr("Unable to locate header (.h) files in project.");
            }
        }
        //Everything looks ok.
        return {};
    },

    // This function gets the file names from Multer, appends the uploads directory
    // and keeps only the "likely" required keys for future processing.
    getFileNames: function ( files ) {
        // Retrieve all files original name and new server name
        var keys = [ 'originalname', 'filename','destination'];
        var uploadedFiles = _.map( files, obj => _.pick(obj, keys) );

        // Append uploads directory.
        _.forEach( uploadedFiles, function(f ){
            _.update(f, 'filename', obj => f.destination + "/" +  f.filename);
        });
        return uploadedFiles;
    },

    writeResults: function( obj, options ) {
        var filename = options.file;
        var uploadDir = path.dirname(  options.filepath );
        var file = path.join(uploadDir,filename);
        Logger.info("Writing", file, "now", file);
        fs.writeFileSync( file , JSON.stringify(obj), 'utf-8');
        return file;
    },

    // This function mimicks the file object provided by Multer
    // If more fields are discovered to be useful just add them.
    createFileObject: function( filename ) {
        return {
            'originalname': path.basename(filename),
            'filename':filename,
            'content': '',
        };
    }
};<commit_msg>Improving the file upload failure message<commit_after>var path = require('path');
var mkdirp = require('mkdirp');
var fs = require('fs');
var _ = require('lodash');
const execSync = require('child_process').execSync;

var Logger = require( __configs + "loggingConfig");
var Errors = require(__components + "Errors/errors");

module.exports =  {

   /* Helper functions */
    getYearMonthDayStr:function (){
        var dateObj = new Date();
        var arr = [ dateObj.getFullYear(), dateObj.getMonth() + 1, dateObj.getDate()];
        return arr.join('-');
    },

    replaceExt: function( filename, newExt ) {
      var ext = path.extname(filename);
      return filename.replace(ext, newExt);
    },

    getExt: function ( filename ) {
        if(!filename)
            return "";

        var extId = filename.lastIndexOf(".");
        return (extId >= 0 ? filename.substr(extId+1) : "");
    },

    // This checks zip, tar and tz. (Only tested zip)
    isZip: function (filename) {
        var ext = path.extname(filename);
        return  ext == ".zip" || ext == ".tar" || ext == '.gz';
    },
    // This check .odt, .docx and .doc  files
    isDoc: function ( filename ) {

        var ext = path.extname(filename);
        return  ext == ".doc" || ext == ".docx" || ext == '.odt';
    },

    isSrcExt: function( filename ) {
        var ext = ['c', "cpp", "cc"];
        return _.includes(ext, this.getExt( filename) );
    },

    validateProgrammingFiles: function( filesInfo ) {

        // Check only one file for zip submission
        var countZip = 0, countSrc = 0;
        for( var i = 0; i < filesInfo.length; i++) {
            if( this.isZip(filesInfo[i].filename) )
                countZip++;
            else if( this.isSrcExt(filesInfo[i].filename) )
                countSrc++;
        }

        if( countZip > 1 || (countZip == 1 && filesInfo.length > 1) ) {
            return Errors.cLogErr("Invalid submission, please submit one file for zip files.");
        }

        if( countZip == 0 && countSrc == 0 )
            return Errors.cLogErr("Invalid submission, please submit one source file.");
        return {};
    },

    validateWritingFiles: function( filesInfo ) {
        //TODO Nothing for now
        return {};
    },

    // This funciton mimicks what would happen if students submitted via a zip file.
    // Place everything in another folder, could eventually even move certain files
    // too appropriate directories ( if needed )
    createProgrammingProject: function( filesInfo, options ) {

        if(filesInfo.length >= 1) {

            options = options || {'dir':'/unzipped'};
            var directory = filesInfo[0].destination;
            var zipDir = path.join( directory, options['dir']);
            var folderCreated = mkdirp.sync( zipDir );
            Errors.cl(folderCreated);

            if( folderCreated ) {
                try {
                    for( var i = 0; i < filesInfo.length; i++ ) {
                        var call ="mv " + filesInfo[i].filename + " " + zipDir
                        execSync( call );
                    }

                    // These will point to the directory and be handled later.
                    // Destroy the original array and replace with a single instance to directory.
                    filesInfo = [ {
                        'filename': zipDir,
                        'originalname': zipDir,
                        'destination': directory,
                        'path': 'directory',
                    }];
                    return filesInfo;
                }
                catch(e) {
                    return Errors.cErr("Cannot copy source files provided");
                }
            }
            else {
                Errors.cl( zipDir );
                return Errors.cErr("Can not create programming create folder");
            }
        }
    },

    /* Wrapper for all file types handling
        Function should take files array (containing basic info for each file)
        Check that unzipping and conversions can function
        update file names to appropritate names (doc changes to text)
        otherwise leave file information unchanged.
        Note: You can/should only handle the fileInfo once.
    */
    handleFileTypes: function( req, res ) {

          // Get files names to be inserted
        var uploadedFiles = this.getFileNames( req.files );

        if( !uploadedFiles || uploadedFiles.length == 0 )
        {
            var e = Errors.cLogErr( "Unable to process upload files. Please only upload word, libra office, or text documents." );
            if( req.session.toolSelect == "Programming" ) {
                e = Errors.cLogErr( "Unable to process upload files. Please only upload zipped/tar project files or .c or .h files." );
            }
            return e;
        }

        var toolType = req.session.toolSelect;
        if( toolType == "Programming") {
            var validate = this.validateProgrammingFiles( uploadedFiles );
            if( Errors.hasErr(validate) ) {
                Errors.logErr(validate);
                return validate;
            }

            var fileInfo = uploadedFiles[0];
            var res = this.handleZipFile( fileInfo );
            if( Errors.hasErr(res) ) {
                // Could not handl zipped format.
                Errors.logErr(res);
                return res;
            }
            else if( _.has(res,'res') ) {
                // Get the directory information
                 fileInfo = res.res;
            }
            else {
                uploadedFiles = this.createProgrammingProject(uploadedFiles);
            }
        }
        else if( toolType == "Writing") {
            var validate = this.validateWritingFiles( uploadedFiles );
            if( Errors.hasErr(validate) ) {
                Errors.logErr(res);
                return validate;
            }
            for( var i = 0; i < uploadedFiles.length;i++) {
                var fileInfo = uploadedFiles[i];
                res = this.handleDocxFile( fileInfo );
                if( Errors.logErr(res)) {
                    // TODO: we need to stop
                    Errors.logErr(res);
                    return res;
                }
                else if(_.has(res,'res')  ){
                    //  file was handled
                    fileInfo = res.res;
                }
            }
        }
        return uploadedFiles;
    },

    separateFiles: function( files, fileTypes) {
        var res = {}
        for( var i = 0; i < files.length; i++ ) {
            var ext = this.getExt( files[i] );
            if( _.includes(fileTypes, ext) ){
                if(_.includes(res, ext) ) {
                    res[ext].push( files[i] )
                }
                else
                    res[ext] = [ files[i] ] ;
            }
        }
        return res;
    },

    findFilesSync: function( startDir ) {

        if( !fs.lstatSync(startDir).isDirectory() ) {
            return Errors.cLogErr("Directory:" + startDir + " does not exists");
        }

        var arr = [];
        var files = fs.readdirSync(startDir);
        for( var i = 0; i < files.length; i++ ) {
            var filename = path.join( startDir, files[i] );
            var stat = fs.lstatSync(filename);
            if( stat.isDirectory() ) {
                arr = arr.concat( this.findFilesSync( filename ) );
            }
            else
                arr.push( filename );
        }
        return arr;
    },

    handleZipFile: function ( fileInfo )
    {
        var res = {};
        var zipfile = fileInfo.filename;
        if( zipfile && this.isZip(zipfile) ){

            var zipDir = path.join( path.dirname(zipfile), '/unzipped');
            try {
                var unarc = this.unarchieve( zipfile, {'dir':zipDir} );
                if( Errors.ifErrLog(unarc) ) {
                    return unarc;
                }
                else {
                    var files = this.findFilesSync(zipDir);
                    var fileGroups = _.groupBy(res, this.getExt);

                    if( fileGroups ){
                        var isValidProject = this.validateProjectStructure(fileGroups);
                        if( Errors.ifErrLog(isValidProject) )
                            return isValidProject

                        // These will not point to the proper directory.
                        fileInfo.filename = zipDir;
                        fileInfo.originalname = zipDir;
                        res = { res: zipDir };
                    }
                }
            }
            catch(err) {
                var emsg =  "Unable to extract : " + fileInfo.originalname;
                Logger.error(err.name + ': ' + err.message);
                res.err = { msg:emsg };
            }
        }
        return res;
    },

    // This function takes the isDoc type and creates a .txt file with the same name
    // and in the same folder.
    // (TODO) Error is logged but not handled.
    handleDocxFile: function ( fileInfo )
    {
        var res = {};
        var filename = fileInfo.filename;
        if( filename && this.isDoc(filename) )
        {
            var childProcess = require('child_process');
            var outDir = path.dirname(filename);
            var args =  [ "--headless", "--convert-to","txt:Text", filename, '--outdir', outDir ];

            // This call soffice (Libra to handle conversion)
            var convertToTxt = childProcess.spawnSync('soffice', args );

            if(convertToTxt.error) {
                var e = Errors.cLogErr("Unable to convert document file: " + fileInfo.originalname);
                return e;
            }
            else {
                // Make the text file the reference file instead of .doc
                fileInfo.filename = this.replaceExt(fileInfo.filename, ".txt");
                fileInfo.originalname = this.replaceExt(fileInfo.originalname, ".txt");
                res = { res: fileInfo };
            }
        }
        return res;
    },

    /* Function will un-archieve a zip or gz file
        By Default: will create a new folder unzipped and extract to it.
    */
    unarchieve: function ( file, options ) {
        options = options || {};
        options.dir = options.dir || "./unzipped";
        var res = {}
        try {
            var call = "";
            var folderCreated = false;
            var ext = this.getExt(file );

            if( ext == 'gz') {
                folderCreated = mkdirp.sync( options.dir );
                options.params = options.params || "-xzf";
                call = "tar " + options.params + " " + file + " -C " + options.dir;
            }
            else if( ext == 'zip')  {
                folderCreated = true;
                options.params = options.params || " -q ";
                call ="unzip " + options.params + file + " -d " + options.dir;
            }
            if( folderCreated ) {
                execSync( call );
            }
            else {
                throw Errors.cLogErr("Unable to unzip project folder.");
            }
        }
        catch ( e ) {
            var e = Errors.cErrLog("Unable to extract files: " + file);
            return e;
        }

        return {};
    },

    // Minor validation of the project, including checking for a makefile, *c and *.h files
    validateProjectStructure: function ( zipDir, groupedFiles ) {

        var res = {};
        if( groupedFiles ) {
            var noExts = _.get(groupedFiles, "");
            var makeFile = path.join(zipDir,'Makefile');
            var hasMakeFile = _.includes(noExts, makeFile) || _.includes(noExts, _.lowerCase(makeFile) );

            if( !noExts || !hasMakeFile ) {
                return Errors.cLogErr("Unable to identify project makefile, please ensure file you've included a makefile at the top level of your project.");
            }

            var cFiles = _.get(groupedFiles,"c");
            var hFiles = _.get(groupedFiles,'h');

            if( !cFiles ) {
                return Errors.cLogErr("Unable to locate source (.c) files in project.");
            }

            if( !hFiles )  {
                return Errors.cLogErr("Unable to locate header (.h) files in project.");
            }
        }
        //Everything looks ok.
        return {};
    },

    // This function gets the file names from Multer, appends the uploads directory
    // and keeps only the "likely" required keys for future processing.
    getFileNames: function ( files ) {
        // Retrieve all files original name and new server name
        var keys = [ 'originalname', 'filename','destination'];
        var uploadedFiles = _.map( files, obj => _.pick(obj, keys) );

        // Append uploads directory.
        _.forEach( uploadedFiles, function(f ){
            _.update(f, 'filename', obj => f.destination + "/" +  f.filename);
        });
        return uploadedFiles;
    },

    writeResults: function( obj, options ) {
        var filename = options.file;
        var uploadDir = path.dirname(  options.filepath );
        var file = path.join(uploadDir,filename);
        Logger.info("Writing", file, "now", file);
        fs.writeFileSync( file , JSON.stringify(obj), 'utf-8');
        return file;
    },

    // This function mimicks the file object provided by Multer
    // If more fields are discovered to be useful just add them.
    createFileObject: function( filename ) {
        return {
            'originalname': path.basename(filename),
            'filename':filename,
            'content': '',
        };
    }
};<|endoftext|>