Merge branch 'master' into derek-moore-bug405_email_notifications_for_comments

Conflicts:
	mediagoblin/db/mongo/migrations.py
This commit is contained in:
Christopher Allan Webber 2012-03-18 12:12:41 -05:00
commit 94e6052375
37 changed files with 4497 additions and 76 deletions

1
.gitignore vendored
View File

@ -9,6 +9,7 @@
/lib/
/include/
/parts/
/share/
/mediagoblin.egg-info
/docs/_build/
/docs/build

23
extlib/video-js/demo.html Normal file
View File

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<head>
<title>Video.js | HTML5 Video Player</title>
<link href="http://vjs.zencdn.net/c/video-js.css" rel="stylesheet" type="text/css">
<!-- video.js must be in the <head> for older IEs to work. -->
<script src="http://vjs.zencdn.net/c/video.js"></script>
</head>
<body>
<video id="example_video_1" class="video-js vjs-default-skin" controls preload="none" width="640" height="264"
poster="http://video-js.zencoder.com/oceans-clip.png"
data-setup="{}">
<source src="http://video-js.zencoder.com/oceans-clip.mp4" type='video/mp4' />
<source src="http://video-js.zencoder.com/oceans-clip.webm" type='video/webm' />
<source src="http://video-js.zencoder.com/oceans-clip.ogv" type='video/ogg' />
</video>
</body>
</html>

View File

