From d3b368b9a4ec59426dc8cec931fdd94d02cc48ab Mon Sep 17 00:00:00 2001 From: Fenel Joseph Date: Thu, 19 May 2016 11:20:59 -0400 Subject: [PATCH 01/24] Logic adjusted, awaiting QA --- lib/quail/quail/common/services/media/youtube.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/quail/quail/common/services/media/youtube.php b/lib/quail/quail/common/services/media/youtube.php index 77ef55d0d..20609f2bb 100755 --- a/lib/quail/quail/common/services/media/youtube.php +++ b/lib/quail/quail/common/services/media/youtube.php @@ -38,20 +38,20 @@ class youtubeService extends mediaService function captionsMissing($link_url) { - $url = 'https://www.googleapis.com/youtube/v3/captions?part=id&videoId='; //'https://www.googleapis.com/youtube/v3/videos?part=contentDetails&id='; + $url = $search_url; $api_key = constant( 'GOOGLE_API_KEY' ); if( $youtube_id = $this->isYouTubeVideo($link_url) ) { $url = $url.$youtube_id.'&key='.$api_key; $response = Request::get($url)->send(); - if( !$response ) { - return true; + // If the video was pulled due to copyright violations or is unavailable, the items array will be empty. + // Another error will result in this case + if( empty($response->body->items) ) { + return false; } - - // If the video was pulled due to copyright violations, the items array will be empty. - // TODO: Make this return a different error, warning the instructor that the video is no longer available - return ( empty($response->body->items) )? true : false; + + return ( $response->body->items->contentDetails->caption )? false : true; } return false; From 8548613cda6869084da445f499366a755d675bcc Mon Sep 17 00:00:00 2001 From: Fenel Joseph Date: Thu, 19 May 2016 12:16:06 -0400 Subject: [PATCH 02/24] Adjusted incorrect syntax --- lib/quail/quail/common/services/media/youtube.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/quail/quail/common/services/media/youtube.php b/lib/quail/quail/common/services/media/youtube.php index 20609f2bb..61b09c557 100755 --- a/lib/quail/quail/common/services/media/youtube.php +++ b/lib/quail/quail/common/services/media/youtube.php @@ -38,7 +38,7 @@ class youtubeService extends mediaService function captionsMissing($link_url) { - $url = $search_url; + $url = $this->search_url; $api_key = constant( 'GOOGLE_API_KEY' ); if( $youtube_id = $this->isYouTubeVideo($link_url) ) { From ef9ad9bc860894026cbdc6ef038eaaa9bda289b5 Mon Sep 17 00:00:00 2001 From: Fenel Joseph Date: Thu, 19 May 2016 12:20:46 -0400 Subject: [PATCH 03/24] qa commit --- lib/quail/quail/common/services/media/youtube.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/quail/quail/common/services/media/youtube.php b/lib/quail/quail/common/services/media/youtube.php index 61b09c557..e76bdfb6c 100755 --- a/lib/quail/quail/common/services/media/youtube.php +++ b/lib/quail/quail/common/services/media/youtube.php @@ -50,6 +50,8 @@ function captionsMissing($link_url) if( empty($response->body->items) ) { return false; } + + die($response->body->items->contentDetails->caption); return ( $response->body->items->contentDetails->caption )? false : true; } From f035d9b5842cb3b8085a05dddeca3caa7ae074e0 Mon Sep 17 00:00:00 2001 From: Fenel Joseph Date: Mon, 23 May 2016 16:14:06 -0400 Subject: [PATCH 04/24] see result of youtube api call --- lib/quail/quail/common/services/media/youtube.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/quail/quail/common/services/media/youtube.php b/lib/quail/quail/common/services/media/youtube.php index e76bdfb6c..5a3250885 100755 --- a/lib/quail/quail/common/services/media/youtube.php +++ b/lib/quail/quail/common/services/media/youtube.php @@ -51,7 +51,7 @@ function captionsMissing($link_url) return false; } - die($response->body->items->contentDetails->caption); + error_log( print_r($response->body, true) ); return ( $response->body->items->contentDetails->caption )? false : true; } From ddce93e7be64c5870c95415ae304d822d71266ec Mon Sep 17 00:00:00 2001 From: Fenel Joseph Date: Tue, 24 May 2016 12:19:12 -0400 Subject: [PATCH 05/24] viewing api response --- lib/quail/quail/common/services/media/youtube.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/quail/quail/common/services/media/youtube.php b/lib/quail/quail/common/services/media/youtube.php index 5a3250885..6e18677c2 100755 --- a/lib/quail/quail/common/services/media/youtube.php +++ b/lib/quail/quail/common/services/media/youtube.php @@ -51,7 +51,7 @@ function captionsMissing($link_url) return false; } - error_log( print_r($response->body, true) ); + error_log( print_r($response->body, true return ( $response->body->items->contentDetails->caption )? false : true; } From 5d6d526efa951fdcab1fb812e9f85339ba2837b3 Mon Sep 17 00:00:00 2001 From: Fenel Joseph Date: Tue, 24 May 2016 12:49:31 -0400 Subject: [PATCH 06/24] Extra logging --- lib/quail/quail/common/services/media/youtube.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/quail/quail/common/services/media/youtube.php b/lib/quail/quail/common/services/media/youtube.php index 6e18677c2..77e5aa927 100755 --- a/lib/quail/quail/common/services/media/youtube.php +++ b/lib/quail/quail/common/services/media/youtube.php @@ -51,6 +51,7 @@ function captionsMissing($link_url) return false; } + error_log( 'TEST' ); error_log( print_r($response->body, true return ( $response->body->items->contentDetails->caption )? false : true; From ff0998dacf31628e651cdbbe78d7e6bed8f747f8 Mon Sep 17 00:00:00 2001 From: fe805415 Date: Tue, 24 May 2016 13:06:22 -0400 Subject: [PATCH 07/24] Fixed syntax --- lib/quail/quail/common/services/media/youtube.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/quail/quail/common/services/media/youtube.php b/lib/quail/quail/common/services/media/youtube.php index 77e5aa927..62e1077ab 100755 --- a/lib/quail/quail/common/services/media/youtube.php +++ b/lib/quail/quail/common/services/media/youtube.php @@ -52,7 +52,8 @@ function captionsMissing($link_url) } error_log( 'TEST' ); - error_log( print_r($response->body, true + error_log( print_r($response->body, true) ); + echo( print_r($response->body, true) ); return ( $response->body->items->contentDetails->caption )? false : true; } From fbee9968c907bf1e018a2cb14215b5ef00420edd Mon Sep 17 00:00:00 2001 From: fe805415 Date: Tue, 24 May 2016 13:26:20 -0400 Subject: [PATCH 08/24] killing process to observe change in syntax --- lib/quail/quail/common/services/media/youtube.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/quail/quail/common/services/media/youtube.php b/lib/quail/quail/common/services/media/youtube.php index 62e1077ab..36a526343 100755 --- a/lib/quail/quail/common/services/media/youtube.php +++ b/lib/quail/quail/common/services/media/youtube.php @@ -54,6 +54,7 @@ function captionsMissing($link_url) error_log( 'TEST' ); error_log( print_r($response->body, true) ); echo( print_r($response->body, true) ); + die(); return ( $response->body->items->contentDetails->caption )? false : true; } From bc9c0e774203225b0c3adce4c4b9cc48cb3e10ef Mon Sep 17 00:00:00 2001 From: fe805415 Date: Tue, 24 May 2016 13:50:19 -0400 Subject: [PATCH 09/24] die() no longer needed --- lib/quail/quail/common/services/media/youtube.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/quail/quail/common/services/media/youtube.php b/lib/quail/quail/common/services/media/youtube.php index 36a526343..bc5db11aa 100755 --- a/lib/quail/quail/common/services/media/youtube.php +++ b/lib/quail/quail/common/services/media/youtube.php @@ -53,8 +53,6 @@ function captionsMissing($link_url) error_log( 'TEST' ); error_log( print_r($response->body, true) ); - echo( print_r($response->body, true) ); - die(); return ( $response->body->items->contentDetails->caption )? false : true; } From aa6d6e9d06525c73a1f27e2a12a0909b46ff3fd7 Mon Sep 17 00:00:00 2001 From: fe805415 Date: Tue, 24 May 2016 13:56:20 -0400 Subject: [PATCH 10/24] testing.. --- lib/quail/quail/common/services/media/youtube.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/quail/quail/common/services/media/youtube.php b/lib/quail/quail/common/services/media/youtube.php index bc5db11aa..8ac3e1b6b 100755 --- a/lib/quail/quail/common/services/media/youtube.php +++ b/lib/quail/quail/common/services/media/youtube.php @@ -52,7 +52,7 @@ function captionsMissing($link_url) } error_log( 'TEST' ); - error_log( print_r($response->body, true) ); + error_log( print_r($response->body->items->contentDetails->caption, true) ); return ( $response->body->items->contentDetails->caption )? false : true; } From 44c9bd8c412644aa89008a6e3cef47d1cb3ccca0 Mon Sep 17 00:00:00 2001 From: fe805415 Date: Tue, 24 May 2016 13:59:37 -0400 Subject: [PATCH 11/24] testing.. --- lib/quail/quail/common/services/media/youtube.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/quail/quail/common/services/media/youtube.php b/lib/quail/quail/common/services/media/youtube.php index 8ac3e1b6b..1d62e1d92 100755 --- a/lib/quail/quail/common/services/media/youtube.php +++ b/lib/quail/quail/common/services/media/youtube.php @@ -52,9 +52,9 @@ function captionsMissing($link_url) } error_log( 'TEST' ); - error_log( print_r($response->body->items->contentDetails->caption, true) ); + error_log( print_r($response->body->items[0[->contentDetails->caption, true) ); - return ( $response->body->items->contentDetails->caption )? false : true; + return ( $response->body->items[0]->contentDetails->caption )? false : true; } return false; From 8001e8d9674273d2f9a03ff3ac6372076d6f63f3 Mon Sep 17 00:00:00 2001 From: fe805415 Date: Tue, 24 May 2016 16:06:38 -0400 Subject: [PATCH 12/24] syntax fix --- lib/quail/quail/common/services/media/youtube.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/quail/quail/common/services/media/youtube.php b/lib/quail/quail/common/services/media/youtube.php index 1d62e1d92..700ba6ec6 100755 --- a/lib/quail/quail/common/services/media/youtube.php +++ b/lib/quail/quail/common/services/media/youtube.php @@ -28,7 +28,7 @@ class youtubeService extends mediaService /** * @var string The service point to request caption data from YouTube */ - var $search_url = 'https://www.googleapis.com/youtube/v3/videos?part=contentDetails&id='; + var $search_url = 'https://www.googleapis.com/youtube/v3/videos?part=contentDetails&id='; //'https://www.googleapis.com/youtube/v3/captions?part=snippet&id='; /** * Checks to see if a video is missing caption information in YouTube @@ -52,7 +52,7 @@ function captionsMissing($link_url) } error_log( 'TEST' ); - error_log( print_r($response->body->items[0[->contentDetails->caption, true) ); + error_log( print_r($response->body->items[0]->contentDetails->caption, true) ); return ( $response->body->items[0]->contentDetails->caption )? false : true; } From c3cdd04aede96a88a450dca194a9325371e98290 Mon Sep 17 00:00:00 2001 From: fe805415 Date: Tue, 24 May 2016 16:10:17 -0400 Subject: [PATCH 13/24] syntax to recognize string versus boolean added --- lib/quail/quail/common/services/media/youtube.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/quail/quail/common/services/media/youtube.php b/lib/quail/quail/common/services/media/youtube.php index 700ba6ec6..f2a00b822 100755 --- a/lib/quail/quail/common/services/media/youtube.php +++ b/lib/quail/quail/common/services/media/youtube.php @@ -52,9 +52,10 @@ function captionsMissing($link_url) } error_log( 'TEST' ); + error_log( print_r($response->body->items[0]->contentDetails->id, true) ); error_log( print_r($response->body->items[0]->contentDetails->caption, true) ); - return ( $response->body->items[0]->contentDetails->caption )? false : true; + return ( $response->body->items[0]->contentDetails->caption == 'true' )? false : true; } return false; From af4ce3a871c4325dc1bffd3670ef3858b858515d Mon Sep 17 00:00:00 2001 From: fe805415 Date: Tue, 24 May 2016 16:19:45 -0400 Subject: [PATCH 14/24] Switched to YouTube Captions api --- lib/quail/quail/common/services/media/youtube.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/quail/quail/common/services/media/youtube.php b/lib/quail/quail/common/services/media/youtube.php index f2a00b822..33b91b703 100755 --- a/lib/quail/quail/common/services/media/youtube.php +++ b/lib/quail/quail/common/services/media/youtube.php @@ -28,7 +28,7 @@ class youtubeService extends mediaService /** * @var string The service point to request caption data from YouTube */ - var $search_url = 'https://www.googleapis.com/youtube/v3/videos?part=contentDetails&id='; //'https://www.googleapis.com/youtube/v3/captions?part=snippet&id='; + var $search_url = 'https://www.googleapis.com/youtube/v3/captions?part=snippet&videoId='; //'https://www.googleapis.com/youtube/v3/videos?part=contentDetails&id=' /** * Checks to see if a video is missing caption information in YouTube @@ -52,8 +52,7 @@ function captionsMissing($link_url) } error_log( 'TEST' ); - error_log( print_r($response->body->items[0]->contentDetails->id, true) ); - error_log( print_r($response->body->items[0]->contentDetails->caption, true) ); + error_log( print_r($response->body, true) ); return ( $response->body->items[0]->contentDetails->caption == 'true' )? false : true; } From 9886f061058a8fc8c68cad4e4eb888cad289de3c Mon Sep 17 00:00:00 2001 From: fe805415 Date: Tue, 24 May 2016 16:35:59 -0400 Subject: [PATCH 15/24] full syntax for implementation added --- lib/quail/quail/common/services/media/youtube.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/quail/quail/common/services/media/youtube.php b/lib/quail/quail/common/services/media/youtube.php index 33b91b703..a8af4cfec 100755 --- a/lib/quail/quail/common/services/media/youtube.php +++ b/lib/quail/quail/common/services/media/youtube.php @@ -53,8 +53,16 @@ function captionsMissing($link_url) error_log( 'TEST' ); error_log( print_r($response->body, true) ); - - return ( $response->body->items[0]->contentDetails->caption == 'true' )? false : true; + + $caption_tracks = $response->body->items; + + if( empty($caption_tracks) ) { + return true; + } + + foreach ( $caption_tracks as $track ) { + return ( $track->snippet->trackKind == 'standard')? false: true; + } } return false; From 0c812f9c4c4ef93e65013a769a10d48eb7834e18 Mon Sep 17 00:00:00 2001 From: fe805415 Date: Tue, 24 May 2016 16:41:34 -0400 Subject: [PATCH 16/24] adjusted syntax for correct detection --- lib/quail/quail/common/services/media/youtube.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/quail/quail/common/services/media/youtube.php b/lib/quail/quail/common/services/media/youtube.php index a8af4cfec..ac6b64b57 100755 --- a/lib/quail/quail/common/services/media/youtube.php +++ b/lib/quail/quail/common/services/media/youtube.php @@ -51,9 +51,6 @@ function captionsMissing($link_url) return false; } - error_log( 'TEST' ); - error_log( print_r($response->body, true) ); - $caption_tracks = $response->body->items; if( empty($caption_tracks) ) { @@ -61,8 +58,11 @@ function captionsMissing($link_url) } foreach ( $caption_tracks as $track ) { - return ( $track->snippet->trackKind == 'standard')? false: true; + if ( $track->snippet->trackKind != 'ASR' ) + return false; } + + return true; } return false; From 3d511e1b728796826139594a3c8e1e415a62da66 Mon Sep 17 00:00:00 2001 From: Philip Carter Date: Fri, 1 Jul 2016 11:53:03 -0400 Subject: [PATCH 17/24] Incremental docker build. --- Dockerfile | 16 ++++++++++++++++ composer_depend.sh | 8 ++++++++ docker-compose.yml | 24 ++++++++++++++++++++++++ 3 files changed, 48 insertions(+) create mode 100644 Dockerfile create mode 100644 composer_depend.sh create mode 100644 docker-compose.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..5d764a97e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +# Get php 5.6 +FROM php:5.6 + +RUN apt-get update && apt-get install git-core -y +RUN apt-get autoremove -y && apt-get clean + +# ADD composer.phar /var/www/composer.phar +# RUN php /tmp/composer.phar self-update +# ADD composer.json /var/www/composer.json +# RUN php /tmp/composer.phar update /tmp/composer.json + +ADD composer_depend.sh /composer_depend.sh +RUN chmod +x /composer_depend.sh + + +WORKDIR /var/www diff --git a/composer_depend.sh b/composer_depend.sh new file mode 100644 index 000000000..fe86cf277 --- /dev/null +++ b/composer_depend.sh @@ -0,0 +1,8 @@ +#! /usr/bin/env bash +# This script is intended to update our projects composer dependencies. + +echo Hello udoit + +php composer.phar self-update + +php composer.phar update \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..d428a83e1 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,24 @@ +web: + build: . + ports: + - "8000:8000" + # links: + # - mysql:mysql + environment: + - MYSQL_ROOT_PASSWORD=root + - MYSQL_DATABASE=udoit + - MYSQL_USER=root + - MYSQL_PASSWORD=root + volumes: + - .:/var/www + command: /composer_depend.sh + +# mysql: +# image: mysql:5.6 +# ports: +# - "3306:3306" +# environment: +# - MYSQL_ROOT_PASSWORD=root +# - MYSQL_DATABASE=udoit +# - MYSQL_USER=root +# - MYSQL_PASSWORD=root \ No newline at end of file From c324aad87ce1f5f07443e0e1a0b926fb3dc67f05 Mon Sep 17 00:00:00 2001 From: Philip Carter Date: Wed, 6 Jul 2016 15:45:58 -0400 Subject: [PATCH 18/24] Working test on docker-compose up. --- Dockerfile | 6 +----- composer.phar | Bin 1697172 -> 1641245 bytes composer_depend.sh | 9 +++++++-- docker-compose.yml | 14 +------------- 4 files changed, 9 insertions(+), 20 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5d764a97e..56fa838f0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,13 +4,9 @@ FROM php:5.6 RUN apt-get update && apt-get install git-core -y RUN apt-get autoremove -y && apt-get clean -# ADD composer.phar /var/www/composer.phar -# RUN php /tmp/composer.phar self-update -# ADD composer.json /var/www/composer.json -# RUN php /tmp/composer.phar update /tmp/composer.json +# build a dev environment ADD composer_depend.sh /composer_depend.sh RUN chmod +x /composer_depend.sh - WORKDIR /var/www diff --git a/composer.phar b/composer.phar index 6df798e85a8bf8cc2b6972998389ea4e2f48f785..c0d6e92adc3237fb893ffc02ae32af567b465f04 100755 GIT binary patch delta 13456 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 R>hCLl3x4Pisw}H1`)@mWLx=zX delta 56729 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 diff --git a/composer_depend.sh b/composer_depend.sh index fe86cf277..9341c723f 100644 --- a/composer_depend.sh +++ b/composer_depend.sh @@ -1,8 +1,13 @@ #! /usr/bin/env bash # This script is intended to update our projects composer dependencies. +# This also runs phpunit tests. -echo Hello udoit +echo Starting composer_depend.sh script... php composer.phar self-update -php composer.phar update \ No newline at end of file +php composer.phar update --no-scripts + +vendor/phpunit/phpunit/phpunit + +echo Finished composer_depend.sh script... \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index d428a83e1..41ba33f1d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,8 +2,6 @@ web: build: . ports: - "8000:8000" - # links: - # - mysql:mysql environment: - MYSQL_ROOT_PASSWORD=root - MYSQL_DATABASE=udoit @@ -11,14 +9,4 @@ web: - MYSQL_PASSWORD=root volumes: - .:/var/www - command: /composer_depend.sh - -# mysql: -# image: mysql:5.6 -# ports: -# - "3306:3306" -# environment: -# - MYSQL_ROOT_PASSWORD=root -# - MYSQL_DATABASE=udoit -# - MYSQL_USER=root -# - MYSQL_PASSWORD=root \ No newline at end of file + command: /composer_depend.sh \ No newline at end of file From 98b67705b62598c46da6e8e54e06535a9958ae99 Mon Sep 17 00:00:00 2001 From: Philip Carter Date: Wed, 6 Jul 2016 16:56:37 -0400 Subject: [PATCH 19/24] Run all the tests on alpine. --- Dockerfile | 10 ++-------- composer_depend.sh | 7 +++++-- docker-compose.yml | 9 +-------- run_tests.sh | 1 + 4 files changed, 9 insertions(+), 18 deletions(-) mode change 100644 => 100755 composer_depend.sh create mode 100755 run_tests.sh diff --git a/Dockerfile b/Dockerfile index 56fa838f0..5061a62a9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,6 @@ # Get php 5.6 -FROM php:5.6 +FROM php:5.6-alpine -RUN apt-get update && apt-get install git-core -y -RUN apt-get autoremove -y && apt-get clean - -# build a dev environment - -ADD composer_depend.sh /composer_depend.sh -RUN chmod +x /composer_depend.sh +RUN apk update && apk add git WORKDIR /var/www diff --git a/composer_depend.sh b/composer_depend.sh old mode 100644 new mode 100755 index 9341c723f..8474e3faa --- a/composer_depend.sh +++ b/composer_depend.sh @@ -1,7 +1,10 @@ -#! /usr/bin/env bash +#! /usr/bin/env ash # This script is intended to update our projects composer dependencies. # This also runs phpunit tests. +# exit on failure +set -e + echo Starting composer_depend.sh script... php composer.phar self-update @@ -10,4 +13,4 @@ php composer.phar update --no-scripts vendor/phpunit/phpunit/phpunit -echo Finished composer_depend.sh script... \ No newline at end of file +echo Finished composer_depend.sh script... diff --git a/docker-compose.yml b/docker-compose.yml index 41ba33f1d..251c07144 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,12 +1,5 @@ web: build: . - ports: - - "8000:8000" - environment: - - MYSQL_ROOT_PASSWORD=root - - MYSQL_DATABASE=udoit - - MYSQL_USER=root - - MYSQL_PASSWORD=root volumes: - .:/var/www - command: /composer_depend.sh \ No newline at end of file + command: ./composer_depend.sh \ No newline at end of file diff --git a/run_tests.sh b/run_tests.sh new file mode 100755 index 000000000..7626264e6 --- /dev/null +++ b/run_tests.sh @@ -0,0 +1 @@ +docker-compose run --rm web ./composer_depend.sh From f6ef423bc9b6dc0329d27468a84226ed772b5077 Mon Sep 17 00:00:00 2001 From: fe805415 Date: Wed, 6 Jul 2016 17:54:04 -0400 Subject: [PATCH 20/24] code within function restored --- lib/Ufixit.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/Ufixit.php b/lib/Ufixit.php index 8ad147d20..410956f4e 100755 --- a/lib/Ufixit.php +++ b/lib/Ufixit.php @@ -473,7 +473,11 @@ public function renameElement($node, $name) */ public function replaceContent($html, $error, $corrected) { - $html = $this->replaceContent($html, $error_html, $corrected_error); + $error_html = HTMLMinify::minify(str_replace($this->annoying_entities, $this->entity_replacements, $error_html), ['doctype' => 'html5']); + $corrected_error = HTMLMinify::minify(str_replace($this->annoying_entities, $this->entity_replacements, $corrected_error), ['doctype' => 'html5']); + $html = HTMLMinify::minify(str_replace($this->annoying_entities, $this->entity_replacements, htmlentities($html)), ['doctype' => 'html5']); + + $html = str_replace($error_html, $corrected_error, html_entity_decode($html)); return $html; } From d8aa6c8a3af7126761d95e11e12a80d671e50738 Mon Sep 17 00:00:00 2001 From: Jacob Bates Date: Tue, 20 Dec 2016 17:42:57 -0500 Subject: [PATCH 21/24] Added test for YouTube API, updated travis.yml so that it doesn't run the functional tests automatically, and updated the README and HEROKU to contain better installation instructions. --- .travis.yml | 3 ++- HEROKU.md | 49 ++++++++++++++++++++++++++++++++++++++- README.md | 31 ++++++++++++++++++------- phpunit.xml | 1 + tests/FunctionalTests.php | 44 +++++++++++++++++++++++++++++++++++ tests/UfixitTest.php | 31 ++++++++++++------------- 6 files changed, 133 insertions(+), 26 deletions(-) create mode 100644 tests/FunctionalTests.php diff --git a/.travis.yml b/.travis.yml index 5be994652..19a683c74 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,4 +9,5 @@ php: - '5.4' - '5.5' - '5.6' -script: ./vendor/phpunit/phpunit/phpunit +script: + - './vendor/phpunit/phpunit/phpunit --exclude-group functional' diff --git a/HEROKU.md b/HEROKU.md index 72d7e8db9..296d0fb55 100755 --- a/HEROKU.md +++ b/HEROKU.md @@ -3,6 +3,53 @@ Use the button below to quick start your deploy. [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) +## Installation Instructions +Installing UDOIT using the Heroku button is very easy, but still requires some setup. If you prefer to watch a video demonstrating the process step-by-step, watch the [UDOIT Installation CanvasLIVE video](https://www.youtube.com/watch?v=g1LgnErkvsA). + +Below are the written directions if you prefer to follow along that way. + +### Step 1: Create a Google/YouTube API key + +### Step 2: Setting up Heroku +After clicking the Heroku button above: +1. Create an account (if you don't have one already). +2. Give the app a name. +3. Fill out the `OAUTH2_ID` and `OAUTH2_KEY` fields with dummy data. (We'll fix it later.) +4. Fill out the `OAUTH2_URI` field with `https://yourapp.herokuapp.com/oauth2response.php`. (Replace 'yourapp' with the name you gave in step 2.) +5. Click the Deploy button and wait for the process to complete. + +### Step 3: Request a Developer Key +UDOIT uses Oauth2 to take actions on behalf of the user, so you'll need to ask your Canvas administrator to generate a Developer Key for you. (If you are an admin, go to your institution's account administration page in Canvas and click on 'Developer Keys'.) Here is the information you need to provide them: + +* Key Name: Probably 'UDOIT' or 'UDOIT Test' for your test instance +* Owner Email: The email address of whoever is responsible for UDOIT at your institution +* Redirect URI: This is the URI of the `oauth2response.php` file in the UDOIT directory. + * This should be `https://yourapp.herokuapp.com/oauth2response.php`. (Replace 'yourapp' with the name of your UDOIT instance on Heroku.) +* Icon URL: The URL of the UDOIT icon. This is `https://yourapp.herokuapp.com/assets/img/udoit_icon.png`. (Replace 'yourapp' with the name of your UDOIT instance on Heroku.) + +### Step 4: Add your Developer Key to UDOIT +1. In Heroku, click the 'Manage App' button for your install of UDOIT. +2. Go to the 'Settings' tab. +3. Copy and paste the following values from the Developer Key: + * ID into OAUTH2_ID + * Secret into OAUTH2_KEY +4. Verify that your OAUTH2_URI is correct. (See above.) + +### Step 5: Install the UDOIT LTI + +1. You can install UDOIT at the sub-account level or the course level. Either way, start by going to the **settings** area. +2. Click the **Apps** tab. +3. Click the **View App Configurations** button. +4. Click the **Add App** button. +5. Under **Configuration Type**, choose **By URL**. +6. In the **Name** field, enter `UDOIT`. +7. In the **Consumer Key** field, copy the value from OAUTH2_ID +8. In the **Shared Secret** field, copy the value from OAUTH2_KEY +9. In the **Config URL** field, paste value from OAUTH2_URI. +10. Finish by clicking **Submit**. + +UDOIT should now be available in the course navigation menu. + # Manual Deploy on Heroku You can use our configuration to launch a new Heroku app using [Heroku's app-setups api](https://devcenter.heroku.com/articles/setting-up-apps-using-the-heroku-platform-api). @@ -34,7 +81,7 @@ These variables can be set in the curl post above. You can also set them later * `SHARED_SECRET` - LTI secret entered when adding UDOIT LTI to Canvas * `OAUTH2_ID` - from the developer api key created by your admin * `OAUTH2_KEY` - from the developer api key created by your admin -* `OAUTH2_URI` - full url to your oauth2responce.php - EX: `https://your.herokuapp.com/oauth2response.php` +* `OAUTH2_URI` - full url to your oauth2response.php - EX: `https://your.herokuapp.com/oauth2response.php` * `GOOGLE_API_KEY` - add a google api key for youtube video support * `USE_HEROKU_CONFIG` - set to `true` to enable the Heroku configuration diff --git a/README.md b/README.md index d976f9a29..665deeda3 100755 --- a/README.md +++ b/README.md @@ -98,24 +98,33 @@ $ php lib/db_create_tables.php The table schema can be found in [lib/db_create_tables.php](lib/db_create_tables.php) ## Configuration and Setup -If you didn't already make `config/localConfig.php` when you set up the database do it now. +If you didn't already make `config/localConfig.php` when you set up the database, do it now. ### Canvas API Please refer to the [Canvas API Policy](http://www.canvaslms.com/policies/api-policy) before using this application, as it makes heavy use of the Canvas API. +### LTI Security +UDOIT uses the security processes built into the LTI specification to ensure that users are only accessing UDOIT from within your instance of Canvas. There are two values that need to be set in order for this security process to work. These values should be different from each other. You will use them again when you are installing the LTI in Canvas. + Edit `config/localConfig.php`: -* `$consumer_key`: A consumer key you make up. Used when installing the LTI in Canvas. -* `$shared_secret`: The shared secret you make up. Used when installing the LTI in Canvas. +* `$consumer_key`: A value you make up. +* `$shared_secret`: The value you make up. ### Canvas Oauth2 -UDOIT uses Oauth2 to take actions on behalf of the user, you'll need to [sign up for a developer key](https://docs.google.com/forms/d/1C5vOpWHAAl-cltj2944-NM0w16AiCvKQFJae3euwwM8/viewform) +UDOIT uses Oauth2 to take actions on behalf of the user, so you'll need to ask your Canvas administrator to generate a Developer Key for you. Here is the information you need to provide them: -Edit `config/localConfig.php`: +* Key Name: Probably 'UDOIT' or 'UDOIT Test' for your test instance +* Owner Email: The email address of whoever is responsible for UDOIT at your institution +* Redirect URI: This is the URI of the `oauth2response.php` file in the UDOIT directory. + * If you did a normal install into the web root of your server, it would be `https://www.example.com/public/oauth2response.php`. (Replace 'www.example.com' with the url of your UDOIT server.) +* Icon URL: The URL of the UDOIT icon. This is `https://www.example.com/public/assets/img/udoit_icon.png`. (Replace 'www.example.com' with the url of your UDOIT server.) -* `$oauth2_id`: The Client_ID Instructure gives you -* `$oauth2_key`: The Secret Instructure gives you -* `$oauth2_uri`: The "Oauth2 Redirect URI" you provided Instructure. This is the URI of the `auth2response.php` file in the UDOIT directory. +After you receive your Developer Key from your Canvas admin, edit the following variables in `config/localConfig.php`: + +* `$oauth2_id`: The Client_ID yoru Canvas admin gives you +* `$oauth2_key`: The Secret your Canvas admin gives you +* `$oauth2_uri`: The Redirect URI you provided to your Canvas admin ### Installing the LTI in Canvas Log into Canvas to add UDOIT: @@ -156,6 +165,12 @@ We use phpunit to run unit tests on UDOIT. To run the tests, type the following $ ./vendor/phpunit/phpunit/phpunit ``` +By default, phpunit will run all tests, including the functional tests that require access to outside APIs. If you would like to exclude those tests, run this command: + +``` +$ ./vendor/phpunit/phpunit/phpunit --exclude-group functional +``` + ## Contributors * [Jacob Bates](https://github.com/bagofarms) * [Eric Colon](https://github.com/accell) diff --git a/phpunit.xml b/phpunit.xml index 6f3ec1b21..89c2124c8 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -2,6 +2,7 @@ tests/UfixitTest.php + tests/FunctionalTests.php \ No newline at end of file diff --git a/tests/FunctionalTests.php b/tests/FunctionalTests.php new file mode 100644 index 000000000..4f737264d --- /dev/null +++ b/tests/FunctionalTests.php @@ -0,0 +1,44 @@ +assertEquals('', $buffer); + // } + + /* Tests the youtube api call to make sure a video with captions is detected as having captions */ + public function testYouTubeAPIHasCaptions() { + $vid_url = 'https://www.youtube.com/watch?v=zo6aRvf-l_s'; + + $yt_service = new youtubeService(); + $captions_missing = $yt_service->captionsMissing($vid_url); + + $this->assertFalse($captions_missing); + } + + /* Tests the youtube api call to make sure a video without captions is detected as not having captions */ + public function testYouTubeAPINoCaptions() { + $vid_url = 'https://www.youtube.com/watch?v=nBH89Y0Xj7c'; + + $yt_service = new youtubeService(); + $captions_missing = $yt_service->captionsMissing($vid_url); + + $this->assertTrue($captions_missing); + } +} \ No newline at end of file diff --git a/tests/UfixitTest.php b/tests/UfixitTest.php index 62d398032..6b4dd81d4 100644 --- a/tests/UfixitTest.php +++ b/tests/UfixitTest.php @@ -13,7 +13,11 @@ public function setUp () { ]; } - public function checkOutputBuffer() { + public function tearDown () { + unset($data); + } + + private function checkOutputBuffer() { $buffer = ob_get_clean(); $this->assertEquals('', $buffer); } @@ -31,7 +35,7 @@ public function testFixAltText() { $this->checkOutputBuffer(); } - public function testFixCssColor_OneColorBold() { + public function testFixCssColorOneColorBold() { $error_html = 'Bad Contrasting Text'; $error_colors = ['ffff00']; $new_content = ['000000']; @@ -47,7 +51,7 @@ public function testFixCssColor_OneColorBold() { $this->checkOutputBuffer(); } - public function testFixCssColor_TwoColorsItalic() { + public function testFixCssColorTwoColorsItalic() { $error_html = 'Bad Contrasting Text'; $error_colors = ['ffff00', 'ffffff']; $new_content = ['000000', 'fffff0']; @@ -63,7 +67,7 @@ public function testFixCssColor_TwoColorsItalic() { $this->checkOutputBuffer(); } - public function testFixCssColor_TwoColorsBoldItalic() { + public function testFixCssColorTwoColorsBoldItalic() { $error_html = 'Bad Contrasting Text'; $error_colors = ['ffff00', 'ffffff']; $new_content = ['000000', 'fffff0']; @@ -79,7 +83,7 @@ public function testFixCssColor_TwoColorsBoldItalic() { $this->checkOutputBuffer(); } - public function testFixLink_NewText() { + public function testFixLinkNewText() { $error_html = ''; $new_content = 'test'; $expected = 'test'; @@ -92,7 +96,7 @@ public function testFixLink_NewText() { $this->checkOutputBuffer(); } - public function testFixLink_DeleteLink() { + public function testFixLinkDeleteLink() { $error_html = ''; $new_content = ''; $expected = ''; @@ -105,7 +109,7 @@ public function testFixLink_DeleteLink() { $this->checkOutputBuffer(); } - public function testFixHeading_NewHeading() { + public function testFixHeadingNewHeading() { $error_html = '

'; $new_content = 'Heading Text'; $expected = '

Heading Text

'; @@ -118,7 +122,7 @@ public function testFixHeading_NewHeading() { $this->checkOutputBuffer(); } - public function testFixHeading_DeleteHeading() { + public function testFixHeadingDeleteHeading() { $error_html = '

'; $new_content = ''; $expected = ''; @@ -131,7 +135,7 @@ public function testFixHeading_DeleteHeading() { $this->checkOutputBuffer(); } - public function testFixTableHeaders_Row() { + public function testFixTableHeadersRow() { $error_html = '
Header OneHeader Two
1.304.50
'; $sel_header = 'row'; $expected = ''."\n".'Header One'."\n".'Header Two'."\n".''."\n"; @@ -144,7 +148,7 @@ public function testFixTableHeaders_Row() { $this->checkOutputBuffer(); } - public function testFixTableHeaders_Col() { + public function testFixTableHeadersCol() { $error_html = '
Header OneHeader Two
Title4.50
'; $sel_header = 'col'; $expected = ''."\n".'Header One'."\n".'Header Two'."\n".''."\n".''."\n".'Title'."\n".'4.50'."\n".''; @@ -157,7 +161,7 @@ public function testFixTableHeaders_Col() { $this->checkOutputBuffer(); } - public function testFixTableHeaders_Both() { + public function testFixTableHeadersBoth() { $error_html = '
Header OneHeader Two
Title4.50
'; $sel_header = 'both'; $expected = ''."\n".'Header One'."\n".'Header Two'."\n".''."\n".''."\n".'Title'."\n".'4.50'."\n".''; @@ -182,9 +186,4 @@ public function testFixTableThScopes() { $this->assertEquals($expected, $output); $this->checkOutputBuffer(); } - - public function tearDown () { - unset($data); - } - } \ No newline at end of file From 2aeb904c2af6f0ccc78d25dc7e248b92685a731b Mon Sep 17 00:00:00 2001 From: Jacob Bates Date: Tue, 20 Dec 2016 18:04:07 -0500 Subject: [PATCH 22/24] Moved functional tests into their own test suite so that i can keep Travis from running them --- .travis.yml | 2 +- phpunit.xml | 4 +++- tests/FunctionalTests.php | 7 ++----- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 19a683c74..9d813e1cf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,4 +10,4 @@ php: - '5.5' - '5.6' script: - - './vendor/phpunit/phpunit/phpunit --exclude-group functional' + - './vendor/phpunit/phpunit/phpunit --testsuite "UDOIT"' diff --git a/phpunit.xml b/phpunit.xml index 89c2124c8..4aba5fc7d 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -2,7 +2,9 @@ tests/UfixitTest.php - tests/FunctionalTests.php + + + tests/FunctionalTests.php \ No newline at end of file diff --git a/tests/FunctionalTests.php b/tests/FunctionalTests.php index 4f737264d..c53990f24 100644 --- a/tests/FunctionalTests.php +++ b/tests/FunctionalTests.php @@ -4,9 +4,6 @@ include_once('lib/quail/quail/common/services/media/youtube.php'); include_once('config/settings.php'); -/** - * @group functional - */ class FunctionalTests extends PHPUnit_Framework_TestCase { // public function setUp () { @@ -27,7 +24,7 @@ public function testYouTubeAPIHasCaptions() { $vid_url = 'https://www.youtube.com/watch?v=zo6aRvf-l_s'; $yt_service = new youtubeService(); - $captions_missing = $yt_service->captionsMissing($vid_url); + $captions_missing = $yt_service->captionsMissing($vid_url); $this->assertFalse($captions_missing); } @@ -37,7 +34,7 @@ public function testYouTubeAPINoCaptions() { $vid_url = 'https://www.youtube.com/watch?v=nBH89Y0Xj7c'; $yt_service = new youtubeService(); - $captions_missing = $yt_service->captionsMissing($vid_url); + $captions_missing = $yt_service->captionsMissing($vid_url); $this->assertTrue($captions_missing); } From b0fcf7c1a365f65426fba6112564c70fa063a51b Mon Sep 17 00:00:00 2001 From: Jacob Bates Date: Wed, 21 Dec 2016 15:23:47 -0500 Subject: [PATCH 23/24] Finished editing README.md and HEROKU.md. Should be ready to merge now. --- HEROKU.md | 18 +++++++++++------- README.md | 32 ++++++++++++++++++++++---------- 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/HEROKU.md b/HEROKU.md index 296d0fb55..2429bf6f9 100755 --- a/HEROKU.md +++ b/HEROKU.md @@ -10,30 +10,34 @@ Below are the written directions if you prefer to follow along that way. ### Step 1: Create a Google/YouTube API key +See the [Google/YouTube API instructions](README.md#googleyoutube-api-key) in the README. + ### Step 2: Setting up Heroku After clicking the Heroku button above: + 1. Create an account (if you don't have one already). 2. Give the app a name. 3. Fill out the `OAUTH2_ID` and `OAUTH2_KEY` fields with dummy data. (We'll fix it later.) 4. Fill out the `OAUTH2_URI` field with `https://yourapp.herokuapp.com/oauth2response.php`. (Replace 'yourapp' with the name you gave in step 2.) +5. Copy and paste your Google/YouTube API key. 5. Click the Deploy button and wait for the process to complete. ### Step 3: Request a Developer Key UDOIT uses Oauth2 to take actions on behalf of the user, so you'll need to ask your Canvas administrator to generate a Developer Key for you. (If you are an admin, go to your institution's account administration page in Canvas and click on 'Developer Keys'.) Here is the information you need to provide them: -* Key Name: Probably 'UDOIT' or 'UDOIT Test' for your test instance -* Owner Email: The email address of whoever is responsible for UDOIT at your institution -* Redirect URI: This is the URI of the `oauth2response.php` file in the UDOIT directory. +* ***Key Name:*** Probably ***UDOIT*** or ***UDOIT Test*** for your test instance +* ***Owner Email:*** The email address of whoever is responsible for UDOIT at your institution +* ***Redirect URI:*** This is the URI of the `oauth2response.php` file in the UDOIT directory. * This should be `https://yourapp.herokuapp.com/oauth2response.php`. (Replace 'yourapp' with the name of your UDOIT instance on Heroku.) -* Icon URL: The URL of the UDOIT icon. This is `https://yourapp.herokuapp.com/assets/img/udoit_icon.png`. (Replace 'yourapp' with the name of your UDOIT instance on Heroku.) +* ***Icon URL:*** The URL of the UDOIT icon. This is `https://yourapp.herokuapp.com/assets/img/udoit_icon.png`. (Replace ***yourapp*** with the name of your UDOIT instance on Heroku.) ### Step 4: Add your Developer Key to UDOIT 1. In Heroku, click the 'Manage App' button for your install of UDOIT. 2. Go to the 'Settings' tab. 3. Copy and paste the following values from the Developer Key: - * ID into OAUTH2_ID - * Secret into OAUTH2_KEY -4. Verify that your OAUTH2_URI is correct. (See above.) + * ID into ***OAUTH2_ID*** + * Secret into ***OAUTH2_KEY*** +4. Verify that your ***OAUTH2_URI*** is correct. (See above.) ### Step 5: Install the UDOIT LTI diff --git a/README.md b/README.md index 665deeda3..a36c7cc1b 100755 --- a/README.md +++ b/README.md @@ -114,11 +114,11 @@ Edit `config/localConfig.php`: ### Canvas Oauth2 UDOIT uses Oauth2 to take actions on behalf of the user, so you'll need to ask your Canvas administrator to generate a Developer Key for you. Here is the information you need to provide them: -* Key Name: Probably 'UDOIT' or 'UDOIT Test' for your test instance -* Owner Email: The email address of whoever is responsible for UDOIT at your institution -* Redirect URI: This is the URI of the `oauth2response.php` file in the UDOIT directory. +* ***Key Name:*** Probably ***UDOIT*** or ***UDOIT Test*** for your test instance +* ***Owner Email:*** The email address of whoever is responsible for UDOIT at your institution +* ***Redirect URI:*** This is the URI of the `oauth2response.php` file in the UDOIT directory. * If you did a normal install into the web root of your server, it would be `https://www.example.com/public/oauth2response.php`. (Replace 'www.example.com' with the url of your UDOIT server.) -* Icon URL: The URL of the UDOIT icon. This is `https://www.example.com/public/assets/img/udoit_icon.png`. (Replace 'www.example.com' with the url of your UDOIT server.) +* ***Icon URL:*** The URL of the UDOIT icon. This is `https://www.example.com/public/assets/img/udoit_icon.png`. (Replace 'www.example.com' with the url of your UDOIT server.) After you receive your Developer Key from your Canvas admin, edit the following variables in `config/localConfig.php`: @@ -126,15 +126,27 @@ After you receive your Developer Key from your Canvas admin, edit the following * `$oauth2_key`: The Secret your Canvas admin gives you * `$oauth2_uri`: The Redirect URI you provided to your Canvas admin +### Google/YouTube API Key +In order for UDOIT to scan YouTube videos for closed captioning, you will need to create a YouTube Data API key. Follow the instructions below: + +1. Go to the [Google Developer Console](https://console.developers.google.com). +2. Create a project. +3. Enable ***YouTube Data API V3*** +4. Create an ***API key*** credential. + ### Installing the LTI in Canvas Log into Canvas to add UDOIT: -1. Under **Configuration Type**, choose **By URL**. -2. In the **Name** field, enter `UDOIT`. -3. In the **Consumer Key** field, copy the value from `$consumer_key` from `config/localConfig.php` -4. In the **Shared Secret** field, copy the value from `$shared_secret` from `config/localConfig.php` -5. In the **Config URL** field, paste the **FULL URL** that points to `udoit.xml.php`. **See** LTI Config URL Notes. -6. Finish by clicking **Submit**. +1. You can install UDOIT at the sub-account level or the course level. Either way, start by going to the **settings** area. +2. Click the **Apps** tab. +3. Click the **View App Configurations** button. +4. Click the **Add App** button. +5. Under **Configuration Type**, choose **By URL**. +6. In the **Name** field, enter `UDOIT`. +7. In the **Consumer Key** field, copy the value from `$consumer_key` from `config/localConfig.php` +8. In the **Shared Secret** field, copy the value from `$shared_secret` from `config/localConfig.php` +9. In the **Config URL** field, paste the **FULL URL** that points to `udoit.xml.php`. **See** LTI Config URL Notes. +10. Finish by clicking **Submit**. #### LTI Config URL Notes The URL of your UDOIT LTI config depends on your webserver install. The file is located the `public` directory. The examples below should give you are some possible values: From 76637f84549a86d60dfd69319b796251f31c68c6 Mon Sep 17 00:00:00 2001 From: Jacob Bates Date: Wed, 21 Dec 2016 16:58:11 -0500 Subject: [PATCH 24/24] Added warning about unlisted or deleted videos. Fixed bug that ignored videos that were unlisted or deleted from caption detection. --- config/tests.php | 2 +- lib/quail/quail/common/services/media/youtube.php | 13 ++++--------- lib/quail/quail/guidelines/translations/en.txt | 4 ++-- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/config/tests.php b/config/tests.php index a40e40fd8..c8768c01f 100755 --- a/config/tests.php +++ b/config/tests.php @@ -157,7 +157,7 @@ [ 'name' => 'videosEmbeddedOrLinkedNeedCaptions', 'title' => 'Synchronized captions should be provided for prerecorded web-based video', - 'desc' => '

Captions should be included in the video to provide dialogue to users who are hearing impaired.

', + 'desc' => '

Captions should be included in the video to provide dialogue to users who are hearing impaired. (Please note that videos that have been removed, deleted, or are Unlisted will also cause this error, and will need to be manually verified.)

', 'resources' => [ 'Adding Captions to Youtube', 'Creating Captions for Video Uploaded to Canvas', diff --git a/lib/quail/quail/common/services/media/youtube.php b/lib/quail/quail/common/services/media/youtube.php index fb36a434c..9d6cbcdfb 100755 --- a/lib/quail/quail/common/services/media/youtube.php +++ b/lib/quail/quail/common/services/media/youtube.php @@ -44,21 +44,16 @@ function captionsMissing($link_url) $url = $url.$youtube_id.'&key='.$api_key; $response = Request::get($url)->send(); - // If the video was pulled due to copyright violations or is unavailable, the items array will be empty. + // If the video was pulled due to copyright violations, is unlisted, or is unavailable, the items array will be empty. // Another error will result in this case if( empty($response->body->items) ) { - return false; - } - - $caption_tracks = $response->body->items; - - if( empty($caption_tracks) ) { return true; } - foreach ( $caption_tracks as $track ) { - if ( $track->snippet->trackKind != 'ASR' ) + foreach ( $response->body->items as $track ) { + if ( $track->snippet->trackKind != 'ASR' ) { return false; + } } return true; diff --git a/lib/quail/quail/guidelines/translations/en.txt b/lib/quail/quail/guidelines/translations/en.txt index 36fe7b94c..c88f76f51 100755 --- a/lib/quail/quail/guidelines/translations/en.txt +++ b/lib/quail/quail/guidelines/translations/en.txt @@ -152,7 +152,7 @@ "embedMustNotHaveEmptyAlt","""Embed"" elements cannot have an empty ""alt"" attribute","

All embed elements should have an ""alt"" attribute that is not empty.

Example

Wrong

<embed src=""dog.mov"" alt=""""/>

Right

<embed src=""dog.mov"" alt=""A movie featuring a dog dancing a ballet.""/>

","1" "iframeMustNotHaveLongdesc","Inline frames (""iframes"") should not have a ""longdesc"" attribute","

Iframe elements should not have a ""longdesc"" attribute.

","1" "iframeIsNotUsed","Iframes need to be checked to make sure content within is accessible","

Iframe elements should be checked to make sure content within is accessible.

","2" -"videoEmbedChecked","Synchronized captions should be provided for prerecorded web-based video","

Captions should be included in the video to provide dialogue to users who are hearing impaired.

","2" +"videoEmbedChecked","Synchronized captions should be provided for prerecorded web-based video","

Captions should be included in the video to provide dialogue to users who are hearing impaired. (Please note that videos that have been removed, deleted, or are Unlisted will also cause this error, and will need to be manually verified.)

","2" "radioMarkedWithFieldgroupAndLegend","All radio button groups are marked using fieldset and legend elements.","form element content should contain both fieldset and legend elements if there are related radio buttons.","1" "selectWithOptionsHasOptgroup","Form select elements should use optgroups for long selections","

Select form elements with long lists of items should use the optgroup tag to group like selections together.

","2" "aSuspiciousLinkText","Link text should be descriptive","

Links should be descriptive of the content they're linking to, such as 'Class Schedule' rather than 'schedule.html' or 'click here'.

","1" @@ -235,7 +235,7 @@ "cssTextHasContrast","Insufficient text color contrast with the background","

Text color should be easily viewable and should not be the only indicator of meaning or function. Color balance should have at least a 4.5:1 ratio for small text and 3:1 ratio for large text.

","1" "cssTextStyleEmphasize","Avoid using color alone for emphasis","

When emphasizing text, you may use color with sufficient contrast as long as you also apply some other form of emphasis, such as bold or italics. This ensures that screen reader users are aware of the text's importance.

","1" "videoProvidesCaptions","All video tags should provide captions","

Videos used on online courses are required to have closed captioning. Unfortunately, automatic captioning (such as on YouTube videos) is not accurate enough for educational use. You have three options:

  • Contact the creator of the video and request captions be added.
  • Create captions yourself using a service like Amara (http://amara.org/).
  • Find a different video that has closed captioning.
","2" -"videosEmbeddedOrLinkedNeedCaptions","Synchronized captions should be provided for prerecorded web-based video","

Captions should be included in the video to provide dialogue to users who are hearing impaired.

","1" +"videosEmbeddedOrLinkedNeedCaptions","Synchronized captions should be provided for prerecorded web-based video","

Captions should be included in the video to provide dialogue to users who are hearing impaired. (Please note that videos that have been removed, deleted, or are Unlisted will also cause this error, and will need to be manually verified.)

","1" "documentIsWrittenClearly","The document should be written to the target audience and read clearly","

If a document is beyond a 10th grade level, then a summary or other guide should also be provided to guide the user through the content.

","2" "headersHaveText","Headings should contain text","

Sighted and screen reader users depend on headings to organize the content on the page. Headings should not be empty and should represent an accurate outline of the content

","1" "labelsAreAssignedToAnInput","All labels should be associated with an input","

All label elements should be assigned to an input item, and should have a for attribute which equals the id attribute of a form element.

","1"