Skip to content
This repository has been archived by the owner on Feb 13, 2019. It is now read-only.

Commit

Permalink
Merge branch 'master' into ia-logging
Browse files Browse the repository at this point in the history
  • Loading branch information
MichaelButkovic committed Jun 10, 2016
2 parents 8e581e8 + b0420f2 commit 4386876
Show file tree
Hide file tree
Showing 18 changed files with 431 additions and 20 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Development

## Version 2.7.0

- Added `videos.VideoMixin` and merged `videohub-client` repository into `django-bulbs` (one less repository!)

## Version 2.6.0

- Added Series Detail View to Videos App.
Expand Down
2 changes: 1 addition & 1 deletion bulbs/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "2.6.0"
__version__ = "2.7.0"
8 changes: 6 additions & 2 deletions bulbs/content/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,13 @@ def SponsoredBoost(field_name, boost_mode="multiply", weight=5):
def VideohubChannel(included_ids=None, excluded_ids=None):
f = MatchAll()
if included_ids:
f &= Nested(path="video", filter=Terms(**{"video.channel_id": included_ids}))
f &= Nested(path="videohub_ref", filter=Terms(**{"videohub_ref.channel_id": included_ids}))
if excluded_ids:
f &= ~Nested(path="video", filter=Terms(**{"video.channel_id": excluded_ids}))
f &= ~Nested(path="videohub_ref", filter=Terms(**{"videohub_ref.channel_id": excluded_ids}))


def VideohubVideo():
return Nested(path='videohub_ref', filter=Exists(field='videohub_ref.id'))


def TagBoost(slugs, boost_mode="multiply", weight=5):
Expand Down
10 changes: 7 additions & 3 deletions bulbs/content/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from .filters import (
Authors, Evergreen, FeatureTypes, InstantArticle, Published, Status, Tags,
VideohubChannel
VideohubChannel, VideohubVideo,
)


