ui/controllers/job_packaging_controller.js

const $ = require('jquery');
const { AppSetting } = require('../../core/app_setting');
const { BagItProfile } = require('../../bagit/bagit_profile');
const { BaseController } = require('./base_controller');
const { Constants } = require('../../core/constants');
const { Context } = require('../../core/context');
const { Job } = require('../../core/job');
const { JobPackageOpForm } = require('../forms/job_package_op_form');
const path = require('path');
const Templates = require('../common/templates');
const { Util } = require('../../core/util');
const { constants } = require('buffer');

/**
 * The JobPackaingController presents the page that allows users
 * to define how a Job's files should be packaged.
 *
 * @param {URLSearchParams} params - The URL search params parsed
 * from the URL used to reach this page. This should contain at
 * least the Job Id.
 *
 * @param {string} params.id - The id of the Job being worked
 * on. Job.id is a UUID string.
 */
class JobPackagingController extends BaseController {

    constructor(params) {
        super(params, 'Jobs');
        this.model = Job;
        this.job = Job.find(this.params.get('id'));
    }

    /**
     * This displays a form where users can choose how this
     * Job's files should be packaged.
     */
    show() {
        let form = new JobPackageOpForm(this.job);
        return this._renderPackagingForm(form);
    }

    /**
     * This renders the form that allows users to choose how
     * this job's files should be packaged.
     */
    _renderPackagingForm(form) {
        let data = {
            job: this.job,
            form: form
        }
        let html = Templates.jobPackaging(data);
        return this.containerContent(html);
    }

    /**
     * This parses form input from the user and assigns the
     * values to the Job's {@link PackageOperation}.
     */
    _parseJobPackagingForm() {
        let form = new JobPackageOpForm(this.job);
        form.parseFromDOM();
        this.job.packageOp.packageFormat = form.obj.packageFormat;
        this.job.packageOp.pluginId = form.obj.pluginId;
        this.job.packageOp.bagItSerialization = form.obj.bagItSerialization;
        this.job.packageOp.outputPath = form.obj.outputPath;
        this.job.packageOp.packageName = form.obj.packageName;

        // Load the BagIt Profile only if necessary. If the user is
        // moving backwards through the form, the profile may already
        // be saved with custom values. We don't want to overwrite it
        // by reloading it.
        let needsProfile = (this.job.bagItProfile == null && form.obj.bagItProfileId);
        let selectedProfileChanged = (this.job.bagItProfile && form.obj.bagItProfileId && form.obj.bagItProfileId != this.job.bagItProfile.id);
        let profileShouldBeRemoved = (this.job.bagItProfile && !form.obj.bagItProfileId)
        if (needsProfile || selectedProfileChanged) {
            this.job.bagItProfile = BagItProfile.find(form.obj.bagItProfileId);
        } else if (profileShouldBeRemoved) {
            this.job.bagItProfile = null;
        }
        return form;
    }

    /**
     * This saves changes to the Job's {@link PackageOperation},
     * optionally validating those changes first. If the withValidation
     * parameter is true and the changes are not valid, this will not
     * save the changes.
     *
     * @param {boolean} withValidation - If this is true, this method
     * will validate the user's changes before trying to save them, and
     * it will not save invalid changes. If false, this saves the users
     * changes without validating them.
     */
    _updatePackaging(withValidation) {
        let form = this._parseJobPackagingForm();
        if (withValidation) {
            if (this.job.packageOp.packageFormat == Context.y18n.__('Choose One')) {
                form.obj.errors['packageFormat'] = Context.y18n.__("You must specify a package format.");
            }
            if (this.job.packageOp.packageFormat == 'BagIt' && !this.job.bagItProfile) {
                form.obj.errors['bagItProfileId'] = Context.y18n.__("When choosing BagIt format, you must choose a BagIt profile.");
            }
            if (!this.job.packageOp.outputPath) {
                form.obj.errors['outputPath'] = Context.y18n.__("You must specify an output path.");
            }
            if (!this.job.packageOp.packageName) {
                form.obj.errors['packageName'] = Context.y18n.__("You must specify a package name.");
            }
            // TODO: Validate bag name.
        }
        if(!withValidation || (withValidation && !form.hasErrors())) {
            this.job.save();
        }
        return form
    }

    /**
     * This handles the click on the Back button, sending the user
     * back to the Job files page.
     */
    back() {
        this._updatePackaging(false);
        return this.redirect('JobFiles', 'show', this.params);
    }

