Fix Bug 965823 - Add a custom Twitter timeline widget to the Student Ambassadors landing page

This commit is contained in:
Kohei Yoshino 2014-03-19 03:03:57 -04:00
Родитель 940b814505
Коммит 98b3a042d3
20 изменённых файлов: 607 добавлений и 3 удалений

3
.gitmodules поставляемый
Просмотреть файл

@ -43,3 +43,6 @@
[submodule "vendor-local/src/raven"]
path = vendor-local/src/raven
url = git://github.com/getsentry/raven-python.git
[submodule "vendor-local/src/tweepy"]
path = vendor-local/src/tweepy
url = https://github.com/tweepy/tweepy.git

Просмотреть файл

@ -280,3 +280,79 @@
{% macro twitter_share_url(url, tweet_text) -%}
{{ 'https://www.twitter.com/intent/tweet?url=%s&text=%s'|format(url|urlencode, tweet_text|urlencode)|e }}
{%- endmacro %}
{% macro twitter_follow_button(account_name, account_id, show_account_id=True) -%}
{%- if show_account_id -%}
{% set label = _('Follow @%s')|format(account_id) %}
{%- else -%}
{% set label = _('Follow') %}
{%- endif -%}
<a href="https://twitter.com/{{ account_id }}" title="{{ _('Follow %s on Twitter')|format(account_name) }}" class="twitter-follow-button">{{ label }}</a>
{%- endmacro %}
{% macro twitter_timeline_widget(title='', heading_level=3, max_length=0) -%}
{% if tweets -%}
<section id="twitter-timeline-widget" itemscope itemtype="http://schema.org/Blog">
<header>
<h{{ heading_level }} itemprop="name">
{%- if title -%}
{{ title }}
{%- else -%}
{{ _('Twitter Timeline of %s')|format(tweets[0].user.name) }}
{%- endif -%}
</h{{ heading_level }}>
<p>{{ twitter_follow_button(tweets[0].user.name, tweets[0].user.screen_name, False) }}</p>
</header>
<div class="tweets">
{% for _tweet in tweets %}
{%- if _tweet.retweeted_status -%}
{% set retweet = true %}
{% set tweet = _tweet.retweeted_status %}
{%- else -%}
{% set retweet = false %}
{% set tweet = _tweet %}
{%- endif -%}
<article itemprop="blogPost" itemscope itemtype="http://schema.org/BlogPosting">
<header>
<h{{ heading_level + 1 }} class="timestamp">
<a href="https://twitter.com/{{ tweet.user.screen_name }}/status/{{ tweet.id }}" class="post">
{{ format_tweet_timestamp(tweet)|safe }}
</a>
</h{{ heading_level + 1 }}>
<div itemprop="author" itemscope itemtype="http://schema.org/Person">
<a href="https://twitter.com/{{ tweet.user.screen_name }}" class="author">
<img src="{{ tweet.user.profile_image_url_https }}" alt="" itemprop="image">
<span itemprop="name">{{ tweet.user.name }}</span>
<span itemprop="alternateName">@{{ tweet.user.screen_name }}</span>
</a>
</div>
</header>
<div>
<p itemprop="articleBody">{{ format_tweet_body(tweet)|safe }}</p>
{% if retweet -%}
<p class="retweet-credit">{{ _('Retweeted by %s')|format('<a href="%s" class="credit">%s</a>'|format('https://twitter.com/'+_tweet.user.screen_name, _tweet.user.name)|safe) }}</p>
{% endif -%}
{% if tweet.entities.media -%}
{% for medium in tweet.entities.media -%}
{% if medium.type == 'photo' -%}
<p class="media"><a href="{{ medium.expanded_url }}" class="image"><img src="{{ medium.media_url_https }}" alt="" itemprop="image"></a></p>
{% endif -%}
{% endfor -%}
{% endif -%}
</div>
<footer>
<ul class="actions">
<li><a class="reply" href="https://twitter.com/intent/tweet?in_reply_to={{ tweet.id }}">{{ _('Reply') }}</a></li>
<li><a class="retweet" href="https://twitter.com/intent/retweet?tweet_id={{ tweet.id }}">{{ _('Retweet') }}</a></li>
<li><a class="favorite" href="https://twitter.com/intent/favorite?tweet_id={{ tweet.id }}">{{ _('Favorite') }}</a></li>
</ul>
</footer>
</article>
{% if max_length > 0 and loop.index == max_length -%}
{% break %}
{% endif -%}
{% endfor %}
</div>
{% endif -%}
</section>
{%- endmacro %}

