diff --git a/.gitignore b/.gitignore index 89d0de42..d39c25e5 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,5 @@ *.sublime-* /.idea /.vagrant -*.iml \ No newline at end of file +*.iml +*.DS_Store diff --git a/Gemfile b/Gemfile index d2807c82..540b11b2 100644 --- a/Gemfile +++ b/Gemfile @@ -36,7 +36,8 @@ gem 'will_paginate', '~> 3.0' gem 'tubesock' gem 'faye-websocket' gem 'nokogiri' -gem 'rest-client' +gem 'd3-rails' +gem 'rest-client’ group :development do gem 'better_errors', platform: :ruby diff --git a/Gemfile.lock b/Gemfile.lock index 4b0c4d5f..a2739805 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -98,6 +98,8 @@ GEM concurrent-ruby (1.0.0-java) concurrent-ruby-ext (1.0.0) concurrent-ruby (~> 1.0.0) + d3-rails (3.5.11) + railties (>= 3.1) database_cleaner (1.5.1) debug_inspector (0.0.2) diff-lcs (1.2.5) @@ -362,6 +364,7 @@ DEPENDENCIES coffee-rails (~> 4.0.0) concurrent-ruby (~> 1.0.0) concurrent-ruby-ext (~> 1.0.0) + d3-rails database_cleaner docker-api (~> 1.25.0) factory_girl_rails (~> 4.0) diff --git a/app/assets/.DS_Store b/app/assets/.DS_Store new file mode 100644 index 00000000..29fe5375 Binary files /dev/null and b/app/assets/.DS_Store differ diff --git a/app/assets/javascripts/.DS_Store b/app/assets/javascripts/.DS_Store new file mode 100644 index 00000000..5008ddfc Binary files /dev/null and b/app/assets/javascripts/.DS_Store differ diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index aa61da76..a78c7a67 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -14,6 +14,7 @@ // //= require ace/ace //= require chosen.jquery.min +//= require d3 //= require jquery.turbolinks //= require jquery_ujs //= require jstree/jstree.min diff --git a/app/assets/javascripts/exercise_graphs.js b/app/assets/javascripts/exercise_graphs.js new file mode 100644 index 00000000..2d3588c6 --- /dev/null +++ b/app/assets/javascripts/exercise_graphs.js @@ -0,0 +1,281 @@ +$(function() { + // http://localhost:3333/exercises/38/statistics good for testing + // originally at--> localhost:3333/exercises/69/statistics + + if ($.isController('exercises') && $('.graph-functions-2').isPresent()) { + // GET THE DATA + var submissions = $('#data').data('submissions'); + var submissions_length = submissions.length; + + submissionsScoreAndTimeAssess = [[0,0]]; + submissionsScoreAndTimeSubmits = [[0,0]]; + var maximumValue = 0; + + var wtimes = $('#wtimes').data('working_times'); //.hidden#wtimes data-working_times=ActiveSupport::JSON.encode(working_times_until) + + // console.log(submissions); + // console.log(wtimes); + + for (var i = 0;i 0) { + submissionArray[1] = workingTime; + } + + if(submission.score>maximumValue){ + maximumValue = submission.score; + } + submissionsScoreAndTimeAssess.push(submissionArray); + } else if(submission.cause == "submit"){ + var workingTime = get_minutes(wtimes[i]); + var submissionArray = [submission.score, 0]; + + if (workingTime > 0) { + submissionArray[1] = workingTime; + } + + if(submission.score>maximumValue){ + maximumValue = submission.score; + } + submissionsScoreAndTimeSubmits.push(submissionArray); + } + } + // console.log(submissionsScoreAndTimeAssess.length); + // console.log(submissionsScoreAndTimeSubmits); + + function get_minutes (time_stamp) { + try { + hours = time_stamp.split(":")[0]; + minutes = time_stamp.split(":")[1]; + seconds = time_stamp.split(":")[2]; + + minutes = parseFloat(hours * 60) + parseInt(minutes); + if (minutes > 0){ + return minutes; + } else{ + return parseFloat(seconds/60); + } + } catch (err) { + return 0; + } + } + + function getWidth() { + if (self.innerHeight) { + return self.innerWidth; + } + + if (document.documentElement && document.documentElement.clientWidth) { + return document.documentElement.clientWidth; + } + + if (document.body) { + return document.body.clientWidth; + } + } + + function graph_assesses() { + // MAKE THE GRAPH + var width_ratio = .8; + var height_ratio = .7; // percent of height + + var margin = {top: 100, right: 20, bottom: 70, left: 70},//30,50 + width = (getWidth() * width_ratio) - margin.left - margin.right, + height = (width * height_ratio) - margin.top - margin.bottom; + + // Set the ranges + var x = d3.scale.linear().range([0, width]); + var y = d3.scale.linear().range([height,0]); + + //var x = d3.scale.linear() + // .range([0, width]); + //var y = d3.scale.linear() + // .range([0,height]); // - (height/20 + + var xAxis = d3.svg.axis() + .scale(x) + .orient("bottom") + .ticks(20); + + + var yAxis = d3.svg.axis() + .scale(d3.scale.linear().domain([0,maximumValue]).range([height,0]))//y + // .scale(y) + .orient("left") + .ticks(maximumValue) + .innerTickSize(-width) + .outerTickSize(0); + + //var line = d3.svg.line() + // .x(function(d) { return x(d.date); }) + // .y(function(d) { return y(d.close); }); + + var line = d3.svg.line() + .x(function (d) { + // console.log(d[1]); + return x(d[1]); + }) + .y(function (d) { + // console.log(d[0]); + return y(d[0]); + }); + + var svg = d3.select("#progress_chart").append("svg") //PLACEMENT GOES HERE <--------------- + .attr("width", width + margin.left + margin.right) + .attr("height", height + margin.top + margin.bottom) + .append("g") + .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); + + + x.domain(d3.extent(submissionsScoreAndTimeAssess, function (d) { + // console.log(d[1]); + return (d[1]); + })); + y.domain(d3.extent(submissionsScoreAndTimeAssess, function (d) { + // console.log(d[0]); + return (d[0]); + })); + + svg.append("g") //x axis + .attr("class", "x axis") + .attr("transform", "translate(0," + height + ")") + .call(xAxis); + + svg.append("text")// x axis label + .attr("class", "x axis") + .attr("text-anchor", "middle") + .attr("x", width / 2) + .attr("y", height) + .attr("dy", ((height / 20) + 20) + 'px') + .text("Time Spent on Assignment (Minutes)") + .style('font-size', 14); + + svg.append("g") // y axis + .attr("class", "y axis") + .call(yAxis); + + svg.append("text") // y axis label + .attr("transform", "rotate(-90)") + .attr("x", -height / 2) + .attr("dy", "-3em") + .style("text-anchor", "middle") + .text("Score") + .style('font-size', 14); + + svg.append("text")// Title + .attr("class", "x axis") + .attr("text-anchor", "middle") + .attr("x", (width / 2))//+300) + .attr("y", 0) + .attr("dy", '-1.5em') + .text("Assesses Timeline") + .style('font-size', 20) + .style('text-decoration', 'underline'); + + // + // svg.append("path") + // //.datum() + // .attr("class", "line") + // .attr('id', 'myPath')// new + // .attr("stroke", "black") + // .attr("stroke-width", 5) + // .attr("fill", "none")// end new + // .attr("d", line(submissionsScoreAndTimeAssess));//--- + + svg.append("path") + .datum(submissionsScoreAndTimeAssess) + .attr("class", "line") + .attr('id', 'myPath')// new + .attr("stroke", "orange") + .attr("stroke-width", 5) + .attr("fill", "none")// end new + .attr("d", line);//--- + + + svg.selectAll("dot") // Add dots to assesses + .data(submissionsScoreAndTimeAssess) + .enter().append("circle") + .attr("r", 3.5) + .attr("cx", function(d) { return x(d[1]); }) + .attr("cy", function(d) { return y(d[0]); }); + + + svg.append("path") + .datum(submissionsScoreAndTimeSubmits) + .attr("class", "line2") + .attr('id', 'myPath')// new + .attr("stroke", "blue") + .attr("stroke-width", 5) + .attr("fill", "none")// end new + .attr("d", line);//--- + + svg.selectAll("dot") // Add dots to submits + .data(submissionsScoreAndTimeSubmits) + .enter().append("circle") + .attr("r", 3.5) + .attr("cx", function(d) { return x(d[1]); }) + .attr("cy", function(d) { return y(d[0]); }); + + + var color_hash = { 0 : ["Submissions", "blue"], + 1 : ["Assesses", "orange"] + } + + // add legend + var legend = svg.append("g") + .attr("class", "legend") + .attr("x", 65) + .attr("y", 25) + .attr("height", 100) + .attr("width", 100); + + var dataset = [submissionsScoreAndTimeSubmits,submissionsScoreAndTimeAssess]; + + legend.selectAll('g').data(dataset) + .enter() + .append('g') + .each(function(d, i) { + var g = d3.select(this); + g.append("rect") + .attr("x", 20) + .attr("y", i*25 + 8) + .attr("width", 10) + .attr("height", 10) + .style("fill", color_hash[String(i)][1]); + + g.append("text") + .attr("x", 40) + .attr("y", i * 25 + 18) + .attr("height",30) + .attr("width",100) + .style("fill", color_hash[String(i)][1]) + .text(color_hash[String(i)][0]); + + }); + + + + // function type(d) { + // d.frequency = +d.frequency; + // return d; + // } + + //.on("mousemove", mMove)//new again + //.append("title"); + + } + + try{ + graph_assesses(); + } catch(err){ + // not enough data + } + + } + +}); diff --git a/app/assets/javascripts/working_time_graphs.js b/app/assets/javascripts/working_time_graphs.js new file mode 100644 index 00000000..315d2c09 --- /dev/null +++ b/app/assets/javascripts/working_time_graphs.js @@ -0,0 +1,331 @@ +$(function() { + // http://localhost:3333/exercises/38/statistics good for testing + // originally at--> localhost:3333/exercises/69/statistics + + if ($.isController('exercises') && $('.graph-functions').isPresent()) { + var working_times = $('#data').data('working-time'); + + function get_minutes (time_stamp){ + try{ + hours = time_stamp.split(":")[0]; + minutes = time_stamp.split(":")[1]; + seconds = time_stamp.split(":")[2]; + + minutes = parseFloat(hours * 60) + parseInt(minutes); + return minutes + } catch (err){ + return 0; + } + + } + + // GET ALL THE DATA ------------------------------------------------------------------------------ + minutes_array = _.map(working_times,function(item){return get_minutes(item)}); + minutes_array_length = minutes_array.length; + + maximum_minutes = _.max(minutes_array); + var minutes_count = new Array(maximum_minutes); + + for (var i = 0; i < minutes_array_length; i++){ + var studentTime = minutes_array[i]; + + for (var j = 0; j < studentTime; j++){ + if (minutes_count[j] == undefined){ + minutes_count[j] = 0; + } else{ + minutes_count[j] += 1; + } + } + } + + // minutes_count[(maximum_minutes + 1)] = 0; + //$('.graph-functions').html("

