Skip to content

Commit

Permalink
Merge pull request #4 from showwin/add_python
Browse files Browse the repository at this point in the history
add Python implementation
  • Loading branch information
showwin authored May 9, 2018
2 parents d7fe31f + bebf4b0 commit c810a19
Show file tree
Hide file tree
Showing 16 changed files with 505 additions and 10 deletions.
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,10 @@ build/

*unicorn.pid
*mysql.log

__pycache__/
*.py[cod]
*.pyc
*.py.bak
.DS_Store
.python-version
24 changes: 17 additions & 7 deletions Dockerfile_app
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,33 @@ RUN apt-get install -y nginx
COPY admin/ssl/ /etc/nginx/ssl/
COPY admin/config/nginx.conf /etc/nginx/nginx.conf

USER ishocon

# Ruby のインストール
RUN apt-get install -y git ruby-dev libssl-dev libreadline-dev gcc make libmysqlclient-dev && \
RUN sudo apt-get install -y git ruby-dev libssl-dev libreadline-dev gcc make libmysqlclient-dev && \
git clone https://github.com/sstephenson/rbenv.git ~/.rbenv
ENV PATH "$HOME/.rbenv/bin:$PATH"
RUN eval "$(/root/.rbenv/bin/rbenv init -)" && \
RUN PATH="$HOME/.rbenv/bin:$PATH" && \
eval "$(rbenv init -)" && \
git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build && \
/root/.rbenv/bin/rbenv install 2.5.1 && /root/.rbenv/bin/rbenv rehash && /root/.rbenv/bin/rbenv global 2.5.1
rbenv install 2.5.1 && rbenv rehash && rbenv global 2.5.1

USER ishocon
# Python のインストール
RUN sudo apt-get install -y curl
RUN git clone https://github.com/pyenv/pyenv.git ~/.pyenv && \
PYENV_ROOT="$HOME/.pyenv" && PATH="$PYENV_ROOT/bin:$PATH" && \
eval "$(pyenv init -)" && \
pyenv install 3.6.5 && pyenv global 3.6.5 && \
cd && curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py && python get-pip.py && rm get-pip.py

# アプリケーション
RUN mkdir /home/ishocon/data /home/ishocon/webapp
COPY admin/ishocon2.dump.tar.bz2 /home/ishocon/data/ishocon2.dump.tar.bz2
COPY webapp/ /home/ishocon/webapp
COPY admin/config/bashrc ~/.bashrc
COPY admin/config/bashrc /home/ishocon/.bashrc

RUN cd /home/ishocon/webapp/ruby && sudo gem install bundler && bundle install
RUN sudo chown -R ishocon:ishocon ~/
RUN LC_ALL=C.UTF-8 && LANG=C.UTF-8 && cd /home/ishocon/webapp/python && \
/home/ishocon/.pyenv/shims/pip install -r requirements.txt