Просмотреть файл

@ -7,6 +7,8 @@ import feedparser
from django.conf import settings
from django.core.cache import cache
from bedrock.mozorg.util import TwitterAPI
@cronjobs.register
def update_feeds():
@ -15,3 +17,16 @@ def update_feeds():
# Cache for a year (it will be set by the cron job no matter
# what on a set interval)
cache.set('feeds-%s' % name, feed_info, 60 * 60 * 24 * 365)
@cronjobs.register
def update_tweets():
for account in settings.TWITTER_ACCOUNTS:
try:
tweets = TwitterAPI(account).user_timeline(screen_name=account)
except:
tweets = []
# Cache for a year (it will be set by the cron job no matter
# what on a set interval)
cache.set('tweets-%s' % account, tweets, 60 * 60 * 24 * 365)

Просмотреть файл

@ -1,3 +1,4 @@
# flake8: noqa
import download_buttons
import misc
import social_widgets

Просмотреть файл

@ -0,0 +1,85 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
from datetime import datetime
import urllib
import jingo
from lib.l10n_utils.dotlang import _
@jingo.register.function
def format_tweet_body(tweet):
"""
Return a tweet in an HTML format.
@param tweet: A Tweepy Status object retrieved with the Twitter REST API.
See the developer document for details:
https://dev.twitter.com/docs/platform-objects/tweets
"""
text = tweet.text
entities = tweet.entities
# Hashtags (#something)
for hashtags in entities['hashtags']:
hash = hashtags['text']
text = text.replace('#' + hash,
('<a href="https://twitter.com/search?q=%s&amp;src=hash" class="hash">#%s</a>'
% ('%23' + urllib.quote(hash), hash)))
# Mentions (@someone)
for user in entities['user_mentions']:
name = user['screen_name']
text = text.replace('@' + name,
('<a href="https://twitter.com/%s" class="mention">@%s</a>'
% (urllib.quote(name), name)))
# URLs
for url in entities['urls']:
text = text.replace(url['url'],
('<a href="%s" title="%s">%s</a>'
% (url['url'], url['expanded_url'], url['display_url'])))
# Media
if entities.get('media'):
for medium in entities['media']:
text = text.replace(medium['url'],
('<a href="%s" title="%s" class="media">%s</a>'
% (medium['url'], medium['expanded_url'], medium['display_url'])))
return text
@jingo.register.function
def format_tweet_timestamp(tweet):
"""
Return an HTML time element filled with a tweet timestamp.
@param tweet: A Tweepy Status object retrieved with the Twitter REST API.
For a tweet posted within the last 24 hours, the timestamp label should be
a relative format like "20s", "3m" or 5h", otherwise it will be a simple
date like "6 Jun". See the Display Requirements for details:
https://dev.twitter.com/terms/display-requirements
"""
now = datetime.utcnow()
created = tweet.created_at # A datetime object
diff = now - created # A timedelta Object
if diff.days == 0:
if diff.seconds < 60:
label = _('%ds') % diff.seconds
elif diff.seconds < 60 * 60:
label = _('%dm') % round(diff.seconds / 60)
else:
label = _('%dh') % round(diff.seconds / 60 / 60)
else:
label = created.strftime("%-d %b")
full = created.strftime("%Y-%m-%d %H:%M")
return ('<time datetime="%s" title="%s" itemprop="dateCreated">%s '
'<span class="full">(%s)</span></time>'
% (created.isoformat(), full, label, full))

Просмотреть файл

