Fashion Revolution

Live Wall

www.fashrevwall.com

http://tinyurl.com/hu39c5n

What's Fashion Revolution?

  • Non for profit working on raising awareness of the true cost of fashion, helping people shop with a conscience.

 

http://fashionrevolution.org

  • They challenge the fashion industry to provide greater transparency, safer work places, fair pay.

Fashion Revolution Week

  • Series of events around the world calling for accountability through all steps in the clothes-making process: debates, online discussions, seminars, ...

18-24 April 2016

  • 1,134 people killed and 2,500 injured when a garment factory complex collapsed in Bangladesh in April 2013.

The Challenge

Build a Live Wall Feed with images of clothing labels posted on Twitter with the #FashRevWall hashtag during Fashion Revolution Week... 

... in only two weeks!

The Tech Stack

Front-End stuff

  • HTML/Sass
  • Django Templates
  • Bootstrap
  • Grunt
  • npm

Back-End stuff

  • Django
  • Tweepy
  • Pytest
  • PostgreSQL
  • Heroku

The Team

Projet Manager and Front-End Lead

Back-End Lead

Back-End Developer

Front-End Developer

Lili Kastillo

Raquel Alegre

Mariza Dima

Karen Lee

Twitter Client 

Data storage

 

Front-End

Tweet DB table

Author Date Tweet
lili 2016-04-23 So excited about #FashRevWall
raquel 2016-04-23 #FashRevWall is about to start!

Tweepy queries Twitter API and filters data

Heroku deployment

Twitter Client

"""
Represents a client that connects to Twitter to retrieve tweets based on a
search criteria using Twitter REST API and Tweepy.
"""
import json
import tweepy
from tweepy import OAuthHandler

class TwitterClient:
    def __init__(self):
        self.api = self._get_twitter_api()

    def _get_twitter_api(self):
        """
        Since we are only reading public information from Twitter, we don't need
        access token/secret values.
        """
        with open('secrets.json') as secrets_file:
            secrets = json.load(secrets_file)

        consumer_key = secrets['consumer_key']
        consumer_secret = secrets['consumer_secret']
        access_token = secrets['access_token']
        access_secret = secrets['access_secret']

        self.auth = OAuthHandler(consumer_key, consumer_secret)
        self.auth.set_access_token(access_token, access_secret)

        return tweepy.API(self.auth)

    def get_tweets_by_hashtag(self, hashtag, n):
        """
        Receives a string hashtag and returns the list of last n Tweets
        containing it.
        """
        tweets = []
        results = tweepy.Cursor(self.api.search, q=hashtag).items(n)
        for tweet in results:
            tweets.append(tweet)
        return tweets

    def get_images_by_hashtag(self, hashtag, n):
        """
        Receives a string hashtag and returns the list of last n Tweets
        containing it.
        """
        images = []
        tweets = self.get_tweets_by_hashtag(hashtag, n)
        for tweet in tweets:
            try:
                image_url = tweet.entities['media'][0]['media_url']
            except KeyError:
                print "No media in tweet with ID: {}".format(tweet.id)
                continue
            images.append(image_url)
        return images

Tweepy: Python module for Twitter's API

Django Models and DBs

from django.db import models
from django.utils import timezone


class Tweet(models.Model):
    image_url = models.CharField(max_length=200, unique=True)
    user = models.CharField(max_length=100)
    created_at = models.DateTimeField(default=timezone.now)

The tweet model maps to the tweet PostgreSQL table

Front-End

@import "bootstrap-custom";

$max-width: 63em; 
$max-text-width: 45em;
$font-size-base: 16px;
$font-size-text: 1.2em;

@font-face {
    font-family: "Kelson Sans";
    src: url("kelson-font/Kelson_Sans_Regular.otf") format("opentype");
}
@font-face {
    font-family: "Kelson Sans";
    font-weight: bold;
    src: url("kelson-font/Kelson_Sans_Bold.otf") format("opentype");
}

body {
    background: #ef5b58;
    font-family: "Kelson Sans"; 
    font-size: $font-size-base;
} 
h1 {
    font-family: "Kelson Sans";
    font-size: 3em;
    line-height: 1.5em;
    text-decoration: underline;;
}

a {
    padding: 0;
    margin: 0;
}
.container {
    max-width: $max-text-width;
    padding: 2em;
    font-size: $font-size-text;
    line-height: $font-size-text*1.4;
}

.color-container {
    background-color: #ef5b58;
    padding: 4em 0;
    p {
        text-align: center;

    }
}
.twitter-image {
    width: 17em;
    height: 17em;
    display: block;
    background-repeat: no-repeat;
    background-position: center; 
    background-size: cover;
    border: 1em solid #fcfcfc;  
    border-bottom-width: 0;
    box-shadow: 1px 1px 1px 1px rgba(0,0,0,.1) inset;
}

.tweet-list-container {
    width: auto;
    margin: 0 auto;
}
.tweet-container {
    box-shadow:4px 4px 0px rgba(0,0,0,.1);  
}

.tweet-container:hover {
    transform: scale(1.01);
}
#twitter-images-list {
    list-style: none;
    margin: 0 auto;
    display: block;
    padding: 3em 0;
    display: flex;
    max-width: $max-width;
    flex-direction: row;
    flex-wrap: wrap;
}

#twitter-images-list li {
    margin: 1.5em auto;
    display:inline-block; 

}

.tweet-text {
    background:#fcfcfc;
    line-height:1.3em;
    text-align: center;
    padding: 1em;
    font-weight: bold;
    padding:0.5em 0;
    border: .7em solid #fcfcfc;  
    display:block;
    font-weight:bold;
    letter-spacing:0.1em;
    text-transform:uppercase;
    margin: 0 0 1em 0;
    color:#181818;
    font-family: "Kelson Sans";
}

#top-bar {
    height: 200px;
}
#top-bar #logo-wrapper {
    background: url('../img/FashRev_pattern.png');
    height: 200px;
    padding: 0;
}
#top-bar #logo {
    height: 200px;
    width: 200px;
    margin: 0 auto;
    display: block;
    position: relative;
    top: 40px;
}
.white-container {
    background: #ffffff;
    padding-top: 40px;
    padding-bottom: 40px;
}
.hashtag {
    font-weight: bold;
    font-size: 25px;
}
a {
    font-weight: bold;
    color: #000000;
    font-style:italic;
}
.video-wrapper {
    position: relative;
    padding-bottom: 56.25%; /* 16:9 */
    padding-top: 25px;
    height: 0; 
}

.video-wrapper iframe {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
}

We used bootstrap for the grid and coded the rest

 

Deployment

Django + Heroku = Dream Team

Performance Optimization

 80 out of 100 is not bad at all, but there is more we could have done.
 

What we did

  • Minified CSS, HTML & JavaScript
  • Lazyloading the images

What we could also do:

  • Host the images and optimise them
  • Use a CDN
  • Moar caching
  • Serve the HTML gzipped

Team Work

  • Slack
  • Face to face meetings/hacking sessions
  • GitHub issues
  • Code reviews

 

 

 

Demo!

www.fashrevwall.com

Summary

Things we liked:

  • We learned stuff
  • Fashion Revolution founder was very involved
  • Contributed to a good cause
  • It was very exciting to see the web go live
  • We met new people to work with
  • We had fun!

 

 

Things we didn't like so much:

  • Time was very tight!
  • Not as many users as expected
  • Not so much publicity about our work to get more pictures on the wall
  • Heroku constraints (more $$$ = more freedom)
Made with Slides.com