    /**
     * This handles the Next button click, sending the user forward to
     * the Job metadata page if the the Job includes a bagging step.
     * If the Job does not include bagging, this sends the user ahead
     * to the Job upload page.
     */
    next() {
        let form = this._updatePackaging(true);
        if (form.hasErrors()) {
            // Errors. Stay on packaging screen.
            form.setErrors();
            form._listPackageFormats();
            form._listBagItProfiles();
            return this._renderPackagingForm(form);
        }
        else if (this.job.packageOp.packageFormat == 'BagIt') {
            return this.redirect('JobMetadata', 'show', this.params);
        } else {
            return this.redirect('JobUpload', 'show', this.params);
        }
    }

    /**
     * The postRenderCallback attaches event handlers to elements
     * that this controller has just rendered.
     */
    postRenderCallback(fnName) {
        $("select[name=packageFormat]").change(this.onFormatChange());
        $("select[name=bagItProfileId]").change(this.onProfileChange());
        $('#jobPackageOpForm_packageName').on('keyup', this.onPackageNameKeyUp());
        $('select[name=bagItSerialization]').change(this.onSerializationChange);
    }

    /**
     * This function loads a BagIt profile when the user selects one
     * from the list.
     */
    onProfileChange() {
        let controller = this;
        //let updateOutputPath = this.onPackageNameKeyUp();
        return function() {
            var format = $("select[name=packageFormat]").val();
            var profileId = $("select[name=bagItProfileId]").val();
            if (!profileId || format != 'BagIt') {
                controller.job.bagItProfile = null;
            } else {
                controller.job.bagItProfile = BagItProfile.find(profileId);
            }
            controller.updateSerializationOptions();
            controller.onPackageNameKeyUp();
        }
    }

    /**
     * This updates the options in the serialization list based on 
     * the allowed serialization values in the selected BagIt profile.
     */
    updateSerializationOptions() {
        let controller = this;
        let profile = controller.job.bagItProfile;
        let selectedValue = $('select[name=bagItSerialization]').val();
        let none = Context.y18n.__('None');
        let formats = [{ id: '', name: none }];
        let accepted = profile.acceptSerialization.filter(format => format in Constants.SUPPORTED_SERIALIZATION_EXTENSIONS).sort()
        if (accepted.length == 1 && profile.serialization == 'required') {
            // Only one format, and it's required
            formats = accepted;
            selectedValue = accepted[0];
        } else {
            // Multiple formats suppored and/or serialization optional.
            formats = formats.concat(accepted);
        }
        $('select[name=bagItSerialization]').empty();
        for (let mimeType of formats) {
            let extension = Constants.SERIALIZATION_EXTENSIONS[mimeType];
            let option = new Option(extension, extension, false, extension == selectedValue);
            $('select[name=bagItSerialization]').append(option);
        }
    }

    /**
     * This fires when the serialization select list option changes.
     * It sets the file extension of the output path to match the 
     * selected serialization option.
     */
    onSerializationChange() {
        let extension = $('select[name=bagItSerialization]').val().trim()
        if (extension == "undefined" || typeof(extension) == "undefined") {
            extension = ""
        }
        let baggingDir = AppSetting.firstMatching('name', 'Bagging Directory').value
        let packageName = $('#jobPackageOpForm_packageName').val().trim()
        if (packageName) {
            packageName = path.basename(packageName) + extension
            $('#jobPackageOpForm_outputPath').val(path.join(baggingDir, packageName));
        }
    }

    /**
     * This function shows or hides a list of BagIt profiles, based
     * on whether this job includes a bagging step. For jobs that
     * include bagging, the user must speficy a BagIt profile.
     */
    onFormatChange() {
        let job = this.job;
        let updateOutputPath = this.onPackageNameKeyUp();
        return function() {
            var format = $("select[name=packageFormat]").val();
            if (format == 'BagIt') {
                $('#jobProfileContainer').show();
                let profileId = $("select[name=bagItProfileId]").val();
                job.bagItProfile = BagItProfile.find(profileId);
            } else {
                $('#jobProfileContainer').hide();
                job.bagItProfile = null;
            }
            updateOutputPath();
        }
    }

    /**
     * Returns a function that sets the output path to a sane value
     * when the user changes the package name. The bag will be written
     * to this output path.
     */
    onPackageNameKeyUp() {
        let controller = this;
        return function(e) {
            let baggingDir = AppSetting.firstMatching('name', 'Bagging Directory').value;
            let packageName = $('#jobPackageOpForm_packageName').val().trim();
            var format = $("select[name=packageFormat]").val();
            let extension = $('select[name=bagItSerialization]').val();
            if (packageName && !packageName.endsWith(extension)) {
                packageName += extension;
            }
            if (extension == '') {
                // If user switched a package format having a file extension
                // to one with no extension, strip the extension from the
                // output path.
                packageName = Util.bagNameFromPath(packageName);
            }
            $('#jobPackageOpForm_outputPath').val(path.join(baggingDir, packageName));
        }
    }

}

module.exports.JobPackagingController = JobPackagingController;