@ -1,3 +1,4 @@
{% from "macros.html" import twitter_timeline_widget with context %}
{% extends "mozorg/base-resp.html" %}
{% block facebook_id %}107747252595375{# facebook.com/Firefox.Student.Ambassadors #}{% endblock %}
@ -90,7 +91,18 @@
</section>
</div>
</div>
{% if tweets %}
<div class="row">
<div class="col-full">
{{ twitter_timeline_widget(title=_('See What Were Up To'), heading_level=2, max_length=6) }}
</div>
</div>
{% endif %}
</div>
{% endblock %}
{% block email_form %}{% endblock %}
{% block js %}
{{ js('contribute-studentambassadors-landing') }}
{% endblock %}

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -0,0 +1,51 @@
# coding: utf-8
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import json
import os.path
from django.test.client import RequestFactory
import tweepy
from bedrock.mozorg.tests import TestCase
from bedrock.mozorg.helpers.social_widgets import * # noqa
TEST_FILES_ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)),
'test_files')
class TestFormatTweet(TestCase):
rf = RequestFactory()
with open(os.path.join(TEST_FILES_ROOT, 'data', 'tweets.json')) as file:
tweets = json.load(file)
# For test, select and parse a tweet containing a hashtag, mention and URL
tweet = tweepy.models.Status.parse(tweepy.api, tweets[5])
def test_format_tweet_body(self):
"""Should return a tweet in an HTML format"""
# Note that … is a non-ASCII character. That's why the UTF-8 encoding is
# specified at the top of the file.
expected = (
u'Want more information about the <a href="https://twitter.com/'
u'mozstudents" class="mention">@mozstudents</a> program? Sign-up '
u'and get a monthly newsletter in your in-box <a href="http://t.co/'
u'0thqsyksC3" title="http://www.mozilla.org/en-US/contribute/'
u'universityambassadors/">mozilla.org/en-US/contribu…</a> <a href='
u'"https://twitter.com/search?q=%23students&amp;src=hash" class='
u'"hash">#students</a>')
self.assertEqual(format_tweet_body(self.tweet), expected)
def test_format_tweet_timestamp(self):
"""Should return a timestamp in an HTML format"""
expected = (
u'<time datetime="2014-01-16T19:28:24" title="2014-01-16 19:28" '
u'itemprop="dateCreated">16 Jan <span class="full">(2014-01-16 '
u'19:28)</span></time>')
self.assertEqual(format_tweet_timestamp(self.tweet), expected)

Просмотреть файл

@ -164,8 +164,9 @@ urlpatterns = patterns('',
name='mozorg.contribute_embed',
kwargs={'template': 'mozorg/contribute-embed.html',
'return_to_form': False}),
page('contribute/studentambassadors',
'mozorg/contribute/studentambassadors/landing.html'),
url('^contribute/studentambassadors/$',
views.contribute_studentambassadors_landing,
name='mozorg.contribute.studentambassadors.landing'),
url('^contribute/studentambassadors/join/$',
views.contribute_studentambassadors_join,
name='mozorg.contribute.studentambassadors.join'),

Просмотреть файл

@ -10,6 +10,7 @@ from django.conf.urls import url
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
import tweepy
import commonware.log
from lib import l10n_utils
from lib.l10n_utils.dotlang import lang_file_has_tag
@ -122,3 +123,17 @@ def get_fb_like_locale(request_locale):
lang = 'en_US'
return lang
def TwitterAPI(account):
"""
Connect to the Twitter REST API using the Tweepy library.
https://dev.twitter.com/docs/api/1.1
http://pythonhosted.org/tweepy/html/
"""
keys = settings.TWITTER_APP_KEYS[account]
auth = tweepy.OAuthHandler(keys['CONSUMER_KEY'], keys['CONSUMER_SECRET'])
auth.set_access_token(keys['ACCESS_TOKEN'], keys['ACCESS_TOKEN_SECRET'])
return tweepy.API(auth)

Просмотреть файл

@ -6,6 +6,7 @@ import re
import json
from django.conf import settings
from django.core.cache import cache
from django.core.context_processors import csrf
from django.http import HttpResponseRedirect
from django.views.decorators.csrf import csrf_exempt, csrf_protect
@ -208,6 +209,13 @@ def plugincheck(request, template='mozorg/plugincheck.html'):
return l10n_utils.render(request, template, data)
@xframe_allow
def contribute_studentambassadors_landing(request):
return l10n_utils.render(request,
'mozorg/contribute/studentambassadors/landing.html',
{'tweets': cache.get('tweets-mozstudents') or []})
@csrf_protect
def contribute_studentambassadors_join(request):
form = ContributeStudentAmbassadorForm(request.POST or None)

Просмотреть файл