") + + // var minutes_count = new Array(10); + // var minutes_array_len = minutes_array.length; + // for (var i=0; i< minutes_count; i++){ + // + // for (var j = 0; j < minutes_array_len; j++){ + // if () + // } + // } + + function getWidth() { + if (self.innerHeight) { + return self.innerWidth; + } + + if (document.documentElement && document.documentElement.clientWidth) { + return document.documentElement.clientWidth; + } + + if (document.body) { + return document.body.clientWidth; + } + } + + // DRAW THE LINE GRAPH ------------------------------------------------------------------------------ + function draw_line_graph() { + var width_ratio = .8; + var height_ratio = .7; // percent of height + + // currently sets as percentage of window width, however, unfortunately + // is not yet responsive + + var margin = {top: 100, right: 20, bottom: 70, left: 70},//30,50 + width = (getWidth() * width_ratio) - margin.left - margin.right, + height = (width * height_ratio) - margin.top - margin.bottom; + + //var formatDate = d3.time.format("%M"); + + var x = d3.scale.linear() + .range([0, width]); + var y = d3.scale.linear() + .range([height, 0]); // - (height/20 + var xAxis = d3.svg.axis() + .scale(x) + .orient("bottom") + .ticks(20); + var yAxis = d3.svg.axis() + .scale(y) + .orient("left") + .ticks(20) + .innerTickSize(-width) + .outerTickSize(0); + + var line = d3.svg.line() + .x(function (d, i) { + return x(i); + }) + .y(function (d) { + return y(d / minutes_count[0] * 100); + }); + + var svg = d3.select("#chart_1").append("svg") //PLACEMENT GOES HERE <--------------- + .attr("width", width + margin.left + margin.right) + .attr("height", height + margin.top + margin.bottom) + .append("g") + .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); + + x.domain(d3.extent(minutes_count, function (d, i) { + return (i); + })); + y.domain(d3.extent(minutes_count, function (d) { + return (d / minutes_count[0] * 100); + })); + + svg.append("g") //x axis + .attr("class", "x axis") + .attr("transform", "translate(0," + height + ")") + .call(xAxis); + + svg.append("text")// x axis label + .attr("class", "x axis") + .attr("text-anchor", "middle") + .attr("x", width / 2) + .attr("y", height) + .attr("dy", ((height / 20) + 20) + 'px') + .text("Time Spent on Assignment (Minutes)") + .style('font-size', 14); + + svg.append("g") // y axis + .attr("class", "y axis") + .call(yAxis); + + svg.append("text") // y axis label + .attr("transform", "rotate(-90)") + .attr("x", -height / 2) + .attr("dy", "-3em") + .style("text-anchor", "middle") + .text("Students (%)") + .style('font-size', 14); + + svg.append("text")// Title + .attr("class", "x axis") + .attr("text-anchor", "middle") + .attr("x", (width / 2))//+300) + .attr("y", 0) + .attr("dy", '-1.5em') + .text("Time Spent by Students on Exercise") + .style('font-size', 20) + .style('text-decoration', 'underline'); + + svg.append("path") + .datum(minutes_count) + .attr("class", "line") + .attr('id', 'myPath')// new + .attr("stroke", "orange") + .attr("stroke-width", 5) + .attr("fill", "none")// end new + .attr("d", line);//--- + //.on("mousemove", mMove)//new again + //.append("title"); + + // function type(d) { + // d.frequency = +d.frequency; + // return d; + // } + } + + draw_line_graph(); + + // THIS SHOULD DISPLAY THE X AND Y VALUES BUT + // THE RESULTS ARE WRONG AT THE END FOR SOME REASON + + //function mMove() { + // var x_width = getWidth() * width_ratio; + // //var x_value = m[0]*(minutes_count.length/x_width); + // + // var y_height = x_width * height_ratio; + // //var y_value = (((y_height - m[1])/y_height)*100); + // + // //console.log('y is: ' + y_value); + // var m = d3.mouse(this); + // d3.select("#myPath").select("title") + // .text((y_height-m[1])/(y_height) * 100 + "% of Students" +"\n"+ + // (m[0]*(minutes_count.length/x_width)) +" Minutes");//text(m[1]); + //} + + // DRAW THE SECOND GRAPH ------------------------------------------------------------------------------ + // + function draw_bar_graph() { + var group_incrament = 5; + var group_ranges = group_incrament; // just for the start + var minutes_array_for_bar = []; + + do { + var section_value = 0; + for (var i = 0; i < minutes_array.length; i++) { + if ((minutes_array[i] < group_ranges) && (minutes_array[i] >= (group_ranges - group_incrament))) { + section_value++; + } + } + minutes_array_for_bar.push(section_value); + group_ranges += group_incrament; + } + while (group_ranges < maximum_minutes + group_incrament); + + //console.log(minutes_array_for_bar); // this var used as the bars + //minutes_array_for_bar = [39, 20, 28, 20, 39, 34, 26, 23, 16, 8]; + + var max_of_array = Math.max.apply(Math, minutes_array_for_bar); + var min_of_array = Math.min.apply(Math, minutes_array_for_bar); + + + var width_ratio = .8; + var height_ratio = .7; // percent of height + + var margin = {top: 100, right: 20, bottom: 70, left: 70},//30,50 + width = (getWidth() * width_ratio) - margin.left - margin.right, + height = (width * height_ratio) - margin.top - margin.bottom; + + var x = d3.scale.ordinal() + .rangeRoundBands([0, width], .1); + + var y = d3.scale.linear() + .range([0,height-(margin.top + margin.bottom)]); + + + var xAxis = d3.svg.axis() + .scale(x) + .orient("bottom") + .ticks(10); + + + var yAxis = d3.svg.axis() + .scale(d3.scale.linear().domain([0,max_of_array]).range([height,0]))//y + .orient("left") + .ticks(10) + .innerTickSize(-width); + + var tip = d3.tip() + .attr('class', 'd3-tip') + .offset([-10, 0]) + .html(function(d) { + return "Students: " + d + ""; + }); + + var svg = d3.select("#chart_2").append("svg") + .attr("width", width + margin.left + margin.right) + .attr("height", height + margin.top + margin.bottom) + .append("g") + .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); + + svg.call(tip); + + x.domain(minutes_array_for_bar.map(function (d, i) { + i++; + var high_side = i * group_incrament; + var low_side = high_side - group_incrament; + return (low_side+"-"+high_side); + })); + + y.domain(minutes_array_for_bar.map(function (d) { + return (d); + })); + + svg.append("g") + .attr("class", "x axis") + .attr("transform", "translate(0," + height + ")") + .call(xAxis); + + svg.append("g") + .attr("class", "y axis") + .call(yAxis) + .append("text") + .attr("transform", "rotate(-90)") + .attr("y", 6) + .attr("dy", ".71em"); + //.style("text-anchor", "end") + //.text("Students"); + + svg.append("text") // y axis label + .attr("transform", "rotate(-90)") + .attr("x", -height / 2) + .attr("dy", "-3em") + .style("text-anchor", "middle") + .text("Students") + .style('font-size', 14); + + svg.append("text")// x axis label + .attr("class", "x axis") + .attr("text-anchor", "middle") + .attr("x", width / 2) + .attr("y", height) + .attr("dy", ((height / 20) + 20) + 'px') + .text("Working Time (Minutes)") + .style('font-size', 14); + + y = d3.scale.linear() + .domain([(0),max_of_array]) + .range([0,height]); + + + svg.selectAll(".bar") + .data(minutes_array_for_bar) + .enter().append("rect") + .attr("class", "bar") + .attr("x", function(d,i) { var bar_incriment = width/ minutes_array_for_bar.length; + var bar_x = i * bar_incriment; + return (bar_x)}) + .attr("width", x.rangeBand()) + .attr("y", function(d) { return height - y(d); }) + .attr("height", function(d) { return y(d); }) + .on('mouseover', tip.show) + .on('mouseout', tip.hide); + + svg.append("text")// Title + .attr("class", "x axis") + .attr("text-anchor", "middle") + .attr("x", (width / 2))//+300) + .attr("y", 0) + .attr("dy", '-1.5em') + .text("Distribution of Time Spent by Students") + .style('font-size', 20) + .style('text-decoration', 'underline'); + + } + // draw_bar_graph(); + } + +}); diff --git a/app/assets/stylesheets/exercises.css.scss b/app/assets/stylesheets/exercises.css.scss index fffbcc23..f348c025 100644 --- a/app/assets/stylesheets/exercises.css.scss +++ b/app/assets/stylesheets/exercises.css.scss @@ -11,3 +11,81 @@ input[type='file'] { #exercise_template_code { font-family: monospace; } + +// Graph Settings + +.axis path { + fill: none; + stroke: #100; + shape-rendering: crispEdges; +} +.axis line { + fill: none; + stroke: #999; + //shape-rendering: crispEdges; +} + +.y.axis path { + display: none; +} + +.line { + fill: none; + //stroke: orange;//steelblue; + stroke-width: 2px; +} +.line2 { + fill: none; + //stroke: red;//steelblue; + stroke-width: 3px; +} + +div#chart_1 { + background-color: #FAFAFA; +} + +div#chart_2 { + background-color: #FAFAFA; +} + + + + +.bar { + fill: orange; +} + +.bar:hover { + fill: #ffd897; +} + + + +.d3-tip { + line-height: 1; + font-weight: bold; + padding: 12px; + background: rgba(0, 0, 0, 0.8); + color: #fff; + border-radius: 2px; +} + +/* Creates a small triangle extender for the tooltip */ +.d3-tip:after { + box-sizing: border-box; + display: inline; + font-size: 10px; + width: 100%; + line-height: 1; + color: rgba(0, 0, 0, 0.8); + content: "\25BC"; + position: absolute; + text-align: center; +} + +/* Style northward tooltips differently */ +.d3-tip.n:after { + margin: -1px 0 0 0; + top: 100%; + left: 0; +} \ No newline at end of file diff --git a/app/assets/stylesheets/statistics.css.scss b/app/assets/stylesheets/statistics.css.scss index ebabe3f7..f656cd9a 100644 --- a/app/assets/stylesheets/statistics.css.scss +++ b/app/assets/stylesheets/statistics.css.scss @@ -49,3 +49,4 @@ div.negative-result { -moz-box-shadow: 0px 0px 11px 1px rgba(222,0,0,1); box-shadow: 0px 0px 11px 1px rgba(222,0,0,1); } + diff --git a/app/views/exercises/external_users/statistics.html.slim b/app/views/exercises/external_users/statistics.html.slim index da898ddd..56bd614b 100644 --- a/app/views/exercises/external_users/statistics.html.slim +++ b/app/views/exercises/external_users/statistics.html.slim @@ -32,7 +32,7 @@ h1 = "#{@exercise} (external user #{@external_user})" option data-submission=submission =index - index += 1 - + - working_times_until = Array.new #timeline .table-responsive table.table @@ -54,7 +54,11 @@ h1 = "#{@exercise} (external user #{@external_user})" - else .unit-test-result.negative-result td = Time.at(deltas[1..index].inject(:+)).utc.strftime("%H:%M:%S") if index > 0 + -working_times_until.push((Time.at(deltas[1..index].inject(:+)).utc.strftime("%H:%M:%S") if index > 0)) p = t('.addendum') + .hidden#wtimes data-working_times=ActiveSupport::JSON.encode(working_times_until); + div#progress_chart.col-lg-12 + .graph-functions-2 - else p = t('.no_data_available') diff --git a/app/views/exercises/statistics.html.slim b/app/views/exercises/statistics.html.slim index 717535be..89b7d6a1 100644 --- a/app/views/exercises/statistics.html.slim +++ b/app/views/exercises/statistics.html.slim @@ -1,3 +1,4 @@ +script src="http://labratrevenge.com/d3-tip/javascripts/d3.tip.v0.6.3.js" h1 = @exercise = row(label: '.participants', value: @exercise.users.distinct.count) @@ -13,6 +14,18 @@ h1 = @exercise - Hash[:internal_users => t('.internal_users'), :external_users => t('.external_users')].each_pair do |symbol, label| strong = label + -if symbol==:external_users + -working_time_array = [] + - @exercise.send(symbol).distinct().each do |user| + -working_time = @exercise.average_working_time_for(user.id) or 0 + -working_time_array.push working_time + hr + .hidden#data data-working-time=ActiveSupport::JSON.encode(working_time_array) + .graph-functions + div#chart_1 + hr + /div#chart_2 + /hr .table-responsive table.table.table-striped.sortable thead @@ -27,4 +40,4 @@ h1 = @exercise td = link_to_if symbol==:external_users, label, {controller: "exercises", action: "statistics", external_user_id: user.id, id: @exercise.id} td = us['maximum_score'] or 0 td = us['runs'] - td = @exercise.average_working_time_for(user.id) or 0 + td = @exercise.average_working_time_for(user.id) or 0 \ No newline at end of file diff --git a/public/uploads/files/4/chai.ogg b/public/uploads/files/4/chai.ogg new file mode 100644 index 00000000..86190d0c Binary files /dev/null and b/public/uploads/files/4/chai.ogg differ diff --git a/public/uploads/files/5/devstories.mp4 b/public/uploads/files/5/devstories.mp4 new file mode 100644 index 00000000..9e7d328d Binary files /dev/null and b/public/uploads/files/5/devstories.mp4 differ diff --git a/public/uploads/files/6/devstories.webm b/public/uploads/files/6/devstories.webm new file mode 100644 index 00000000..84fff0ed Binary files /dev/null and b/public/uploads/files/6/devstories.webm differ diff --git a/public/uploads/files/7/poster.png b/public/uploads/files/7/poster.png new file mode 100644 index 00000000..d950cef4 Binary files /dev/null and b/public/uploads/files/7/poster.png differ