28
36

More than 5 years have passed since last update.

Djangoで作る自分だけのTwitter クライアント

Last updated at Posted at 2016-10-25

ザックリ作ってみる

bottleのときもそうでしたが、Twitterクライアントを作るところからやるとDjangoの基本的理解が深まるんじゃないかなー。というところで、投稿・リプライ・ホームタイムラインくらいが1画面で見れるようなものを作ります。

スクリーンショット 2016-10-25 13.09.09.png

このようなイメージに仕上がります。

1.APIのリミット表示
2.タイムラインを1つ表示
3.タイムラインのユーザー名または画像クリックでリプライ準備
4.テキストツイートが出来る

といった仕様になります。

同じような条件下で試す場合は前の記事を参考にどうぞ。
http://qiita.com/Gen6/items/735245423b65698428be

中身

.py

myapp/views.py
from requests_oauthlib import OAuth1Session
import time, calendar
import datetime
import json
import re
import os
import requests
import sys, codecs
sys.stdout = codecs.getwriter('utf-8')(sys.stdout)

from django.http.response import HttpResponse
from django.shortcuts import render


def index(request):

    msg = request.GET.get('words')

    C_KEY = '********************'
    C_SECRET = '********************'
    A_KEY = '********************'
    A_SECRET = '********************'

    url = 'https://api.twitter.com/1.1/statuses/update.json'
    params = {'status': msg,'lang': 'ja'}
    tw = OAuth1Session(C_KEY,C_SECRET,A_KEY,A_SECRET)
    req = tw.post(url, params = params)


    url = 'https://api.twitter.com/1.1/statuses/home_timeline.json'
    params = {'count': 1}
    req = tw.get(url, params = params)

    if req.status_code == 200:
        timeline = json.loads(req.text)
        limit = req.headers['x-rate-limit-remaining']

        for tweet in timeline:
            Text = (tweet['text'])
            User = (tweet['user']['screen_name'])
            Name = (tweet['user']['name'])
            Img = (tweet['user']['profile_image_url'])
            Created_at = YmdHMS(tweet['created_at'])

            Message = {
                'Words': msg,
                'timeline': timeline,
                'API_limit': limit,
                'Text': Text,
                'User': User,
                'Name': Name,
                'Img': Img,
                'Created_at': Created_at,
            }

            return render(request, 'index.html', Message)

    else:
        Error = {
            'Error_message': 'API制限中',
        }
        return render(request, 'index.html', Error)


def YmdHMS(created_at):
    time_utc = time.strptime(created_at, '%a %b %d %H:%M:%S +0000 %Y')
    unix_time = calendar.timegm(time_utc)
    time_local = time.localtime(unix_time)
    return int(time.strftime('%Y%m%d%H%M%S', time_local))

*最後の関数は以下でも通ります。

YdmHMS.py
def YmdHMS(created_at):
    time_utc = time.strptime(created_at, '%a %b %d %H:%M:%S +0000 %Y')
    unix_time = calendar.timegm(time_utc)
    time_local = time.localtime(unix_time)
    return str(time.strftime('%Y'+'年'+'%m'+'月'+'%d'+'日'+'%H'+'時'+'%M'+'分', time_local))

HTMLとCSS

templates/base.html
<!DOCTYPE html>
<html lang="la">
  <head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link href="../static/css/bootstrap.min.css" rel="stylesheet">
    <link href="../static/css/custom.css" rel="stylesheet">
    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
    <script src="../static/js/bootstrap.min.js"></script>
    <title></title>
  </head>
  <body>
    {% block body %}
    {% endblock %}
  </body>
</html>

URLべた書きなのがダサいですね。

templates/index.html
{% extends "base.html" %}
{% block body %}
  <div class="container">
    <div class="row">
      <div class="col-md-6">
        <form action="" method="get" class="form-group">
          <label><input type="text" size="60" name="words" class="form-control" placeholder="ツイート"></label>
          <input type="submit" class="btn btn-primary" value="送信">
          <input type="button" class="btn btn-warning" value="リロード" onclick="location.reload();" />
        </form>

        {% if Words %}
        <p>「{{ Words }}」 とツイートしました。</p>
        {% endif %}
        <p class="error-message">{{ Error_message }}</p>
        <p><span class="status">API-Limit:{{ API_limit }}</p>
      </div>
      <script type="text/javascript">
      $(function() {
      $(".reply").click(function() {
        var newText = '@{{ User }} ';
        $(':text[name="words"]').val(newText);
      });
      });
      </script>

      <div class="col-md-6">
        {% if API_limit %}
        <dl>
          <dt><span class="reply"><img class="users-img" src="{{ Img }}">{{ User }}/{{ Name }}</span><span class="d-time">{{ Created_at }}</span></dt>
          <dd>{{ Text }}</dd>
        </dl>
        <ul id="output"></ul>
        {% endif %}
      </div>

    </div>
  </div>