@ -148,6 +148,7 @@ MINIFY_BUNDLES = {
'css/mozorg/contribute-page.less',
),
'contribute-studentambassadors-landing': (
'css/base/social-widgets.less',
'css/mozorg/contribute/studentambassadors/landing.less',
),
'contribute-studentambassadors-join': (
@ -444,6 +445,9 @@ MINIFY_BUNDLES = {
'js/mozorg/contribute-form.js',
'js/base/mozilla-input-placeholder.js',
),
'contribute-studentambassadors-landing': (
'js/base/social-widgets.js',
),
'contribute-studentambassadors-join': (
'js/mozorg/contribute-studentambassadors-join.js',
'js/base/mozilla-input-placeholder.js',
@ -815,6 +819,11 @@ FEEDS = {
'mozilla': 'https://blog.mozilla.org/feed/'
}
# Twitter accounts to retrieve tweets with the API
TWITTER_ACCOUNTS = (
'mozstudents',
)
BASKET_URL = 'http://basket.mozilla.com'
# This prefixes /b/ on all URLs generated by `reverse` so that links

Просмотреть файл

@ -28,3 +28,13 @@ GA_ACCOUNT_CODE = ''
SESSION_COOKIE_SECURE = False
USE_GRUNT_LIVERELOAD = False
# Twitter apps' consumer key/secret and access token/secret
TWITTER_APP_KEYS = {
'mozstudents': {
'CONSUMER_KEY': '',
'CONSUMER_SECRET': '',
'ACCESS_TOKEN': '',
'ACCESS_TOKEN_SECRET': ''
},
}

Просмотреть файл

@ -0,0 +1,230 @@
@import "../sandstone/lib.less";
@font-face {
font-family: 'Font Awesome';
src: url('/media/fonts/fontawesome-webfont.eot?#iefix') format('embedded-opentype'),
url('/media/fonts/fontawesome-webfont.woff') format('woff'),
url('/media/fonts/fontawesome-webfont.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
// Twitter Follow button
a.twitter-follow-button {
.inline-block;
border: 1px solid #CCC;
border-radius: 3px;
padding: 0 5px;
color: #333;
#gradient > .vertical(#FFFFFF, #DEDEDE);
font-size: @smallFontSize;
line-height: 1.4;
font-weight: bold;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
white-space: nowrap;
&:hover, &:focus, &:active {
color: #333;
#gradient > .vertical(#F8F8F8, #D9D9D9);
text-decoration: none;
}
&:focus {
border-color: #0089CB;
outline: none;
}
&:active {
background-color: #EFEFEF;
.box-shadow(inset 0 3px 5px rgba(0, 0, 0, .1));
}
&:before {
.inline-block;
color: #55ACEE;
font-size: @baseFontSize;
font-family: 'Font Awesome', sans-serif;
vertical-align: middle;
content: '\0F099\00A0';
}
}
// Twitter timeline widget
#twitter-timeline-widget {
header {
a.twitter-follow-button {
float: right;
margin: -32px 0 0;
}
}
article {
overflow: hidden;
padding: 10px 10px 10px 68px;
line-height: @baseLine;
header {
.timestamp {
float: right;
margin: 0 0 0 10px;
font-size: @smallFontSize;
line-height: @baseLine;
letter-spacing: 0;
a {
color: @textColorTertiary;
}
.full {
.visually-hidden();
}
}
}
[itemprop="author"] {
a {
color: inherit;
&:hover, &:focus, &:active {
text-decoration: none;
[itemprop="name"] {
text-decoration: underline;
}
}
}
img {
float: left;
margin: 0 0 0 -58px;
border-radius: 5px;
width: 48px;
height: 48px;
}
[itemprop="name"] {
font-weight: bold;
}
[itemprop="alternateName"] {
font-size: @smallFontSize;
color: @textColorTertiary;
}
}
div {
p {
margin: 0;
line-height: @baseLine;
}
img {
margin: 5px 0;
width: 100%;
vertical-align: top;
}
.retweet-credit {
font-size: @smallFontSize;
color: @textColorLight;
&:before {
.inline-block;
font-family: 'Font Awesome', sans-serif;
content: '\0F079\00A0';
}
a {
color: inherit;
}
}
}
footer {
overflow: hidden;
.actions {
float: right;
margin: 0;
li {
.inline-block;
margin: 0 0 0 8px;
padding: 0;
font-size: @smallFontSize;
&:first-child {
margin-left: 0;
}
}
a {
color: @textColorTertiary;
&:before {
.inline-block;
font-family: 'Font Awesome', sans-serif;
}
}
.reply:before {
content: '\0F112\00A0';
}
.retweet:before {
content: '\0F079\00A0';
}
.favorite:before {
content: '\0F005\00A0';
}
}
}
}
}
.html-rtl {
#twitter-timeline-widget {
header a.twitter-follow-button,
article header .timestamp,
footer .actions {
float: left;
}
article {
padding: 10px 68px 10px 10px;
header .timestamp {
margin: 0 10px 0 0;
}
[itemprop="author"] img {
float: right;
margin: 0 -58px 0 0;
}
}
footer .actions li {
margin: 0 8px 0 0;
}
}
}
@media only screen and (max-width: @breakMobileLandscape) {
#twitter-timeline-widget {
header a.twitter-follow-button {
float: none;
margin: 0;
}
}
.html-rtl {
#twitter-timeline-widget {
header a.twitter-follow-button {
float: none;
}
}
}
}