COPY docker/start_app.sh /docker/start_app.sh

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ ISHOCONとは `Iikanjina SHOwwin CONtest` の略で、[ISUCON](http://isucon.net
* アプリケーションAMI: `ami-fcba6b9d`
* ベンチマーカーAMI: `ami-eb9c4d8a`
* インスタンスタイプ: `c4.large` (アプリ、ベンチ共に)
* 参考実装言語: Ruby
* 参考実装言語: Ruby, Python

* AWSではなく手元で実行したい場合には [Docker を使ってローカルで環境を整える](https://github.com/showwin/ISHOCON2/blob/master/doc/local_manual.md) をご覧ください。

Expand Down
9 changes: 9 additions & 0 deletions admin/config/bashrc
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
export LC_ALL=C.UTF-8
export LANG=C.UTF-8

export PATH="$HOME/.rbenv/bin:$PATH"
eval "$(rbenv init -)"

export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
if command -v pyenv 1>/dev/null 2>&1; then
eval "$(pyenv init -)"
fi

export PATH=$PATH:/usr/local/go/bin
export GOROOT=/usr/local/go
export GOPATH=/home/ishocon/.local/go
Expand Down
10 changes: 10 additions & 0 deletions doc/local_manual.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ $ docker exec -it ishocon2_app_1 /bin/bash

アプリケーションの起動は [マニュアル](https://github.com/showwin/ISHOCON2/blob/master/doc/manual.md) 参照


### Tips

`docker-compose.yml``app` でローカルの `webapp` ディレクトリをマウントすると、ローカルのファイル変更がすぐに反映されるので便利です。
```
app:
volumes:
- ./webapp:/home/ishocon/webapp
```

## ベンチマーカー

```
Expand Down
9 changes: 9 additions & 0 deletions doc/manual.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,21 @@ $ ls
```

### Web サーバーを立ち上げる

#### Ruby の場合

```
$ cd ~/webapp/ruby
$ unicorn -c unicorn_config.rb
```

#### Python の場合

```
$ cd ~/webapp/python
$ uwsgi --ini app.ini
```

これでブラウザからアプリケーションが見れるようになるので、IPアドレスにアクセスしてみましょう。
HTTPS でのみアクセスできることに注意してください。ブラウザによっては証明書のエラーが表示されますが、無視してページを表示してください。

Expand Down
5 changes: 3 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
version: '3.0'
services:
app:
image: showwin/ishocon2_app:0.9
image: showwin/ishocon2_app:0.9.1
command: /docker/start_app.sh
volumes:
- storage_app:/var/lib/mysql
- ./webapp:/home/ishocon/webapp
ports:
- "443:443"

Expand All @@ -14,7 +15,7 @@ services:
- /var/lib/mysql

bench:
image: showwin/ishocon2_bench:0.9
image: showwin/ishocon2_bench:0.9.1
command: /docker/start_bench.sh
volumes:
- storage_bench:/var/lib/mysql
Expand Down
8 changes: 8 additions & 0 deletions webapp/python/app.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[uwsgi]
module = app
callable = app
master = true
processes = 1
http = :8080
vacuum = true
die-on-term = true
221 changes: 221 additions & 0 deletions webapp/python/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import datetime
import html
import os
import pathlib
import urllib

import MySQLdb.cursors

from flask import Flask, abort, redirect, render_template, request, session

static_folder = pathlib.Path(__file__).resolve().parent / 'public'
app = Flask(__name__, static_folder=str(static_folder), static_url_path='')

app.secret_key = os.environ.get('ISHOCON2_SESSION_SECRET', 'showwin_happy')

_config = {
'db_host': os.environ.get('ISHOCON2_DB_HOST', 'localhost'),
'db_port': int(os.environ.get('ISHOCON2_DB_PORT', '3306')),
'db_username': os.environ.get('ISHOCON2_DB_USER', 'ishocon'),
'db_password': os.environ.get('ISHOCON2_DB_PASSWORD', 'ishocon'),
'db_database': os.environ.get('ISHOCON2_DB_NAME', 'ishocon2'),
}


def config(key):
if key in _config:
return _config[key]
else:
raise "config value of %s undefined" % key


def db():
if hasattr(request, 'db'):
return request.db
else:
request.db = MySQLdb.connect(**{
'host': config('db_host'),
'port': config('db_port'),
'user': config('db_username'),
'passwd': config('db_password'),
'db': config('db_database'),
'charset': 'utf8mb4',
'cursorclass': MySQLdb.cursors.DictCursor,
'autocommit': True,
})
cur = request.db.cursor()
cur.execute("SET SESSION sql_mode='TRADITIONAL,NO_AUTO_VALUE_ON_ZERO,ONLY_FULL_GROUP_BY'")
cur.execute('SET NAMES utf8mb4')
return request.db


def get_election_results():
cur = db().cursor()
cur.execute("""
SELECT c.id, c.name, c.political_party, c.sex, v.count
FROM candidates AS c
LEFT OUTER JOIN
(SELECT candidate_id, COUNT(*) AS count
FROM votes
GROUP BY candidate_id) AS v
ON c.id = v.candidate_id
ORDER BY v.count DESC
""")
return cur.fetchall()


def get_voice_of_supporter(candidate_ids):
cur = db().cursor()
candidate_ids_str = ','.join([str(cid) for cid in candidate_ids])
cur.execute("""
SELECT keyword
FROM votes
WHERE candidate_id IN ({})
GROUP BY keyword
ORDER BY COUNT(*) DESC
LIMIT 10
""".format(candidate_ids_str))
records = cur.fetchall()
return [r['keyword'] for r in records]


def get_all_party_name():
cur = db().cursor()
cur.execute('SELECT political_party FROM candidates GROUP BY political_party')
records = cur.fetchall()
return [r['political_party'] for r in records]


def get_candidate_by_id(candidate_id):
cur = db().cursor()
cur.execute('SELECT * FROM candidates WHERE id = {}'.format(candidate_id))
return cur.fetchone()


def db_initialize():
cur = db().cursor()
cur.execute('DELETE FROM votes')


@app.teardown_request
def close_db(exception=None):
if hasattr(request, 'db'):
request.db.close()


@app.route('/')
def get_index():
candidates = []
election_results = get_election_results()
# 上位10人と最下位のみ表示
candidates += election_results[:10]
candidates.append(election_results[-1])

parties_name = get_all_party_name()
parties = {}
for name in parties_name:
parties[name] = 0
for r in election_results:
parties[r['political_party']] += r['count'] or 0
parties = sorted(parties.items(), key=lambda x: x[1], reverse=True)

sex_ratio = {'men': 0, 'women': 0}
for r in election_results:
if r['sex'] == '男':
sex_ratio['men'] += r['count'] or 0
elif r['sex'] == '女':
sex_ratio['women'] += r['count'] or 0

return render_template('index.html',
candidates=candidates,
parties=parties,
sex_ratio=sex_ratio)


@app.route('/candidates/<int:candidate_id>')
def get_candidate(candidate_id):
cur = db().cursor()
cur.execute('SELECT * FROM candidates WHERE id = {}'.format(candidate_id))
candidate = cur.fetchone()
if not candidate:
return redirect('/')

cur.execute('SELECT COUNT(*) AS count FROM votes WHERE candidate_id = {}'.format(candidate_id))
votes = cur.fetchone()['count']
keywords = get_voice_of_supporter([candidate_id])
return render_template('candidate.html',
candidate=candidate,
votes=votes,
keywords=keywords)


@app.route('/political_parties/<string:name>')
def get_political_party(name):
cur = db().cursor()
votes = 0
for r in get_election_results():
if r['political_party'] == name:
votes += r['count'] or 0

cur.execute('SELECT * FROM candidates WHERE political_party = "{}"'.format(name))
candidates = cur.fetchall()
candidate_ids = [c['id'] for c in candidates]
keywords = get_voice_of_supporter(candidate_ids)
return render_template('political_party.html',
political_party=name,
votes=votes,
candidates=candidates,
keywords=keywords)


@app.route('/vote')
def get_vote():
cur = db().cursor()
cur.execute('SELECT * FROM candidates')
candidates = cur.fetchall()
return render_template('vote.html',
candidates=candidates,
message='')


@app.route('/vote', methods=['POST'])
def post_vote():
cur = db().cursor()
cur.execute('SELECT * FROM users WHERE name = "{}" AND address = "{}" AND mynumber = "{}"'.format(
request.form['name'], request.form['address'], request.form['mynumber']
))
user = cur.fetchone()
cur.execute('SELECT * FROM candidates WHERE name = "{}"'.format(request.form['candidate']))
candidate = cur.fetchone()
voted_count = 0
if user:
cur.execute('SELECT COUNT(*) AS count FROM votes WHERE user_id = {}'.format(user['id']))
voted_count = cur.fetchone()['count']

cur.execute('SELECT * FROM candidates')
candidates = cur.fetchall()
if not user:
return render_template('vote.html', candidates=candidates, message='個人情報に誤りがあります')
elif user['votes'] < (int(request.form['vote_count']) + voted_count):
return render_template('vote.html', candidates=candidates, message='投票数が上限を超えています')
elif not request.form['candidate']:
return render_template('vote.html', candidates=candidates, message='候補者を記入してください')
elif not candidate:
return render_template('vote.html', candidates=candidates, message='候補者を正しく記入してください')
elif not request.form['keyword']:
return render_template('vote.html', candidates=candidates, message='投票理由を記入してください')

for _ in range(int(request.form['vote_count'])):
cur.execute('INSERT INTO votes (user_id, candidate_id, keyword) VALUES ({}, {}, "{}")'.format(
user['id'], candidate['id'], request.form['keyword']
))
return render_template('vote.html', candidates=candidates, message='投票に成功しました')


@app.route('/initialize')
def get_initialize():
db_initialize()


if __name__ == "__main__":
app.run()
5 changes: 5 additions & 0 deletions webapp/python/public/css/bootstrap.min.css

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions webapp/python/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
click==6.7
Flask==1.0.2
itsdangerous==0.24
Jinja2==2.10
MarkupSafe==1.0
mysqlclient==1.3.12
uWSGI==2.0.17
Werkzeug==0.14.1
Loading

0 comments on commit c810a19

Please sign in to comment.