{% endblock %}
static/css/custom.css
.container {
  width: 100%;
}

.row {
  margin: 40px auto;
}

.status {
  background: #E2264D;
  border-radius: 4px;
  color: #fff;
  padding: 4px;
  width: 100px;
  text-align: center;
  margin-right: 10px;
}

.reply {
  cursor: pointer;
  font-weight: bold;
}

.btn-primary {
  margin-right: 2px;
}

.d-time {
  margin-left: 10px;
  border: 1px solid #d8d8d8;
  border-radius: 4px;
  padding: 4px;
  font-weight: normal;
}

ul li {
  list-style: none;
}

.users-img {
  margin-right: 10px;
  border-radius: 4px;
}

dt {
  margin-bottom: 10px;
}

.error-message {
  color: #ff3300;
}

データベースに保存したい場合は次の記事もご覧いただけると幸いです。
http://qiita.com/Gen6/items/907d869cdf1d588a4751

更なるカスタマイズ

スクリーンショット 2016-10-26 13.06.43.png

上記のようなイメージで作成します。

settings.pyを確認する

107行目あたりを書いておきます。

mysite/settings.py
LANGUAGE_CODE = 'ja'

TIME_ZONE = 'Asia/Tokyo'

USE_I18N = True

USE_L10N = True

USE_TZ = True

STATIC_URL = '/static/'

STATICFILES_DIRS = (
    os.path.join(BASE_DIR, "static"),
)

views.pyを書き足す

投稿・メンション・検索を実装するパターンになります。
一つのviewsファイルに書いているのでメンテナンス性は低いですが参考までにどうぞ。

KEYを外部に置いたり、もっとシンプルに書けるわけですがわかりやすく一纏めとしています。
コードをすっきりさせたりするには色々やってみてください。

検索結果はデータベースに保存するパターンとなります。

myapp/views.py
from requests_oauthlib import OAuth1Session
import time, calendar
import datetime
import json
import re
import os
import requests
import sys, codecs
sys.stdout = codecs.getwriter('utf-8')(sys.stdout)

from django.http.response import HttpResponse
from django.shortcuts import render
from myapp.models import Supermodel


def index(request):

    msg = request.GET.get('words')

    C_KEY = '********************'
    C_SECRET = '********************'
    A_KEY = '********************'
    A_SECRET = '********************'

    url = 'https://api.twitter.com/1.1/statuses/update.json'
    params = {'status': msg,'lang': 'ja'}
    tw = OAuth1Session(C_KEY,C_SECRET,A_KEY,A_SECRET)
    req = tw.post(url, params = params)


    url = 'https://api.twitter.com/1.1/statuses/home_timeline.json'
    params = {'count': 1}
    req = tw.get(url, params = params)

    if req.status_code == 200:
        timeline = json.loads(req.text)
        limit = req.headers['x-rate-limit-remaining']

        for tweet in timeline:
            Text = (tweet['text'])
            User = (tweet['user']['screen_name'])
            Name = (tweet['user']['name'])
            Img = (tweet['user']['profile_image_url'])
            Created_at = YmdHMS(tweet['created_at'])

            Message = {
                'Words': msg,
                'timeline': timeline,
                'API_limit': limit,
                'Text': Text,
                'User': User,
                'Name': Name,
                'Img': Img,
                'Created_at': Created_at,
            }

            return render(request, 'index.html', Message)

    else:
        Error = {
            'Error_message': 'API制限中',
        }
        return render(request, 'index.html', Error)


def search(request):

    search_words = 'hogehoge'
    search_words = request.GET.get('words')

    C_KEY = '********************'
    C_SECRET = '********************'
    A_KEY = '********************'
    A_SECRET = '********************'
    tw = OAuth1Session(C_KEY,C_SECRET,A_KEY,A_SECRET)

    url = 'https://api.twitter.com/1.1/search/tweets.json?'
    params = {
                'q': (search_words, 'utf-8'),
                'lang': 'ja',
                'count': '1'
                }
    req = tw.get(url, params = params)

    if req.status_code == 200:
        timeline = json.loads(req.text)
        limit = req.headers['x-rate-limit-remaining']

        for tweet in timeline['statuses']:
            Text = (tweet['text'])
            User = (tweet['user']['screen_name'])
            Name = (tweet['user']['name'])
            Img = (tweet['user']['profile_image_url'])
            Created_at = YmdHMS(tweet['created_at'])


            data = Supermodel()
            data.user_id = User
            data.user_name = Name
            data.user_img = Img
            data.user_text = Text
            data.user_created_at = Created_at
            data.save()

            Message = {
                'Words': search_words,
                'timeline': timeline,
                'API_limit': limit,
                'Text': Text,
                'User': User,
                'Name': Name,
                'Img': Img,
                'Created_at': Created_at,
            }

            return render(request, 'search.html', Message)

    else:
        Error = {
            'Error_message': 'API制限中',
        }
        return render(request, 'search.html', Error)