Просмотреть файл

@ -98,6 +98,10 @@
.span(6)
}
.col-full {
margin: 0 @gridGutterWidth/2;
}
section {
margin-top: 50px;
}
@ -156,6 +160,16 @@
}
}
#twitter-timeline-widget {
.tweets {
.multi-column(auto, 2, 20px);
article {
.multi-column-avoid-break;
}
}
}
@media only screen and (max-width: @breakDesktop) {
#main-feature header {
h1, p {
@ -215,6 +229,12 @@
#main-content section {
margin-top: 40px;
}
#twitter-timeline-widget {
.tweets {
.multi-column-clear;
}
}
}
@media only screen and (min-width: @breakMobileLandscape) and (max-width: @breakTablet) {

Просмотреть файл

@ -344,9 +344,35 @@
overflow: hidden;
}
// Add chevron character after text (with space)
.trailing-arrow() {
&:after {
content: " »"; /* nbsp raquo */
}
}
.multi-column(@width: auto, @count: auto, @gap: normal) {
-webkit-column-width: @width;
-moz-column-width: @width;
-o-column-width: @width;
column-width: @width;
-webkit-column-count: @count;
-moz-column-count: @count;
-o-column-count: @count;
column-count: @count;
-webkit-column-gap: @gap;
-moz-column-gap: @gap;
-o-column-gap: @gap;
column-gap: @gap;
}
.multi-column-clear {
.multi-column(auto, auto, normal);
}
.multi-column-avoid-break {
page-break-inside: avoid;
-webkit-column-break-inside: avoid;
-moz-column-break-inside: avoid;
-o-column-break-inside: avoid;
column-break-inside: avoid;
}

Просмотреть файл

@ -0,0 +1,38 @@
$(function() {
// GA event tracking
function _track(event, cmd) {
if (event.target.target === '_blank' || event.metaKey || event.ctrlKey) {
// New tab
gaTrack(cmd);
} else {
// Current tab
event.preventDefault();
gaTrack(cmd, function() { window.location.href = event.currentTarget.href; });
}
};
// Twitter Follow button
$('.twitter-follow-button').on('click', function(event) {
_track(event, ['_trackEvent', 'Social Interactions', 'Twitter Follow']);
});
// Twitter timeline widget
$('#twitter-timeline-widget').on('click', 'a', function(event) {
if ($(this).hasClass('twitter-follow-button')) {
return; // Tracking will be done by the function above
}
_track(event, ['_trackEvent', 'Social Interactions', 'Twitter ' + ({
'post': 'Post Link Exit',
'author': 'Author Link Exit',
'credit': 'Retweet Credit Link Exit',
'image': 'Preview Image Exit',
'hash': 'Hashtag Link Exit',
'mention': 'Mention Link Exit',
'media': 'Media Link Exit',
'reply': 'Reply',
'retweet': 'Retweet',
'favorite': 'Favorite',
}[$(this).attr('class')] || 'General Link Exit')]);
});
});

Просмотреть файл

@ -11,6 +11,7 @@ GitPython==0.1.7
-e git://github.com/jsocol/commonware.git#egg=commonware
-e git://github.com/mozilla/nuggets.git#egg=nuggets
-e git://github.com/kurtmckee/feedparser#egg=feedparser
-e git://github.com/tweepy/tweepy.git#egg=tweepy
# Security
-e git://github.com/fwenzel/django-sha2.git#egg=django-sha2

@ -0,0 +1 @@
Subproject commit 3941489457db86710b770bc120409d6947904afa

Просмотреть файл

@ -16,3 +16,4 @@ src/raven
src/requests
src/rna
src/six
src/tweepy