@ -0,0 +1,427 @@
/*
VideoJS Default Styles (http://videojs.com)
Version 3.1.0
*/
/*
REQUIRED STYLES (be careful overriding)
================================================================================ */
/* When loading the player, the video tag is replaced with a DIV,
that will hold the video tag or object tag for other playback methods.
The div contains the video playback element (Flash or HTML5) and controls, and sets the width and height of the video.
** If you want to add some kind of border/padding (e.g. a frame), or special positioning, use another containing element.
Otherwise you risk messing up control positioning and full window mode. **
*/
.video-js {
background-color: #000; position: relative; padding: 0;
/* Start with 10px for base font size so other dimensions can be em based and easily calculable. */
font-size: 10px;
/* Allow poster to be vertially aligned. */
vertical-align: middle;
/* display: table-cell; */ /*This works in Safari but not Firefox.*/
}
/* Playback technology elements expand to the width/height of the containing div. <video> or <object> */
.video-js .vjs-tech { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
/* Fix for Firefox 9 fullscreen (only if it is enabled). Not needed when checking fullScreenEnabled. */
.video-js:-moz-full-screen { position: absolute; }
/* Fullscreen Styles */
body.vjs-full-window {
padding: 0; margin: 0;
height: 100%; overflow-y: auto; /* Fix for IE6 full-window. http://www.cssplay.co.uk/layouts/fixed.html */
}
.video-js.vjs-fullscreen {
position: fixed; overflow: hidden; z-index: 1000; left: 0; top: 0; bottom: 0; right: 0; width: 100% !important; height: 100% !important;
_position: absolute; /* IE6 Full-window (underscore hack) */
}
.video-js:-webkit-full-screen {
width: 100% !important; height: 100% !important;
}
/* Poster Styles */
.vjs-poster {
margin: 0 auto; padding: 0; cursor: pointer;
/* Scale with the size of the player div. Works when poster is vertically shorter, but stretches when it's less wide. */
position: relative; width: 100%; max-height: 100%;
}
/* Subtiles Styles */
.video-js .vjs-subtitles { color: #fff; font-size: 20px; text-align: center; position: absolute; bottom: 40px; left: 0; right: 0; }
/* Fading sytles, used to fade control bar. */
.vjs-fade-in {
visibility: visible !important; /* Needed to make sure things hide in older browsers too. */
opacity: 1 !important;
-webkit-transition: visibility 0s linear 0s, opacity 0.3s linear;
-moz-transition: visibility 0s linear 0s, opacity 0.3s linear;
-ms-transition: visibility 0s linear 0s, opacity 0.3s linear;
-o-transition: visibility 0s linear 0s, opacity 0.3s linear;
transition: visibility 0s linear 0s, opacity 0.3s linear;
}
.vjs-fade-out {
visibility: hidden !important;
opacity: 0 !important;
-webkit-transition: visibility 0s linear 1.5s,opacity 1.5s linear;
-moz-transition: visibility 0s linear 1.5s,opacity 1.5s linear;
-ms-transition: visibility 0s linear 1.5s,opacity 1.5s linear;
-o-transition: visibility 0s linear 1.5s,opacity 1.5s linear;
transition: visibility 0s linear 1.5s,opacity 1.5s linear;
}
/* DEFAULT SKIN (override in another file to create new skins)
================================================================================
Instead of editing this file, I recommend creating your own skin CSS file to be included after this file,
so you can upgrade to newer versions easier. You can remove all these styles by removing the 'vjs-default-skin' class from the tag. */
/* The default control bar. Created by bar.js */
.vjs-default-skin .vjs-controls {
position: absolute;
bottom: 0; /* Distance from the bottom of the box/video. Keep 0. Use height to add more bottom margin. */
left: 0; right: 0; /* 100% width of div */
margin: 0; padding: 0; /* Controls are absolutely position, so no padding necessary */
height: 2.6em; /* Including any margin you want above or below control items */
color: #fff; border-top: 1px solid #404040;
/* CSS Gradient */
/* Can use the Ultimate CSS Gradient Generator: http://www.colorzilla.com/gradient-editor/ */
background: #242424; /* Old browsers */
background: -moz-linear-gradient(top, #242424 50%, #1f1f1f 50%, #171717 100%); /* FF3.6+ */
background: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(50%,#242424), color-stop(50%,#1f1f1f), color-stop(100%,#171717)); /* Chrome,Safari4+ */
background: -webkit-linear-gradient(top, #242424 50%,#1f1f1f 50%,#171717 100%); /* Chrome10+,Safari5.1+ */
background: -o-linear-gradient(top, #242424 50%,#1f1f1f 50%,#171717 100%); /* Opera11.10+ */
background: -ms-linear-gradient(top, #242424 50%,#1f1f1f 50%,#171717 100%); /* IE10+ */
/* Filter was causing a lot of weird issues in IE. Elements would stop showing up, or other styles would break. */
/*filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#242424', endColorstr='#171717',GradientType=0 );*/ /* IE6-9 */
background: linear-gradient(top, #242424 50%,#1f1f1f 50%,#171717 100%); /* W3C */
/* Start hidden and with 0 opacity. Opacity is used to fade in modern browsers. */
/* Can't use display block to hide initially because widths of slider handles aren't calculated and avaialbe for positioning correctly. */
visibility: hidden;
opacity: 0;
}
/* General styles for individual controls. */
.vjs-default-skin .vjs-control {
position: relative; float: left;
text-align: center; margin: 0; padding: 0;
height: 2.6em; width: 2.6em;
}
.vjs-default-skin .vjs-control:focus {
outline: 0;
}
/* Hide control text visually, but have it available for screenreaders: h5bp.com/v */
.vjs-default-skin .vjs-control-text { border: 0; clip: rect(0 0 0 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; width: 1px; }
/* Play/Pause
-------------------------------------------------------------------------------- */
.vjs-default-skin .vjs-play-control { width: 5em; cursor: pointer !important; }
/* Play Icon */
.vjs-default-skin.vjs-paused .vjs-play-control div { width: 15px; height: 17px; background: url('video-js.png'); margin: 0.5em auto 0; }
.vjs-default-skin.vjs-playing .vjs-play-control div { width: 15px; height: 17px; background: url('video-js.png') -25px 0; margin: 0.5em auto 0; }
/* Rewind
-------------------------------------------------------------------------------- */
.vjs-default-skin .vjs-rewind-control { width: 5em; cursor: pointer !important; }
.vjs-default-skin .vjs-rewind-control div { width: 19px; height: 16px; background: url('video-js.png'); margin: 0.5em auto 0; }
/* Volume/Mute
-------------------------------------------------------------------------------- */
.vjs-default-skin .vjs-mute-control { width: 3.8em; cursor: pointer !important; float: right; }
.vjs-default-skin .vjs-mute-control div { width: 22px; height: 16px; background: url('video-js.png') -75px -25px; margin: 0.5em auto 0; }
.vjs-default-skin .vjs-mute-control.vjs-vol-0 div { background: url('video-js.png') 0 -25px; }
.vjs-default-skin .vjs-mute-control.vjs-vol-1 div { background: url('video-js.png') -25px -25px; }
.vjs-default-skin .vjs-mute-control.vjs-vol-2 div { background: url('video-js.png') -50px -25px; }
.vjs-default-skin .vjs-volume-control { width: 5em; float: right; }
.vjs-default-skin .vjs-volume-bar {
position: relative; width: 5em; height: 0.6em; margin: 1em auto 0; cursor: pointer !important;
-moz-border-radius: 0.3em; -webkit-border-radius: 0.3em; border-radius: 0.3em;
background: #666;
background: -moz-linear-gradient(top, #333, #666);
background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#333), to(#666));
background: -webkit-linear-gradient(top, #333, #666);
background: -o-linear-gradient(top, #333, #666);
background: -ms-linear-gradient(top, #333, #666);
background: linear-gradient(top, #333, #666);
}
.vjs-default-skin .vjs-volume-level {
position: absolute; top: 0; left: 0; height: 0.6em;
-moz-border-radius: 0.3em; -webkit-border-radius: 0.3em; border-radius: 0.3em;
background: #fff;
background: -moz-linear-gradient(top, #fff, #ccc);
background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#fff), to(#ccc));
background: -webkit-linear-gradient(top, #fff, #ccc);
background: -o-linear-gradient(top, #fff, #ccc);
background: -ms-linear-gradient(top, #fff, #ccc);
background: linear-gradient(top, #fff, #ccc);
}
.vjs-default-skin .vjs-volume-handle {
position: absolute; top: -0.2em; width: 0.8em; height: 0.8em; background: #ccc; left: 0;
border: 1px solid #fff;
-moz-border-radius: 0.6em; -webkit-border-radius: 0.6em; border-radius: 0.6em;
}
/* Progress
-------------------------------------------------------------------------------- */
.vjs-default-skin div.vjs-progress-control {
position: absolute;
left: 4.8em; right: 4.8em; /* Leave room for time displays. */
height: 1.0em; width: auto;
top: -1.3em; /* Set above the rest of the controls. And leave room for 2px of borders (progress bottom and controls top). */
border-bottom: 1px solid #1F1F1F;
border-top: 1px solid #222;
/* CSS Gradient */
background: #333;
background: -moz-linear-gradient(top, #222, #333);
background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#222), to(#333));
background: -webkit-linear-gradient(top, #222, #333);
background: -o-linear-gradient(top, #333, #222);
background: -ms-linear-gradient(top, #333, #222);
background: linear-gradient(top, #333, #222);
/* 1px top shadow */
/* -webkit-box-shadow: 0px -1px 0px 0px rgba(0, 0, 0, 0.15); -moz-box-shadow: 0px -1px 0px 0px rgba(0, 0, 0, 0.15); box-shadow: 0px -1px 0px 0px rgba(0, 0, 0, 0.15);*/
}
/* Box containing play and load progresses. Also acts as seek scrubber. */
.vjs-default-skin .vjs-progress-holder {
position: relative; cursor: pointer !important; /*overflow: hidden;*/
padding: 0; margin: 0; /* Placement within the progress control item */
height: 1.0em;
-moz-border-radius: 0.6em; -webkit-border-radius: 0.6em; border-radius: 0.6em;
/* CSS Gradient */
background: #111;
background: -moz-linear-gradient(top, #111, #262626);
background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#111), to(#262626));
background: -webkit-linear-gradient(top, #111, #262626);
background: -o-linear-gradient(top, #111, #262626);
background: -ms-linear-gradient(top, #111, #262626);
background: linear-gradient(top, #111, #262626);
}
.vjs-default-skin .vjs-progress-holder .vjs-play-progress,
.vjs-default-skin .vjs-progress-holder .vjs-load-progress { /* Progress Bars */
position: absolute; display: block; height: 1.0em; margin: 0; padding: 0;
left: 0; top: 0; /*Needed for IE6*/
-moz-border-radius: 0.6em; -webkit-border-radius: 0.6em; border-radius: 0.6em;
/*width: 0;*/
}
.vjs-default-skin .vjs-play-progress {
/* CSS Gradient. */
background: #fff; /* Old browsers */
background: -moz-linear-gradient(top, #fff 0%, #d6d6d6 50%, #fff 100%);
background: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(0%,#fff), color-stop(50%,#d6d6d6), color-stop(100%,#fff));
background: -webkit-linear-gradient(top, #fff 0%,#d6d6d6 50%,#fff 100%);
background: -o-linear-gradient(top, #fff 0%,#d6d6d6 50%,#fff 100%);
background: -ms-linear-gradient(top, #fff 0%,#d6d6d6 50%,#fff 100%);
background: linear-gradient(top, #fff 0%,#d6d6d6 50%,#fff 100%);
background: #efefef;
background: -moz-linear-gradient(top, #efefef 0%, #f5f5f5 50%, #dbdbdb 50%, #f1f1f1 100%);
background: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(0%,#efefef), color-stop(50%,#f5f5f5), color-stop(50%,#dbdbdb), color-stop(100%,#f1f1f1));
background: -webkit-linear-gradient(top, #efefef 0%,#f5f5f5 50%,#dbdbdb 50%,#f1f1f1 100%);
background: -o-linear-gradient(top, #efefef 0%,#f5f5f5 50%,#dbdbdb 50%,#f1f1f1 100%);
background: -ms-linear-gradient(top, #efefef 0%,#f5f5f5 50%,#dbdbdb 50%,#f1f1f1 100%);
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#efefef', endColorstr='#f1f1f1',GradientType=0 );
background: linear-gradient(top, #efefef 0%,#f5f5f5 50%,#dbdbdb 50%,#f1f1f1 100%);
}
.vjs-default-skin .vjs-load-progress {
opacity: 0.8;
/* CSS Gradient */
background: #666;
background: -moz-linear-gradient(top, #666, #333);
background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#666), to(#333));
background: -webkit-linear-gradient(top, #666, #333);
background: -o-linear-gradient(top, #666, #333);
background: -ms-linear-gradient(top, #666, #333);
background: linear-gradient(top, #666, #333);
}
.vjs-default-skin div.vjs-seek-handle {
position: absolute;
width: 16px; height: 16px; /* Match img pixles */
margin-top: -0.3em;
left: 0; top: 0; /*Needed for IE6*/
background: url('video-js.png') 0 -50px;
/* CSS Curved Corners. Needed to make shadows curved. */
-moz-border-radius: 0.8em; -webkit-border-radius: 0.8em; border-radius: 0.8em;
/* CSS Shadows */
-webkit-box-shadow: 0 2px 4px 0 #000; -moz-box-shadow: 0 2px 4px 0 #000; box-shadow: 0 2px 4px 0 #000;
}
/* Time Display
-------------------------------------------------------------------------------- */
.vjs-default-skin .vjs-time-controls {
position: absolute;
right: 0;
height: 1.0em; width: 4.8em;
top: -1.3em;
border-bottom: 1px solid #1F1F1F;
border-top: 1px solid #222;
background-color: #333;
font-size: 1em; line-height: 1.0em; font-weight: normal; font-family: Helvetica, Arial, sans-serif;
background: #333;
background: -moz-linear-gradient(top, #222, #333);
background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#222), to(#333));
background: -webkit-linear-gradient(top, #222, #333);
background: -o-linear-gradient(top, #333, #222);
background: -ms-linear-gradient(top, #333, #222);
background: linear-gradient(top, #333, #222);
/* 1px top shadow */
/* -webkit-box-shadow: 0px -1px 0px 0px rgba(0, 0, 0, 0.15); -moz-box-shadow: 0px -1px 0px 0px rgba(0, 0, 0, 0.15); box-shadow: 0px -1px 0px 0px rgba(0, 0, 0, 0.15);*/
}
.vjs-default-skin .vjs-current-time { left: 0; }
.vjs-default-skin .vjs-duration { right: 0; display: none; }
.vjs-default-skin .vjs-remaining-time { right: 0; }
.vjs-time-divider { display:none; }
.vjs-default-skin .vjs-time-control { font-size: 1em; line-height: 1; font-weight: normal; font-family: Helvetica, Arial, sans-serif; }
.vjs-default-skin .vjs-time-control span { line-height: 25px; /* Centering vertically */ }
/* Fullscreen
-------------------------------------------------------------------------------- */
.vjs-secondary-controls { float: right; }
.vjs-default-skin .vjs-fullscreen-control { width: 3.8em; cursor: pointer !important; float: right; }
.vjs-default-skin .vjs-fullscreen-control div { width: 16px; height: 16px; background: url('video-js.png') -50px 0; margin: 0.5em auto 0; }
.vjs-default-skin.vjs-fullscreen .vjs-fullscreen-control div { background: url('video-js.png') -75px 0; }
/* Big Play Button (at start)
---------------------------------------------------------*/
.vjs-default-skin .vjs-big-play-button {
display: block; /* Start hidden */ z-index: 2;
position: absolute; top: 50%; left: 50%; width: 8.0em; height: 8.0em; margin: -43px 0 0 -43px; text-align: center; vertical-align: center; cursor: pointer !important;
border: 0.3em solid #fff; opacity: 0.95;
-webkit-border-radius: 25px; -moz-border-radius: 25px; border-radius: 25px;
background: #454545;
background: -moz-linear-gradient(top, #454545 0%, #232323 50%, #161616 50%, #3f3f3f 100%);
background: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(0%,#454545), color-stop(50%,#232323), color-stop(50%,#161616), color-stop(100%,#3f3f3f));
background: -webkit-linear-gradient(top, #454545 0%,#232323 50%,#161616 50%,#3f3f3f 100%);
background: -o-linear-gradient(top, #454545 0%,#232323 50%,#161616 50%,#3f3f3f 100%);
background: -ms-linear-gradient(top, #454545 0%,#232323 50%,#161616 50%,#3f3f3f 100%);
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#454545', endColorstr='#3f3f3f',GradientType=0 );
background: linear-gradient(top, #454545 0%,#232323 50%,#161616 50%,#3f3f3f 100%);
/* CSS Shadows */
-webkit-box-shadow: 4px 4px 8px #000; -moz-box-shadow: 4px 4px 8px #000; box-shadow: 4px 4px 8px #000;
}
.vjs-default-skin div.vjs-big-play-button:hover {
-webkit-box-shadow: 0 0 80px #fff; -moz-box-shadow: 0 0 80px #fff; box-shadow: 0 0 80px #fff;
}
.vjs-default-skin div.vjs-big-play-button span {
position: absolute; top: 50%; left: 50%;
display: block; width: 35px; height: 42px;
margin: -20px 0 0 -15px; /* Using negative margin to center image. */
background: url('video-js.png') -100px 0;
}
/* Loading Spinner
---------------------------------------------------------*/
/* CSS Spinners by Kilian Valkhof - http://kilianvalkhof.com/2010/css-xhtml/css3-loading-spinners-without-images/ */
.vjs-loading-spinner {
display: none;
position: absolute; top: 50%; left: 50%; width: 55px; height: 55px;
margin: -28px 0 0 -28px;
-webkit-animation-name: rotatethis;
-webkit-animation-duration:1s;
-webkit-animation-iteration-count:infinite;
-webkit-animation-timing-function:linear;
-moz-animation-name: rotatethis;
-moz-animation-duration:1s;
-moz-animation-iteration-count:infinite;
-moz-animation-timing-function:linear;
}
@-webkit-keyframes rotatethis {
0% {-webkit-transform:scale(0.6) rotate(0deg); }
12.5% {-webkit-transform:scale(0.6) rotate(0deg); }
12.51% {-webkit-transform:scale(0.6) rotate(45deg); }
25% {-webkit-transform:scale(0.6) rotate(45deg); }
25.01% {-webkit-transform:scale(0.6) rotate(90deg);}
37.5% {-webkit-transform:scale(0.6) rotate(90deg);}
37.51% {-webkit-transform:scale(0.6) rotate(135deg);}
50% {-webkit-transform:scale(0.6) rotate(135deg);}
50.01% {-webkit-transform:scale(0.6) rotate(180deg);}
62.5% {-webkit-transform:scale(0.6) rotate(180deg);}
62.51% {-webkit-transform:scale(0.6) rotate(225deg);}
75% {-webkit-transform:scale(0.6) rotate(225deg);}
75.01% {-webkit-transform:scale(0.6) rotate(270deg);}
87.5% {-webkit-transform:scale(0.6) rotate(270deg);}
87.51% {-webkit-transform:scale(0.6) rotate(315deg);}
100% {-webkit-transform:scale(0.6) rotate(315deg);}
}
@-moz-keyframes rotatethis {
0% {-moz-transform:scale(0.6) rotate(0deg);}
12.5% {-moz-transform:scale(0.6) rotate(0deg);}
12.51% {-moz-transform:scale(0.6) rotate(45deg);}
25% {-moz-transform:scale(0.6) rotate(45deg);}
25.01% {-moz-transform:scale(0.6) rotate(90deg);}
37.5% {-moz-transform:scale(0.6) rotate(90deg);}
37.51% {-moz-transform:scale(0.6) rotate(135deg);}
50% {-moz-transform:scale(0.6) rotate(135deg);}
50.01% {-moz-transform:scale(0.6) rotate(180deg);}
62.5% {-moz-transform:scale(0.6) rotate(180deg);}
62.51% {-moz-transform:scale(0.6) rotate(225deg);}
75% {-moz-transform:scale(0.6) rotate(225deg);}
75.01% {-moz-transform:scale(0.6) rotate(270deg);}
87.5% {-moz-transform:scale(0.6) rotate(270deg);}
87.51% {-moz-transform:scale(0.6) rotate(315deg);}
100% {-moz-transform:scale(0.6) rotate(315deg);}
}
/* Each circle */
div.vjs-loading-spinner .ball1 { opacity: 0.12; position:absolute; left: 20px; top: 0px; width: 13px; height: 13px; background: #fff;
border-radius: 13px; -webkit-border-radius: 13px; -moz-border-radius: 13px; border: 1px solid #ccc; }
div.vjs-loading-spinner .ball2 { opacity: 0.25; position:absolute; left: 34px; top: 6px; width: 13px; height: 13px; background: #fff;
border-radius: 13px; -webkit-border-radius: 13px; -moz-border-radius: 13px; border: 1px solid #ccc; }
div.vjs-loading-spinner .ball3 { opacity: 0.37; position:absolute; left: 40px; top: 20px; width: 13px; height: 13px; background: #fff;
border-radius: 13px; -webkit-border-radius: 13px; -moz-border-radius: 13px; border: 1px solid #ccc; }
div.vjs-loading-spinner .ball4 { opacity: 0.50; position:absolute; left: 34px; top: 34px; width: 13px; height: 13px; background: #fff;
border-radius: 10px; -webkit-border-radius: 10px; -moz-border-radius: 15px; border: 1px solid #ccc; }
div.vjs-loading-spinner .ball5 { opacity: 0.62; position:absolute; left: 20px; top: 40px; width: 13px; height: 13px; background: #fff;
border-radius: 13px; -webkit-border-radius: 13px; -moz-border-radius: 13px; border: 1px solid #ccc; }
div.vjs-loading-spinner .ball6 { opacity: 0.75; position:absolute; left: 6px; top: 34px; width: 13px; height: 13px; background: #fff;
border-radius: 13px; -webkit-border-radius: 13px; -moz-border-radius: 13px; border: 1px solid #ccc; }
div.vjs-loading-spinner .ball7 { opacity: 0.87; position:absolute; left: 0px; top: 20px; width: 13px; height: 13px; background: #fff;
border-radius: 13px; -webkit-border-radius: 13px; -moz-border-radius: 13px; border: 1px solid #ccc; }
div.vjs-loading-spinner .ball8 { opacity: 1.00; position:absolute; left: 6px; top: 6px; width: 13px; height: 13px; background: #fff;
border-radius: 13px; -webkit-border-radius: 13px; -moz-border-radius: 13px; border: 1px solid #ccc; }

1
extlib/video-js/video-js.min.css vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

3744
extlib/video-js/video.js Normal file

File diff suppressed because it is too large Load Diff

21
extlib/video-js/video.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -120,7 +120,7 @@ def login(request):
login_failed = False
if request.method == 'POST' and login_form.validate():
user = request.db.User.one(
user = request.db.User.find_one(
{'username': request.POST['username'].lower()})
if user and user.check_login(request.POST['password']):

View File

@ -2,6 +2,9 @@
# HTML title of the pages
html_title = string(default="GNU MediaGoblin")
# link to source for this MediaGoblin site
source_link = string(default="https://gitorious.org/mediagoblin/mediagoblin")
# Enabled media types
media_types = string_list(default=list("mediagoblin.media_types.image"))

View File

@ -155,6 +155,22 @@ def convert_video_media_data(database):
collection.save(document)
@RegisterMigration(11)
def convert_gps_media_data(database):
"""
Move media_data["gps"]["*"] to media_data["gps_*"].
In preparation for media_data.gps_*
"""
collection = database['media_entries']
target = collection.find(
{'media_data.gps': {'$exists': True}})
for document in target:
for key, value in document['media_data']['gps'].iteritems():
document['media_data']['gps_' + key] = value
del document['media_data']['gps']
collection.save(document)
@RegisterMigration(12)
def user_add_wants_comment_notification(database):
"""
Add wants_comment_notification to user model

View File

@ -310,3 +310,9 @@ def check_media_slug_used(db, uploader_id, slug, ignore_m_id):
existing_user_slug_entries = db.MediaEntry.find(
query_dict).count()
return existing_user_slug_entries
def media_entries_for_tag_slug(db, tag_slug):
return db.MediaEntry.find(
{u'state': u'processed',
u'tags.slug': tag_slug})

View File

@ -14,12 +14,14 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from copy import copy
from mediagoblin.init import setup_global_and_app_config, setup_database
from mediagoblin.db.mongo.util import ObjectId
from mediagoblin.db.sql.models import (Base, User, MediaEntry, MediaComment,
Tag, MediaTag, MediaFile, MediaAttachmentFile)
Tag, MediaTag, MediaFile, MediaAttachmentFile, MigrationData)
from mediagoblin.media_types.image.models import ImageData
from mediagoblin.media_types.video.models import VideoData
from mediagoblin.db.sql.open import setup_connection_and_db_from_config as \
sql_connect
@ -106,6 +108,38 @@ def convert_media_entries(mk_db):
session.close()
def convert_image(mk_db):
session = Session()
for media in mk_db.MediaEntry.find(
{'media_type': 'mediagoblin.media_types.image'}).sort('created'):
media_data = copy(media.media_data)
# TODO: Fix after exif is migrated
media_data.pop('exif', None)
if len(media_data):
media_data_row = ImageData(**media_data)
media_data_row.media_entry = obj_id_table[media._id]
session.add(media_data_row)
session.commit()
session.close()
def convert_video(mk_db):
session = Session()
for media in mk_db.MediaEntry.find(
{'media_type': 'mediagoblin.media_types.video'}).sort('created'):
media_data_row = VideoData(**media.media_data)
media_data_row.media_entry = obj_id_table[media._id]
session.add(media_data_row)
session.commit()
session.close()
def convert_media_tags(mk_db):
session = Session()
session.autoflush = False
@ -155,6 +189,20 @@ def convert_media_comments(mk_db):
session.close()
def convert_add_migration_versions():
session = Session()
for name in ("__main__",
"mediagoblin.media_types.image",
"mediagoblin.media_types.video",
):
m = MigrationData(name=name, version=0)
session.add(m)
session.commit()
session.close()
def run_conversion(config_name):
global_config, app_config = setup_global_and_app_config(config_name)
@ -167,10 +215,16 @@ def run_conversion(config_name):
Session.remove()
convert_media_entries(mk_db)
Session.remove()
convert_image(mk_db)
Session.remove()
convert_video(mk_db)
Session.remove()
convert_media_tags(mk_db)
Session.remove()
convert_media_comments(mk_db)
Session.remove()
convert_add_migration_versions()
Session.remove()
if __name__ == '__main__':

View File

@ -20,6 +20,7 @@ TODO: indexes on foreignkeys, where useful.
import datetime
import sys
from sqlalchemy import (
Column, Integer, Unicode, UnicodeText, DateTime, Boolean, ForeignKey,
@ -28,10 +29,12 @@ from sqlalchemy.orm import relationship
from sqlalchemy.orm.collections import attribute_mapped_collection
from sqlalchemy.sql.expression import desc
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.util import memoized_property
from mediagoblin.db.sql.extratypes import PathTupleWithSlashes, JSONEncoded
from mediagoblin.db.sql.base import Base, DictReadAttrProxy
from mediagoblin.db.mixin import UserMixin, MediaEntryMixin, MediaCommentMixin
from mediagoblin.db.sql.base import Session
# It's actually kind of annoying how sqlalchemy-migrate does this, if
# I understand it right, but whatever. Anyway, don't remove this :P
@ -58,7 +61,7 @@ class User(Base, UserMixin):
TODO: We should consider moving some rarely used fields
into some sort of "shadow" table.
"""
__tablename__ = "users"
__tablename__ = "core__users"
id = Column(Integer, primary_key=True)
username = Column(Unicode, nullable=False, unique=True)
@ -85,10 +88,10 @@ class MediaEntry(Base, MediaEntryMixin):
"""
TODO: Consider fetching the media_files using join
"""
__tablename__ = "media_entries"
__tablename__ = "core__media_entries"
id = Column(Integer, primary_key=True)
uploader = Column(Integer, ForeignKey('users.id'), nullable=False)
uploader = Column(Integer, ForeignKey('core__users.id'), nullable=False)
title = Column(Unicode, nullable=False)
slug = Column(Unicode)
created = Column(DateTime, nullable=False, default=datetime.datetime.now)
@ -168,14 +171,39 @@ class MediaEntry(Base, MediaEntryMixin):
if media is not None:
return media.url_for_self(urlgen)
#@memoized_property
@property
def media_data(self):
# TODO: Replace with proper code to read the correct table
return {}
session = Session()
return session.query(self.media_data_table).filter_by(
media_entry=self.id).first()
def media_data_init(self, **kwargs):
# TODO: Implement this
pass
"""
Initialize or update the contents of a media entry's media_data row
"""
session = Session()
media_data = session.query(self.media_data_table).filter_by(
media_entry=self.id).first()
# No media data, so actually add a new one
if not media_data:
media_data = self.media_data_table(
**kwargs)
session.add(media_data)
# Update old media data
else:
for field, value in kwargs.iteritems():
setattr(media_data, field, value)
@memoized_property
def media_data_table(self):
# TODO: memoize this
models_module = self.media_type + '.models'
__import__(models_module)
return sys.modules[models_module].DATA_MODEL
class FileKeynames(Base):
@ -203,7 +231,7 @@ class MediaFile(Base):
TODO: Highly consider moving "name" into a new table.
TODO: Consider preloading said table in software
"""
__tablename__ = "mediafiles"
__tablename__ = "core__mediafiles"
media_entry = Column(
Integer, ForeignKey(MediaEntry.id),
@ -242,7 +270,7 @@ class MediaAttachmentFile(Base):
class Tag(Base):
__tablename__ = "tags"
__tablename__ = "core__tags"
id = Column(Integer, primary_key=True)
slug = Column(Unicode, nullable=False, unique=True)
@ -259,13 +287,13 @@ class Tag(Base):
class MediaTag(Base):
__tablename__ = "media_tags"
__tablename__ = "core__media_tags"
id = Column(Integer, primary_key=True)
media_entry = Column(
Integer, ForeignKey(MediaEntry.id),
nullable=False)
tag = Column(Integer, ForeignKey('tags.id'), nullable=False)
tag = Column(Integer, ForeignKey('core__tags.id'), nullable=False)
name = Column(Unicode)
# created = Column(DateTime, nullable=False, default=datetime.datetime.now)
@ -292,12 +320,12 @@ class MediaTag(Base):
class MediaComment(Base, MediaCommentMixin):
__tablename__ = "media_comments"
__tablename__ = "core__media_comments"
id = Column(Integer, primary_key=True)
media_entry = Column(
Integer, ForeignKey('media_entries.id'), nullable=False)
author = Column(Integer, ForeignKey('users.id'), nullable=False)
Integer, ForeignKey('core__media_entries.id'), nullable=False)
author = Column(Integer, ForeignKey('core__users.id'), nullable=False)
created = Column(DateTime, nullable=False, default=datetime.datetime.now)
content = Column(UnicodeText, nullable=False)
@ -319,7 +347,7 @@ MODELS = [
######################################################
class MigrationData(Base):
__tablename__ = "migrations"
__tablename__ = "core__migrations"
name = Column(Unicode, primary_key=True)
version = Column(Integer, nullable=False, default=0)

View File

@ -294,6 +294,15 @@ def check_media_slug_used(dummy_db, uploader_id, slug, ignore_m_id):
return does_exist
def media_entries_for_tag_slug(dummy_db, tag_slug):
return MediaEntry.query \
.join(MediaEntry.tags_helper) \
.join(MediaTag.tag_helper) \
.filter(
(MediaEntry.state == u'processed')
& (Tag.slug == tag_slug))
def clean_orphan_tags():
q1 = Session.query(Tag).outerjoin(MediaTag).filter(MediaTag.id==None)
for t in q1:

View File

@ -21,7 +21,9 @@ except ImportError:
if use_sql:
from mediagoblin.db.sql.fake import ObjectId, InvalidId, DESCENDING
from mediagoblin.db.sql.util import atomic_update, check_media_slug_used
from mediagoblin.db.sql.util import atomic_update, check_media_slug_used, \
media_entries_for_tag_slug
else:
from mediagoblin.db.mongo.util import \
ObjectId, InvalidId, DESCENDING, atomic_update, check_media_slug_used
ObjectId, InvalidId, DESCENDING, atomic_update, \
check_media_slug_used, media_entries_for_tag_slug

View File

@ -17,7 +17,7 @@
import sys
from mediagoblin.db.mongo import util as db_util
from mediagoblin.db.open import setup_connection_and_db_from_config
from mediagoblin.db.mongo.open import setup_connection_and_db_from_config
from mediagoblin.init import setup_global_and_app_config
# This MUST be imported so as to set up the appropriate migrations!
@ -41,7 +41,12 @@ def _print_finished_migration(migration_number, migration_func):
def migrate(args):
global_config, app_config = setup_global_and_app_config(args.conf_file)
run_migrate(args.conf_file)
def run_migrate(conf_file):
global_config, app_config = setup_global_and_app_config(conf_file)
connection, db = setup_connection_and_db_from_config(
app_config, use_pymongo=True)
migration_manager = db_util.MigrationManager(db)

View File

@ -14,12 +14,15 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from mediagoblin.db.sql.convert import run_conversion
def mongosql_parser_setup(subparser):
pass
def mongosql(args):
# First, make sure our mongo migrations are up to date...
from mediagoblin.gmg_commands.migrate import run_migrate
run_migrate(args.conf_file)
from mediagoblin.db.sql.convert import run_conversion
run_conversion(args.conf_file)

View File

@ -22,7 +22,9 @@ from mediagoblin.gmg_commands import util as commands_util
def shell_parser_setup(subparser):
pass
subparser.add_argument(
'--ipython', help='Use ipython',
action="store_true")
SHELL_BANNER = """\
@ -34,16 +36,42 @@ Available vars:
- db: database instance
"""
def py_shell(**user_namespace):
"""
Run a shell using normal python shell.
"""
code.interact(
banner=SHELL_BANNER,
local=user_namespace)
def ipython_shell(**user_namespace):
"""
Run a shell for the user using ipython.
"""
try:
from IPython import embed
except:
print "IPython not available... exiting!"
return
embed(
banner1=SHELL_BANNER,
user_ns=user_namespace)
def shell(args):
"""
Setup a shell for the user
either a normal Python shell
or an IPython one
"""
mgoblin_app = commands_util.setup_app(args)
user_namespace = {
'mg_globals': mg_globals,
'mgoblin_app': commands_util.setup_app(args),
'db': mg_globals.database}
code.interact(
banner=SHELL_BANNER,
local={
'mgoblin_app': mgoblin_app,
'mg_globals': mg_globals,
'db': mg_globals.database})
if args.ipython:
ipython_shell(**user_namespace)
else:
py_shell(**user_namespace)

View File

@ -14,7 +14,7 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from mediagoblin.db.util import DESCENDING
from mediagoblin.db.util import media_entries_for_tag_slug, DESCENDING
from mediagoblin.tools.pagination import Pagination
from mediagoblin.tools.response import render_to_response
@ -29,11 +29,16 @@ def _get_tag_name_from_entries(media_entries, tag_slug):
"""
# ... this is slightly hacky looking :\
tag_name = tag_slug
if media_entries.count():
for tag in media_entries[0]['tags']:
for entry in media_entries:
for tag in entry.tags:
if tag['slug'] == tag_slug:
tag_name == tag['name']
tag_name = tag['name']
break
break
# TODO: Remove after SQL-switch, it's mongo specific
if hasattr(media_entries, "rewind"):
media_entries.rewind()
return tag_name
@ -43,9 +48,7 @@ def tag_listing(request, page):
"""'Gallery'/listing for this tag slug"""
tag_slug = request.matchdict[u'tag']
cursor = request.db.MediaEntry.find(
{u'state': u'processed',
u'tags.slug': tag_slug})
cursor = media_entries_for_tag_slug(request.db, tag_slug)
cursor = cursor.sort('created', DESCENDING)
pagination = Pagination(page, cursor)

View File

@ -23,11 +23,11 @@ from sqlalchemy import (
class AsciiData(Base):
__tablename__ = "ascii_data"
__tablename__ = "ascii__mediadata"
id = Column(Integer, primary_key=True)
media_entry = Column(
Integer, ForeignKey('media_entries.id'), nullable=False)
Integer, ForeignKey('core__media_entries.id'), nullable=False)
DATA_MODEL = AsciiData

View File

@ -5,15 +5,17 @@ from sqlalchemy import (
class ImageData(Base):
__tablename__ = "image_data"
__tablename__ = "image__mediadata"
# The primary key *and* reference to the main media_entry
media_entry = Column(Integer, ForeignKey('media_entries.id'),
media_entry = Column(Integer, ForeignKey('core__media_entries.id'),
primary_key=True)
width = Column(Integer)
height = Column(Integer)
gps_longitude = Column(Float)
gps_latitude = Column(Float)
gps_altitude = Column(Float)
gps_direction = Column(Float)
DATA_MODEL = ImageData

View File

@ -115,11 +115,18 @@ def process_image(entry):
# Insert exif data into database
media_data = entry.setdefault('media_data', {})
media_data['exif'] = {
'clean': clean_exif(exif_tags)}
media_data['exif']['useful'] = get_useful(
media_data['exif']['clean'])
media_data['gps'] = gps_data
# TODO: Fix for sql media_data, when exif is in sql
if media_data is not None:
media_data['exif'] = {
'clean': clean_exif(exif_tags)}
media_data['exif']['useful'] = get_useful(
media_data['exif']['clean'])
if len(gps_data):
for key in list(gps_data.keys()):
gps_data['gps_' + key] = gps_data.pop(key)
entry.media_data_init(**gps_data)
# clean up workbench
workbench.destroy_self()

View File

@ -22,10 +22,10 @@ from sqlalchemy import (
class VideoData(Base):
__tablename__ = "video_data"
__tablename__ = "video__mediadata"
# The primary key *and* reference to the main media_entry
media_entry = Column(Integer, ForeignKey('media_entries.id'),
media_entry = Column(Integer, ForeignKey('core__media_entries.id'),
primary_key=True)
width = Column(SmallInteger)
height = Column(SmallInteger)

View File

@ -234,13 +234,14 @@ text-align: center;
height: 0;
}
h3.sidedata {
border: none;
background-color: #212121;
border-radius: 4px 4px 0 0;
padding: 3px 8px;
margin: 20px 0 5px 0;
.media_sidebar h3 {
font-size: 1em;
margin: 0 0 5px;
border: none;
}
.media_sidebar p {
padding-left: 8px;
}
/* forms */

View File

@ -0,0 +1 @@
../../../../extlib/video-js

View File

@ -82,7 +82,10 @@
{% block mediagoblin_footer %}
<footer>
{% trans -%}
Powered by <a href="http://mediagoblin.org">MediaGoblin</a>, a <a href="http://gnu.org/">GNU</a> project
Powered by <a href="http://mediagoblin.org">MediaGoblin</a>, a <a href="http://gnu.org/">GNU</a> project.
{%- endtrans %}
{% trans source_link=app_config['source_link'] -%}
Released under the <a href="http://www.fsf.org/licensing/licenses/agpl-3.0.html">AGPL</a>. <a href="{{ source_link }}">Source code</a> available.
{%- endtrans %}
</footer>
{% endblock mediagoblin_footer %}

View File

@ -18,6 +18,13 @@
{% extends 'mediagoblin/user_pages/media.html' %}
{% block mediagoblin_head %}
{{ super() }}
<script type="text/javascript"
src="{{ request.staticdirect('/js/extlib/video-js/video.js') }}"></script>
<link href="{{ request.staticdirect('/js/extlib/video-js/video-js.css') }}" rel="stylesheet">
{% endblock %}
{% block mediagoblin_media %}
<div class="video-player" style="position: relative;">
<video class="video-js vjs-default-skin"

View File

@ -172,7 +172,7 @@
</div>
<div class="media_sidebar">
{% trans date=media.created.strftime("%Y-%m-%d") -%}
<h3 class="sidedata">Added on</h3>
<h3>Added on</h3>
<p>{{ date }}</p>
{%- endtrans %}
{% if media.tags %}

View File

@ -17,10 +17,10 @@
#}
{% block exif_content %}
{% if media.media_data.has_key('exif')
and app_config['exif_visible']
{% if app_config['exif_visible']
and media.media_data.exif is defined
and media.media_data.exif.has_key('useful') %}
<h3 class="sidedata">EXIF</h3>
<h3>EXIF</h3>
<table>
{% for key, tag in media.media_data.exif.useful.items() %}
<tr>

View File

@ -17,24 +17,27 @@
#}
{% block geolocation_map %}
{% if media.media_data.has_key('gps')
and app_config['geolocation_map_visible']
and media.media_data.gps %}
<h3 class="sidedata">Location</h3>
{% if app_config['geolocation_map_visible']
and media.media_data.gps_latitude is defined
and media.media_data.gps_latitude
and media.media_data.gps_longitude is defined
and media.media_data.gps_longitude %}
<h3>{% trans %}Location{% endtrans %}</h3>
<div>
{% set gps = media.media_data.gps %}
{%- set lon = media.media_data.gps_longitude %}
{%- set lat = media.media_data.gps_latitude %}
{%- set osm_url = "http://openstreetmap.org/?mlat={lat}&mlon={lon}".format(lat=lat, lon=lon) %}
<div id="tile-map" style="width: 100%; height: 196px;">
<input type="hidden" id="gps-longitude"
value="{{ gps.longitude }}" />
value="{{ lon }}" />
<input type="hidden" id="gps-latitude"
value="{{ gps.latitude }}" />
value="{{ lat }}" />
</div>
<p>
<small>
View on
<a href="http://openstreetmap.org/?mlat={{ gps.latitude }}&mlon={{ gps.longitude }}">
OpenStreetMap
</a>
{% trans -%}
View on <a href="{{ osm_url }}">OpenStreetMap</a>
{%- endtrans %}
</small>
</p>
</div>

View File

@ -17,7 +17,7 @@
#}
{% block license_content -%}
<h3 class="sidedata">{% trans %}License{% endtrans %}</h3>
<h3>{% trans %}License{% endtrans %}</h3>
<p>
{% if media.license %}
<a href="{{ media.license }}">{{ media.get_license_data().abbreviation }}</a>

View File

@ -17,7 +17,7 @@
#}
{% block tags_content -%}
<h3 class="sidedata">Tagged with</h3>
<h3>{% trans %}Tagged with{% endtrans %}</h3>
<p>
{% for tag in media.tags %}
{% if loop.last %}

View File

@ -180,6 +180,18 @@ class TestSubmission:
# Does media entry exist?
assert_true(media)
# Add a comment, so we can test for its deletion later.
get_comments = lambda: list(
request.db.MediaComment.find({'media_entry': media._id}))
assert_false(get_comments())
response = self.test_app.post(
request.urlgen('mediagoblin.user_pages.media_post_comment',
user=self.test_user.username,
media=media._id),
{'comment_content': 'i love this test'})
response.follow()
assert_true(get_comments())
# Do not confirm deletion
# ---------------------------------------------------
response = self.test_app.post(
@ -219,6 +231,9 @@ class TestSubmission:
request.db.MediaEntry.find(
{'_id': media._id}).count())
# How about the comment?
assert_false(get_comments())
def test_malicious_uploads(self):
# Test non-suppoerted file with non-supported extension
# -----------------------------------------------------

View File

@ -14,8 +14,12 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
from mediagoblin.db.util import ObjectId, InvalidId
_log = logging.getLogger(__name__)
def setup_user_in_request(request):
"""
Examine a request and tack on a request.user parameter if that's
@ -30,12 +34,12 @@ def setup_user_in_request(request):
except InvalidId:
user = None
else:
user = request.db.User.one({'_id': oid})
user = request.db.User.find_one({'_id': oid})
if not user:
# Something's wrong... this user doesn't exist? Invalidate
# this session.
print "Killing session for %r" % request.session['user_id']
_log.warn("Killing session for user id %r", request.session['user_id'])
request.session.invalidate()
request.user = user

View File

@ -21,7 +21,7 @@ from mediagoblin.tools.translate import fake_ugettext_passthrough as _
class MediaCommentForm(wtforms.Form):
comment_content = wtforms.TextAreaField(
_(''),
'',
[wtforms.validators.Required()])

View File

@ -180,6 +180,10 @@ def media_confirm_delete(request, media):
if form.confirm.data is True:
username = media.get_uploader.username
# Delete all the associated comments
for comment in media.get_comments():
comment.delete()
# Delete all files on the public storage
delete_media_files(media)