def mentions(request):

    msg = request.GET.get('words')

    C_KEY = '********************'
    C_SECRET = '********************'
    A_KEY = '********************'
    A_SECRET = '********************'

    url = 'https://api.twitter.com/1.1/statuses/update.json'
    params = {'status': msg,'lang': 'ja'}
    tw = OAuth1Session(C_KEY,C_SECRET,A_KEY,A_SECRET)
    req = tw.post(url, params = params)


    url = 'https://api.twitter.com/1.1/statuses/mentions_timeline.json'
    params = {'count': 1}
    req = tw.get(url, params = params)

    if req.status_code == 200:
        timeline = json.loads(req.text)
        limit = req.headers['x-rate-limit-remaining']

        for tweet in timeline:
            Text = (tweet['text'])
            User = (tweet['user']['screen_name'])
            Name = (tweet['user']['name'])
            Img = (tweet['user']['profile_image_url'])
            Created_at = YmdHMS(tweet['created_at'])

            Message = {
                'Words': msg,
                'timeline': timeline,
                'API_limit': limit,
                'Text': Text,
                'User': User,
                'Name': Name,
                'Img': Img,
                'Created_at': Created_at,
            }

            return render(request, 'mentions.html', Message)

    else:
        Error = {
            'Error_message': 'API制限中',
        }
        return render(request, 'mentions.html', Error)


def YmdHMS(created_at):
    time_utc = time.strptime(created_at, '%a %b %d %H:%M:%S +0000 %Y')
    unix_time = calendar.timegm(time_utc)
    time_local = time.localtime(unix_time)
    return int(time.strftime('%Y%m%d%H%M%S', time_local))

データベースに保存する動作は以下のコードになってます。
シンプルでわかりやすく何をやっているのか把握が簡単だと思います。

data = Supermodel()
data.user_id = User
data.user_name = Name
data.user_img = Img
data.user_text = Text
data.user_created_at = Created_at
data.save()

各種HTMLファイルを書く

bootstrapを使用しています。

templates/base.html
<!DOCTYPE html>
<html lang="la">
  <head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link href="../static/css/bootstrap.min.css" rel="stylesheet">
    <link href="../static/css/custom.css" rel="stylesheet">
    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
    <script src="../static/js/bootstrap.min.js"></script>
    <title></title>
  </head>
  <body>
    {% block body %}
    {% endblock %}
  </body>
</html>
templates/index.html
{% extends "base.html" %}
{% block body %}
  <div class="container">
    <nav class="nav">
      <ul>
        <li><a href="{% url 'index' %}">POST</a></li>
        <li><a href="{% url 'search' %}">SEARCH</a></li>
        <li><a href="{% url 'mentions' %}">MENTIONS</a></li>
      </ul>
    </nav>
    <div class="row">
      <div class="col-md-6">
        <form action="" method="get" class="form-group">
          <label><input type="text" size="60" name="words" class="form-control" placeholder="ツイート"></label>
          <input type="submit" class="btn btn-primary" value="送信">
          <input type="button" class="btn btn-warning" value="リロード" onclick="location.reload();" />
        </form>

        {% if Words %}
        <p>「{{ Words }}」 とツイートしました。</p>
        {% endif %}
        <p class="error-message">{{ Error_message }}</p>
        <p><span class="status">API-Limit:{{ API_limit }}</p>
      </div>
      <script type="text/javascript">
      $(function() {
      $(".reply").click(function() {
        var newText = '@{{ User }} ';
        $(':text[name="words"]').val(newText);
      });
      });
      </script>

      <div class="col-md-6">
        {% if API_limit %}
        <dl>
          <dt><span class="reply"><img class="users-img" src="{{ Img }}">{{ User }}/{{ Name }}</span><span class="d-time">{{ Created_at }}</span></dt>
          <dd>{{ Text }}</dd>
        </dl>
        {% endif %}
      </div>

    </div>
  </div>

