Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
fun-apps / templates / cms / videoupload / js / videoupload.js
Size: Mime:
<%!
from django.utils.translation import ugettext as _
%>

<%
from django.core.urlresolvers import reverse
def reverse_course(handler_name, kwargs=None):
  kwargs = kwargs or {}
  kwargs['course_key_string'] = unicode(context_course.id)
  return reverse(handler_name, kwargs=kwargs)
%>
<%namespace name='static' file='../../static_content.html'/>

require(["jquery", "underscore", "backbone", "gettext",
         "js/utils/templates", "js/views/modals/base_modal", "common/js/components/views/feedback_notification",
         "videojs-fun", "videoplayer-fun"],
  function ($, _, Backbone, gettext,
    TemplateUtils, BaseModal, NotificationView,
    videojs, videoplayer) {

    var ajaxSettings = (function() {
      var headers = {};

      // Previous calls to $.ajaxSetup(...) might have defined default headers
      // to send with every ajax call. Also, ajax request errors activate a popup
      // on the bottom of the screen. We need to disable additional headers for
      // CORS requests, and errors are handled by each VideoView.

      function resetHeaders() {
        if(_.size(headers) > 0 && (!$.ajaxSettings.headers || _.size($.ajaxSettings.headers) == 0)) {
          $.ajaxSettings.headers = headers;
        }
      }
      function unsetHeaders() {
        if($.ajaxSettings.headers && _.size($.ajaxSettings.headers) > 0) {
          headers = $.ajaxSettings.headers;
          $.ajaxSettings.headers = {};
        }
      }
      function unsetNotify() {
        $.ajaxSettings.notifyOnError = false;
      }
      function resetNotify() {
        $.ajaxSettings.notifyOnError = true;
      }

      return {
        unsetHeaders: unsetHeaders,
        resetHeaders: resetHeaders,
        unsetNotify: unsetNotify,
        resetNotify: resetNotify,
      }
    })();

    function popError(error) {
      var notificationView = new NotificationView.Error({
        "title": gettext("Error"),
        "message": error,
      });
      notificationView.show();
    }

    function deactivateForm(elt) {
      $(elt).find("[type='submit']").attr('disabled', '');
    }
    function reactivateForm(elt) {
      $(elt).find("[type='submit'][disabled]").removeAttr('disabled');
    }

    // Status workflow for a video:
    // preparing -> prepared -> uploading -> uploaded -> creating -> created -> processing -> ready -> deleted
    var Video = Backbone.Model.extend({
      defaults: {
        created_at: "",
        created_at_timestamp: 0,
        embed_url: "",
        encoding_progress: 0,
        error: "",
        external_link: "",
        subtitles: [],
        title: "",
        thumbnail_url: "",
        video_sources: [],
        sync_pending: false,
      },

      url: function() {
        var baseUrl = '${reverse_course("videoupload:video", kwargs={"video_id": "videoid"})}';
        return baseUrl.replace("videoid", this.get("id"));
      },

      setStatus: function(status, data) {
        this.set("status", status);
        this.trigger("status-" + status, data);
      },

      setError: function(error) {
        this.setStatus("error");
        this.set("error", error);
      },

      wasUploaded: function() {
        var videoStatus = this.get("status");
        return videoStatus === "ready";
      },

      videoId: function() {
        return this.get("id") || "";
      },
    });

    var VideoCollection = Backbone.Collection.extend({
      model: Video,
      url: '${reverse_course("videoupload:videos")}',
      parse: function(data) {
        if (data.error) {
          this.trigger("syncError", data.error);
        }
        return data.videos;
      }
    });

    var VideoDeleteModalView = BaseModal.extend({
      options: $.extend({}, BaseModal.prototype.options, {
        title: gettext("Are you sure you want to delete this video?"),
        modalSize: "sm",
      }),

      events: $.extend({}, BaseModal.prototype.events, {
        "click .action-delete": "onClickConfirmDelete"
      }),

      getContentHtml: function() {
        return gettext("Once this video is deleted, it will no longer be available in the courses that include it. This action is irreversible.");
      },

      addActionButtons: function() {
        VideoDeleteModalView.__super__.addActionButtons.apply(this);
        this.addActionButton('delete', gettext('Delete'), true);
      },

      onClickConfirmDelete: function() {
        // This should trigger a DELETE request
        this.model.destroy();
        this.hide()
      }
    });

    var VideoView = Backbone.View.extend({
      tagName: 'tr',
      template: TemplateUtils.loadTemplate("videoupload-video"),

      events: {
        "click .action-edit-title": "toggleTitleEdit",
        "keyup .title input": "onEditTitle",
        "click .action-delete": "onClickDelete",
        "click .action-cancel": "onClickCancel",
        "click .action-parameters": "onClickParameters",
      },

      initialize: function() {
        this.listenTo(this.model, 'change', this.render);
        this.listenTo(this.model, 'change:title', this.changeTitle);
        this.listenTo(this.model, 'sync', this.modelSynced);
        this.listenTo(this.model, 'destroy', this.remove);
        this.listenTo(this.model, 'uploading-progress', this.uploadingProgress);
        this.listenTo(this.model, 'status-uploaded', this.createVideo);
      },

      render: function() {
        var values = this.model.toJSON();
        if (!values.id) {
            values.id = "";
        }
        this.$el.html(this.template(values));
        if (!values.embed_url && values.video_sources && values.video_sources.length > 0) {
          // Time to activate the video player
          var video = this.$el.find('video')[0];
          videoplayer(video);
        }
        return this;
      },

      onEditTitle: function(e) {
        if (e.keyCode == 13) {// enter key validates input
          this.toggleTitleEdit();
          this.model.set("title", this.$(".title input").val());
        } else if (e.keyCode == 27) {// escape key cancels
          this.toggleTitleEdit();
          this.$(".title input").val(this.model.get("title"));
        }
      },

      toggleTitleEdit: function() {
        this.$(".title .togglable").toggleClass("invisible");
        this.$(".title input:visible").focus();
      },

      changeTitle: function() {
        var that = this;
        this.post(this.model.url(), {
          title: this.model.get("title")
        });
      },

      modelSynced: function() {
        if (this.model.get("status") === "processing" && !this.model.get("sync_pending")) {
          // Update model after 5s if it is in 'processing' stage
          this.model.set("sync_pending", true);
          var that = this;
          setTimeout(function() {
            that.model.set("sync_pending", false);
            that.model.fetch();
          }, 5000)
        }
      },

      onClickDelete: function(e) {
        if (this.model.wasUploaded()) {
          var videoDeleteModalView = new VideoDeleteModalView({
            model: this.model
          });
          videoDeleteModalView.show();
        } else {
          this.model.destroy();
        }
      },

      onClickCancel: function(e) {
        this.model.destroy();
      },

      onClickParameters: function(e) {
        var parameterView = new ParameterView({model: this.model});
        parameterView.show();
      },

      createVideo: function(uploadedData) {
        ajaxSettings.resetHeaders();
        ajaxSettings.unsetNotify();
        var that = this;
        $.ajax({
          url: '${reverse_course("videoupload:create-video")}',
          data: {
            title: that.model.get("title"),
            payload: JSON.stringify(uploadedData),
          },
          type: 'POST',
          success: function(data) {
            if (data.error) {
              that.model.setError(data.error);
            } else {
              that.model.setStatus("created", data.url);
              that.model.set("id", data.id);
              that.model.fetch();
            }
          },
          error: function(jqXHR, textStatus, errorThrown) {
            that.model.setError(errorThrown);
          },
        });
      },

      uploadingProgress: function(percent) {
        this.$(".progressbar").progressbar({value: percent});
      },

      post: function(url, postData, successStatus) {
        var that = this;
        ajaxSettings.resetHeaders();
        return $.post(url, postData,
          function(data) {
            if (data.error) {
              that.model.setError(data.error);
            } else {
              that.model.set(data);
              if (successStatus) {
                that.model.setStatus(successStatus);
              }
            }
          }
        ).fail(function(jqXHR, textStatus, errorThrown){
            that.model.setError(errorThrown);
        });
      },
    });

    var VideoCollectionView = Backbone.View.extend({
      el: $("#videoupload-list"),
      template: TemplateUtils.loadTemplate("videoupload-list"),

      initialize: function() {
        this.sortedBy = "created_at_timestamp";
        this.sortedOrder = -1;
        this.listenTo(this.collection, 'request', this.syncing);
        this.listenTo(this.collection, 'sync', this.synced);
        this.listenTo(this.collection, 'add', this.addOne);
        this.listenTo(this.collection, 'syncError', this.syncError);
        this.listenTo(this.collection, 'sort', this.render);
        this.render();
        this.collection.fetch();
      },

      events: {
          "click [data-sortby]": "clickOnSortBy"
      },

      syncing: function(model_or_collection) {
        if (model_or_collection === this.collection) {
            this.$(".syncing").show();
            this.$(".synced").hide();
        }
        return this;
      },

      synced: function(model_or_collection) {
        if (model_or_collection === this.collection) {
            this.$(".syncing").hide();
            this.$(".synced").show();
            this.sort();
            this.render();
        }
        return this;
      },

      render: function() {
        this.$el.html(this.template());

        // Re-draw sort arrows
        this.$("th[data-sortby] .sort-indicator").html("<img src='${static.url('datatables/images/sort_both.png')}'>");
        if (this.sortedBy) {
            var imgSrc = this.sortedOrder === 1 ? "${static.url('datatables/images/sort_asc.png')}" : "${static.url('datatables/images/sort_desc.png')}";
            this.$("th[data-sortby='" + this.sortedBy + "'] .sort-indicator").html("<img src='" + imgSrc + "'>");
        }

        // Display each video
        var that = this;
        this.collection.each(function(video) {
            var videoView = new VideoView({model: video});
            that.$('tbody').append(videoView.render().el);
        });
        return this;
      },

      syncError: function(error) {
        popError(error);
      },

      clickOnSortBy: function(e) {
          var sortBy = $(e.target).attr("data-sortby");
          if (!sortBy) {
              // Click inside of "data-sortby" element
              sortBy = $(e.target).parents("[data-sortby]").attr("data-sortby");
          }
          if (sortBy === this.sortedBy) {
              // Click on same criterion twice inverts the order
              this.sortedOrder = -this.sortedOrder;
          } else {
              // When we change the order we sort by ascending values
              this.sortedOrder = 1;
          }
          this.sortedBy = sortBy;
          this.sort();
      },
      sort: function() {
          var that = this;
          this.collection.comparator = function(v1, v2) {
              function getValue(video) {
                  // Function of one Video element that returns the value from
                  // which the collection should be sorted.
                  var value = video.get(that.sortedBy);
                  if (value.toLowerCase) {
                      value = value.toLowerCase();
                  }
                  return value;
              }
              var val1 = getValue(v1);
              var val2 = getValue(v2);
              if(val1 === val2) {
                  return 0;
              }
              return val1 < val2 ? -that.sortedOrder : that.sortedOrder;
          };
          this.collection.sort();
      },
    });

    var VideoUploadFormView = Backbone.View.extend({
      events: {
        "click button": "onChooseFile",
        "change input": "onFileChosen",
      },
      onChooseFile: function(e) {
        e.preventDefault();

        // Reset content so that we may trigger 'change' events even if the
        // same file is selected twice in a row.
        this.$("input").val(null);

        // Input form is hidden so we need to manually trigger a click
        this.$("input").click();
      },

      onFileChosen: function(e) {
        for(var i=0; i < e.target.files.length; i++) {
          this.uploadToNewUrl(e.target.files[i]);
        }
      },

      uploadToNewUrl: function(videoFile) {
        var that = this;
        var now = new Date();
        var video = new Video({
          title: videoFile.name,
          status: "preparing",
          created_at_timestamp: now.getTime(),
        });
        this.collection.add(video);
        var currentUploadRequest = null;
        this.listenToOnce(video, "destroy", function() {
          if (currentUploadRequest) {
            currentUploadRequest.abort();
          }
        });
        $.getJSON('${reverse_course("videoupload:upload-url")}', function(uploadParams) {
          video.setStatus("prepared", uploadParams);
          video.setStatus("uploading");
          var formData = new FormData();
          formData.append(uploadParams.file_parameter_name, videoFile);
          ajaxSettings.unsetHeaders();// required to remove csrf token
          ajaxSettings.unsetNotify();
          currentUploadRequest = $.ajax({
            url: uploadParams.url,
            data: formData,
            type: 'POST',
            contentType: false,
            processData: false,
            xhr: function() {
              // Track upload progress
              var xhr = new window.XMLHttpRequest();
              xhr.upload.addEventListener("progress", function(event) {
                if (event.lengthComputable) {
                  var progress = event.loaded * 100. / (event.total + 0.0001);
                  video.trigger("uploading-progress", progress);
                }
              });
              return xhr;
            },
            success: function(data) {
              if (data.error) {
                video.setError(data.error)
              } else {
                video.setStatus("uploaded", data);
              }
            },
            error: function(jqXHR, textStatus, errorThrown) {
              video.setError(errorThrown);
            },
          });
        });
      },
    });

    var Subtitle = Backbone.Model.extend({
    });

    var SubtitleCollection = Backbone.Collection.extend({
      model: Subtitle,

      initialize: function(models, options) {
        this.video = options.video;
      },

      url: function() {
        var baseUrl = '${reverse_course("videoupload:video-subtitles", kwargs={"video_id": "videoid"})}';
        return baseUrl.replace("videoid", this.video.get("id"));
      },
    });

    var SubtitleView = Backbone.View.extend({
      tagName: 'tr',
      template: TemplateUtils.loadTemplate("videoupload-parameters-subtitle"),

      initialize: function() {
        this.listenTo(this.model, 'destroy', this.remove);
        this.listenTo(this.model, 'change', this.render);
      },

      events: {
        "click .action-delete": "onClickDelete",
      },

      render: function() {
        this.$el.html(this.template(this.model.toJSON()));
        return this;
      },

      onClickDelete: function(e) {
        // Note: no confirmation required here
        this.model.destroy();
      },
    });

    var ThumbnailView = Backbone.View.extend({
      template: TemplateUtils.loadTemplate("videoupload-parameters-thumbnail"),

      initialize: function() {
        this.listenTo(this.model, 'change:thumbnail_url', this.render);
      },

      render: function() {
        this.$el.html(this.template(this.model.toJSON()));
        return this;
      },
    });

    var ParameterView = BaseModal.extend({
      template: TemplateUtils.loadTemplate("videoupload-parameters"),
      templateSubtitleForm: TemplateUtils.loadTemplate("videoupload-subtitle-form"),
      templateThumbnailForm: TemplateUtils.loadTemplate("videoupload-thumbnail-form"),

      events: $.extend({}, BaseModal.prototype.events, {
        "submit .upload-subtitles form": "onSubmitSubtitlesForm",
        "submit .upload-thumbnail form": "onSubmitThumbnailForm",
      }),

      options: $.extend({}, BaseModal.prototype.options, {
        title: gettext("Video parameters"),
        modalSize: "med",
        viewSpecificClasses: 'videoupload-parameters view-uploads'
      }),

      initialize: function() {
        // this.model is the current video
        ParameterView.__super__.initialize.apply(this);
        this.buttonTemplate = TemplateUtils.loadTemplate("videoupload-modal-button");

        // Fill subtitles
        this.subtitles = new SubtitleCollection([], {video: this.model});
        this.listenTo(this.subtitles, 'request', this.subtitlesSyncing);
        this.listenTo(this.subtitles, 'sync', this.subtitlesSynced);
        this.listenTo(this.subtitles, 'add', this.addSubtitle);

        this.render();
      },

      cancel: function(event) {
        ParameterView.__super__.cancel.apply(this, event);
        this.remove();
      },

      show: function() {
          ParameterView.__super__.show.apply(this);
          this.subtitles.fetch();
      },

      subtitlesSyncing: function(model_or_collection) {
        if (model_or_collection === this.subtitles) {
            this.$(".subtitles .syncing").show();
            this.$(".subtitles .synced").hide();
        }
      },

      subtitlesSynced: function(model_or_collection) {
        if (model_or_collection === this.subtitles) {
            this.$(".subtitles .syncing").hide();
            this.$(".subtitles .synced").show();
        }
      },

      addSubtitle: function(subtitle) {
        var subtitleView = new SubtitleView({model: subtitle});
        this.$('.subtitles tbody').append(subtitleView.render().el);
      },

      addActionButtons: function() {
        this.addActionButton('cancel', gettext('Close'));
      },

      renderContents: function() {
        ParameterView.__super__.renderContents.apply(this);

        // thumbnail view
        var thumbnailView = new ThumbnailView({model: this.model});
        this.$(".thumbnail").html(thumbnailView.render().el);
        this.$(".upload-thumbnail").html(this.templateThumbnailForm());

        // subtitle upload forms
        this.$(".upload-subtitles").html(this.templateSubtitleForm());

        // Fill video_id values in both subtitle/thumbnail forms
        this.$("form [name='video_id']").val(this.model.videoId());
      },

      getContentHtml: function() {
        return this.template(this.model.toJSON());
      },

      onSubmitSubtitlesForm: function(e) {
        var that = this;
        $(e.target).ajaxSubmit({
          url: this.subtitles.url(),
          beforeSend: function() {
            deactivateForm(e.target);
          },
          success: function(response) {
            if (response.error) {
              popError(response.error);
            } else {
              that.subtitles.fetch();
            }
          },
          complete: function() {
            reactivateForm(e.target);
          },
        });
      },

      updateThumbnailUrl: function() {
        // Url to which a thumbnail file can be posted
        var url = '${reverse_course("videoupload:video-update_thumbnail", kwargs={"video_id": "videoid"})}';
        return url.replace("videoid", this.model.get("id"));
      },

      onSubmitThumbnailForm: function(e) {
        var that = this;

        $(e.target).ajaxSubmit({
          url: this.updateThumbnailUrl(),
          beforeSend: function() {
            deactivateForm(e.target);
          },
          success: function(response) {
            if (response.error) {
              popError(response.error);
            } else {
              that.model.fetch();
            }
          },
          complete: function() {
            reactivateForm(e.target);
          },
        });
      },
    });

    // Initialize objects
    var Videos = new VideoCollection;
    var VideoUploadForm = new VideoUploadFormView({
        el: $("#videoupload-form"),
        collection: Videos
    });
    var VideoCollectionApp = new VideoCollectionView({
        collection: Videos
    });

  }
);