Expand All @@ -33,10 +33,14 @@ def evergreen(self, included_channel_ids=None, excluded_channel_ids=None, **kwar
def evergreen_video(self, **kwargs):
"""Filter evergreen content to exclusively video content."""
eqs = self.evergreen(**kwargs)
video_doc_type = getattr(settings, "VIDEO_DOC_TYPE", "")
eqs = eqs.filter(es_filter.Type(value=video_doc_type))
eqs = eqs.filter(VideohubVideo())
return eqs

def videos(self, **kwargs):
return self.search(**kwargs).filter(
VideohubVideo()
)

def instant_articles(self, **kwargs):
"""
QuerySet including all published content approved for instant articles.
Expand Down
30 changes: 30 additions & 0 deletions bulbs/videos/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import migrations, models
import djbetty.fields


class Migration(migrations.Migration):

dependencies = [
]

operations = [
migrations.CreateModel(
name='VideohubVideo',
fields=[
('id', models.IntegerField(serialize=False, primary_key=True)),
('title', models.CharField(max_length=512)),
('description', models.TextField(blank=True, default='')),
('keywords', models.TextField(blank=True, default='')),
('image', djbetty.fields.ImageField(blank=True, null=True, alt_field='_image_alt', default=None, caption_field='_image_caption')),
('_image_alt', models.CharField(blank=True, null=True, max_length=255, editable=False)),
('_image_caption', models.CharField(blank=True, null=True, max_length=255, editable=False)),
('channel_id', models.IntegerField(blank=True, null=True, default=None)),
],
options={
'abstract': False,
},
),
]
Empty file.
13 changes: 13 additions & 0 deletions bulbs/videos/mixins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from django.db import models

from .models import VideohubVideo


class VideoMixin(models.Model):

"""Provides an OnionStudios (videohub) reference ID, standardized across all properties."""

videohub_ref = models.ForeignKey(VideohubVideo, null=True, blank=True)

class Meta:
abstract = True
167 changes: 167 additions & 0 deletions bulbs/videos/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import json

from djes.models import Indexable

from django.conf import settings
from django.db import models
from django.template.defaultfilters import slugify

from djbetty.fields import ImageField

import requests

import six


class VideohubVideo(Indexable):
"""A reference to a video on the onion videohub."""
title = models.CharField(max_length=512)
description = models.TextField(blank=True, default="")
keywords = models.TextField(blank=True, default="")
image = ImageField(null=True, blank=True, alt_field="_image_alt",
caption_field="_image_caption")
_image_alt = models.CharField(null=True, blank=True, editable=False, max_length=255)
_image_caption = models.CharField(null=True, blank=True, editable=False, max_length=255)
# External FK on videohub. Temp workaround until metadata refactor.
channel_id = models.IntegerField(blank=True, null=True, default=None)

# default values
DEFAULT_VIDEOHUB_VIDEO_URL = "http://videohub.local/videos/{}"
DEFAULT_VIDEOHUB_EMBED_URL = "http://videohub.local/embed?id={}"
DEFAULT_VIDEOHUB_API_URL = "http://videohub.local/api/v0/videos/{}"
DEFAULT_VIDEOHUB_API_SEARCH_URL = "http://videohub.local/api/v0/videos/search"

class Mapping:
class Meta:
# Exclude image until we actually need it, to avoid dealing with custom mappings
excludes = ('image',)

@classmethod
def get_serializer_class(cls):
from .serializers import VideohubVideoSerializer
return VideohubVideoSerializer

@classmethod
def search_videohub(cls, query, filters=None, status=None, sort=None, size=None, page=None):
"""searches the videohub given a query and applies given filters and other bits
:see: https://github.com/theonion/videohub/blob/master/docs/search/post.md
:see: https://github.com/theonion/videohub/blob/master/docs/search/get.md
:param query: query terms to search by
:type query: str
:example query: "brooklyn hipsters" # although, this is a little redundant...
:param filters: video field value restrictions
:type filters: dict
:default filters: None
:example filters: {"channel": "onion"} or {"series": "Today NOW"}
:param status: limit the results to videos that are published, scheduled, draft
:type status: str
:default status: None
:example status: "published" or "draft" or "scheduled"
:param sort: video field related sorting
:type sort: dict
:default sort: None
:example sort: {"title": "desc"} or {"description": "asc"}
:param size: the page size (number of results)
:type size: int
:default size: None
:example size": {"size": 20}
:param page: the page number of the results
:type page: int
:default page: None
:example page: {"page": 2} # note, you should use `size` in conjunction with `page`
:return: a dictionary of results and meta information
:rtype: dict
"""
# construct url
url = getattr(settings, "VIDEOHUB_API_SEARCH_URL", cls.DEFAULT_VIDEOHUB_API_SEARCH_URL)
# construct auth headers
headers = {
"Content-Type": "application/json",
"Authorization": settings.VIDEOHUB_API_TOKEN,
}
# construct payload
payload = {
"query": query,
}
if filters:
assert isinstance(filters, dict)
payload["filters"] = filters
if status:
assert isinstance(status, six.string_types)
payload.setdefault("filters", {})
payload["filters"]["status"] = status
if sort:
assert isinstance(sort, dict)
payload["sort"] = sort
if size:
assert isinstance(size, (six.string_types, int))
payload["size"] = size
if page:
assert isinstance(page, (six.string_types, int))
payload["page"] = page
# send request
res = requests.post(url, data=json.dumps(payload), headers=headers)
# raise if not 200
if res.status_code != 200:
res.raise_for_status()
# parse and return response
return json.loads(res.content)

def get_hub_url(self):
"""gets a canonical path to the detail page of the video on the hub
:return: the path to the consumer ui detail page of the video
:rtype: str
"""
url = getattr(settings, "VIDEOHUB_VIDEO_URL", self.DEFAULT_VIDEOHUB_VIDEO_URL)

# slugify needs ascii
ascii_title = ""
if isinstance(self.title, str):
ascii_title = self.title
elif six.PY2 and isinstance(self.title, six.text_type):
# Legacy unicode conversion
ascii_title = self.title.encode('ascii', 'replace')

path = slugify("{}-{}".format(ascii_title, self.id))

return url.format(path)

def get_embed_url(self, targeting=None, recirc=None):
"""gets a canonical path to an embedded iframe of the video from the hub
:return: the path to create an embedded iframe of the video
:rtype: str
"""
url = getattr(settings, "VIDEOHUB_EMBED_URL", self.DEFAULT_VIDEOHUB_EMBED_URL)
url = url.format(self.id)
if targeting is not None:
for k, v in sorted(targeting.items()):
url += '&{0}={1}'.format(k, v)
if recirc is not None:
url += '&recirc={0}'.format(recirc)
return url

def get_api_url(self):
"""gets a canonical path to the api detail url of the video on the hub
:return: the path to the api detail of the video
:rtype: str
"""
url = getattr(settings, 'VIDEOHUB_API_URL', None)
# Support alternate setting (used by most client projects)
if not url:
url = getattr(settings, 'VIDEOHUB_API_BASE_URL', None)
if url:
url = url.rstrip('/') + '/videos/{}'
if not url:
url = self.DEFAULT_VIDEOHUB_API_URL
return url.format(self.id)
42 changes: 42 additions & 0 deletions bulbs/videos/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from djbetty.serializers import ImageFieldSerializer
from rest_framework import serializers

from .models import VideohubVideo


class VideohubVideoSerializer(serializers.ModelSerializer):
id = serializers.IntegerField()
image = ImageFieldSerializer(allow_null=True, default={})
hub_url = serializers.CharField(source="get_hub_url", read_only=True)
embed_url = serializers.CharField(source="get_embed_url", read_only=True)
api_url = serializers.CharField(source="get_api_url", read_only=True)
channel_id = serializers.IntegerField()

class Meta:
model = VideohubVideo

def save(self, **kwargs):
"""
Save and return a list of object instances.
"""
validated_data = [
dict(list(attrs.items()) + list(kwargs.items()))
for attrs in self.validated_data
]

if "id" in validated_data:
ModelClass = self.Meta.model

try:
self.instance = ModelClass.objects.get(id=validated_data["id"])
except ModelClass.DoesNotExist:
pass

return super(VideohubVideoSerializer, self).save(**kwargs)

def to_internal_value(self, data):
# Channel info passed as nested object, but we just store integer ID
channel = data.get('channel')
if channel and 'id' in channel:
data['channel_id'] = channel['id']
return super(VideohubVideoSerializer, self).to_internal_value(data)
5 changes: 5 additions & 0 deletions bulbs/videos/templates/videohub_client/video.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<div class="hub-video">
<div class="hub-video-container">
<iframe name="embedded" allowfullscreen webkitallowfullscreen mozallowfullscreen frameborder="no" width="100%" height="auto" scrolling="no" src="{{ video.get_embed_url }}"></iframe>
</div>
</div>
27 changes: 27 additions & 0 deletions example/testcontent/migrations/0010_testvideocontentobj.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('content', '0010_content_instant_article_id'),
('videos', '0001_initial'),
('testcontent', '0009_testcontentobjthree'),
]

operations = [
migrations.CreateModel(
name='TestVideoContentObj',
fields=[
('content_ptr', models.OneToOneField(auto_created=True, serialize=False, primary_key=True, parent_link=True, to='content.Content')),
('videohub_ref', models.ForeignKey(blank=True, to='videos.VideohubVideo', null=True)),
],
options={
'abstract': False,
},
bases=('content.content', models.Model),
),
]
5 changes: 5 additions & 0 deletions example/testcontent/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from bulbs.content.models import Content, Tag, ElasticsearchImageField
from bulbs.reading_list.mixins import ReadingListMixin
from bulbs.recirc.mixins import BaseQueryMixin
from bulbs.videos.mixins import VideoMixin


class TestContentObj(Content):
Expand Down Expand Up @@ -96,3 +97,7 @@ class TestRecircContentObject(Content, BaseQueryMixin):

class Mapping(Content.Mapping):
query = field.Object(enabled=False)


class TestVideoContentObj(Content, VideoMixin):
"""Fake video"""
File renamed without changes.
2 changes: 1 addition & 1 deletion scripts/makemigrations
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#!/bin/sh -e

django-admin makemigrations --settings=example.settings content
django-admin makemigrations --settings=example.settings "$@"
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"beautifulsoup4>=4.4.1",
"celery==3.1.10",
"contextdecorator==0.10.0",
"django-betty-cropper>=0.2.0",
"django-betty-cropper>=0.2.6",
"django-filter==0.9.2",
"django-json-field==0.5.5",
"django-polymorphic==0.7.1",
Expand Down
6 changes: 3 additions & 3 deletions tests/api/test_content_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -616,14 +616,14 @@ def test_search(self):
self.assertEqual(r.status_code, 200)
self.assertEqual(len(r.data["results"]), 1)
# Content, TestContentObj, TestContentObjTwo, TestContentObjThree,
# TestContentDetailImage, TestRecircContentObject
# TestContentDetailImage, TestRecircContentObject, TestVideoContentObj
r = self.api_client.get(url, dict(search="conte"), format="json")
self.assertEqual(r.status_code, 200)
self.assertEqual(len(r.data["results"]), 6)
self.assertEqual(len(r.data["results"]), 7)
# no query gives us all types
r = self.api_client.get(url, format="json")
self.assertEqual(r.status_code, 200)
self.assertEqual(len(r.data["results"]), 8)
self.assertEqual(len(r.data["results"]), 9)


class TestContentResolveAPI(BaseAPITestCase):
Expand Down
Loading

0 comments on commit 4386876

Please sign in to comment.