{% endblock %}
templates/search.html
{% extends "base.html" %}
{% block body %}
  <div class="container">
    <nav class="nav">
      <ul>
        <li><a href="{% url 'index' %}">POST</a></li>
        <li><a href="{% url 'search' %}">SEARCH</a></li>
        <li><a href="{% url 'mentions' %}">MENTIONS</a></li>
      </ul>
    </nav>
    <div class="row">
      <div class="col-md-6">
        <form action="" method="get" class="form-group">
          <label><input type="text" size="60" name="words" class="form-control" placeholder="検索"></label>
          <input type="submit" class="btn btn-primary" value="送信">
          <input type="button" class="btn btn-warning" value="リロード" onclick="location.reload();" />
        </form>

        {% if Words %}
        <p>「{{ Words }}」 を検索しました。</p>
        {% endif %}
        <p class="error-message">{{ Error_message }}</p>
        <p><span class="status">API-Limit:{{ API_limit }}</p>
      </div>
      <script type="text/javascript">
      $(function() {
      $(".reply").click(function() {
        var newText = '@{{ User }} ';
        $(':text[name="words"]').val(newText);
      });
      });
      </script>

      <div class="col-md-6">
        {% if API_limit %}
        <dl>
          <dt><span class="reply"><img class="users-img" src="{{ Img }}">{{ User }}/{{ Name }}</span><span class="d-time">{{ Created_at }}</span></dt>
          <dd>{{ Text }}</dd>
        </dl>
        {% endif %}
      </div>

    </div>
  </div>

{% endblock %}
templates/mentions.html
{% extends "base.html" %}
{% block body %}
  <div class="container">
    <nav class="nav">
      <ul>
        <li><a href="{% url 'index' %}">POST</a></li>
        <li><a href="{% url 'search' %}">SEARCH</a></li>
        <li><a href="{% url 'mentions' %}">MENTIONS</a></li>
      </ul>
    </nav>
    <div class="row">
      <div class="col-md-6">
        <form action="" method="get" class="form-group">
          <label><input type="text" size="60" name="words" class="form-control" placeholder="ツイート"></label>
          <input type="submit" class="btn btn-primary" value="送信">
          <input type="button" class="btn btn-warning" value="リロード" onclick="location.reload();" />
        </form>

        {% if Words %}
        <p>「{{ Words }}」 とツイートしました。</p>
        {% endif %}
        <p class="error-message">{{ Error_message }}</p>
        <p><span class="status">API-Limit:{{ API_limit }}</p>
      </div>
      <script type="text/javascript">
      $(function() {
      $(".reply").click(function() {
        var newText = '@{{ User }} ';
        $(':text[name="words"]').val(newText);
      });
      });
      </script>

      <div class="col-md-6">
        {% if API_limit %}
        <dl>
          <dt><span class="reply"><img class="users-img" src="{{ Img }}">{{ User }}/{{ Name }}</span><span class="d-time">{{ Created_at }}</span></dt>
          <dd>{{ Text }}</dd>
        </dl>
        {% endif %}
      </div>

    </div>
  </div>

{% endblock %}

CSS

static/css/custom.css
.container {
  width: 100%;
}

.nav {
  background: rgba(0,0,0,0.7);
  color: #fff;
  width: 100%;
  margin: 0 auto;
}

.nav ul li {
  list-style: none;
  margin: 10px auto;
}

.nav ul {
  margin-left: -20px;
}

.nav li {
  float: left;
  margin-right: 10px!important;
}

.nav li a:link, .nav li a:visited {
  color: #fff!important;
}

.row {
  margin: 40px auto;
}

.status {
  background: #E2264D;
  border-radius: 4px;
  color: #fff;
  padding: 4px;
  width: 100px;
  text-align: center;
  margin-right: 10px;
}

.reply {
  cursor: pointer;
  font-weight: bold;
}

.btn-primary {
  margin-right: 2px;
}

.d-time {
  margin-left: 10px;
  border: 1px solid #d8d8d8;
  border-radius: 4px;
  padding: 4px;
  font-weight: normal;
}

ul li {
  list-style: none;
}

.users-img {
  margin-right: 10px;
  border-radius: 4px;
}

dt {
  margin-bottom: 10px;
}

.error-message {
  color: #ff3300;
}

urls.pyを書く

myapp/urls.py
from django.conf.urls import url
from . import views

urlpatterns = [
    url(r'^$', views.index, name='index'),
    url(r'^search/', views.search, name='search'),
    url(r'^mentions/', views.mentions, name='mentions'),
]

models.pyとadmin.pyを書く

ツイート検索結果をデータベースに保存しない場合は以下は必要なしとなりますが、views.pyに記述しているので念のため。SQLITEを使用しますので、基本settings.pyはデフォルトのままとします。

myapp/models.py
from django.db import models


class Supermodel(models.Model):
    user_name = models.CharField(max_length=140)
    user_id = models.CharField(max_length=140)
    user_img = models.CharField(max_length=140)
    user_text = models.TextField(null=True)
    user_created_at = models.CharField(max_length=140)

    def __str__(self):
        return self.user_name
myapp/admin.py
from django.contrib import admin
from myapp.models import Supermodel


class SupermodelAdmin(admin.ModelAdmin):
    list_display = ('id','user_id','user_name','user_img','user_text','user_created_at')

admin.site.register(Supermodel,SupermodelAdmin)

最終的な構成

スクリーンショット 2016-10-26 13.20.58.png

こういう感じになります。

あとはmigrateしてrunserverでどうぞ。

28
36
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
28
36