Help us understand the problem. What is going on with this article?

Django初心者が5か月かけてWEBサービスを開発した過程

More than 1 year has passed since last update.

はじめに

先日、PythonとDjangoで開発した自社用の営業支援システム(SFA : Sales Force Automation)をオープンソースとして公開しました。
Djangoで開発するのは初めてでしたが、どうにか5か月で社内リリースすることができました。
この記事では要件定義からソースの公開までの過程を書いていきます。

※タイトルの「Django初心者」というのは本当ですが、エンジニアとしてはわりとベテランです。初心者向けの内容ではありません。

ランディングページ
https://free-sfa.tk/

ソースコード
https://github.com/sikkimtemi/FreeSFA

デモサイト
https://free-sfa-demo.herokuapp.com/

Heroku上で動かす手順
https://qiita.com/sikkim/items/5bb30abc44e5ac7676f6

背景

弊社はアスクルの代理店で、30名弱のスタッフが東京と鹿児島の拠点で働いています。
私は普段、鹿児島のシステム部門で社内向けのシステムを開発したりメンテナンスしたりしています。
社内では「システムの人」と呼ばれています。1

「システムの人」は2018年12月現在3名です。

  • 上司(副社長):元自衛官。FileMakerで社内システムを作った人。理系だが職業プログラマではない。
  • 同僚(@kokikajiya):元組み込み系エンジニア。社歴は私より2、3年長い。
  • 私:元SIer。サーバーサイドが比較的得意。2016年に中途入社。

既存の社内システム

弊社には10年以上前から社員が自分たちの手で作ってきたFileMakerの社内システムが存在します。
画面数もテーブル数も把握しきれないほど多い、巨大で複雑なシステムです。
自分たちで作っただけあって、社内の人にとっては使いやすいシステムになっているようですが、私のように外部からやってきた開発者にとってはメンテナンス性が良いとはいえません。
メンテナンス性が良くないのは、プログラミングを専業とする人が作ったシステムではないということもありますが、FileMakerの開発者向け機能が信じられないほど貧弱という部分が大きいです。2

四半期ごとの中規模開発

「システムの人」の普段の作業は細々としたメンテナンスですが、四半期ごとにテーマを決めて、そこそこ規模の大きい開発をする方針になっています。この方針に従って、これまでに与信システムの改修や、販売管理システムの刷新などを行ってきました。
2018年春頃に次の開発対象を決めるミーティングを行いましたが、このところ鹿児島のメンバーを対象ユーザーとするシステムの開発が続いていたので、次は東京のメンバーを対象にしようということになりました。
東京は営業職が多いため、営業支援システムを改修することに決まりました。

要件定義

既存の営業支援システム(下図)もFileMakerで作られています。
まずは対象ユーザーとなる東京の営業メンバーにテレビ会議でヒアリングを行いました。
image.png

対象ユーザー

  • 外回りの営業職: 3名
  • 管理職:1名3
  • 営業サポート職:2名

今回のプロジェクトのメインターゲットは外回りの営業職3名です。

既存システムの課題

ヒアリングした結果、既存システムには以下の課題があることがわかりました。

  • 外出先で入力できないので、紙にメモして会社に戻ってきてから訪問履歴を入力している。4
  • 自分用の顧客リストを作ろうとしてデータをインポートしたら、上司から怒られたのでそれ以来Excelで二重管理している。5
  • 手動で作成したマイマップを見ながら既存顧客の挨拶回りをしていたら、同じビルに入っている別の顧客を訪問し忘れた。

次期営業支援システムではこれらの状況を改善するため、以下の要件を開発の軸にすることにしました。

  • 外出先でも入力できるようにiPadからの入力を可能にする。
  • 顧客情報の公開範囲を柔軟に設定できるようにし、自分用の顧客情報とチームで共有する顧客情報を分けられるようにする。
  • 訪問予定リストと地図を連動し、訪問履歴の入力状況に応じて地図上のマーカーが変化するようにする。

開発手段の選定

大まかな要件は決まったので、次は開発手段の選定を行いました。
最初はFileMakerをiOSで動かせるFileMaker Goを使用することも検討しました。
工数のことだけを考えたらこれが最良の選択でしたが、将来ずっとFileMakerで開発していくことを想像したらうんざりしたので、もっと開発しやすい環境を模索することにしました。

WebアプリにするかGooogle Apps Script(GAS)ベースのシステムにするかはかなり迷いましたが、Google Apps Scriptの制限6がボトルネックになりそうだったのでWebアプリにすることにしました。GAS自体は別のシステムではいろいろ活用しています。

次は言語とフレームワークですが、言語は人気があって機械学習に強いPythonにしました。今回のプロジェクトは機械学習と無関係ですが、次あたりは機械学習に挑戦したいと思っています。
PythonのフレームワークはDjangoとFlaskが人気ですが、既存システムの規模が大きいので、規模が大きくなっても大丈夫そうなDjangoにしました。
こうして、Python + Djangoで開発することに決まりました。

Djangoは初挑戦でしたが、初挑戦ついでに今まで興味があっても手を出せなかったものにいろいろ手を出してみようと思い、CircleCIやGoogle Maps APIなども今回初めて動かしてみました。

ちなみにこの時点のプロジェクト名は「次期営業支援システムの開発」でした。

Scrumで開発

それまでは開発者個人が行き当たりばったりに開発を行っていましたが、次期営業支援システムの開発はチームで計画を立てて行わないと実現できそうになかったので、まずは開発体制を整えるところからはじめました。
開発手法はScrumです。
Scrum経験者は私だけだったので、事前に上司と同僚に以下の資料を読んでもらい、「Scrumとはなにか」というところから説明しました。

塹壕より Scrum と XP
https://www.infoq.com/jp/minibooks/scrum-xp-from-the-trenches

エンジニアリング組織論への招待
http://gihyo.jp/book/2018/978-4-7741-9605-3

「塹壕より Scrum と XP」は無料でPDFをダウンロードできます。
「エンジニアリング組織論への招待」はQiitaでお馴染みの @hirokidaichi さんの著書ですね。

開発体制

プロダクトオーナー: 上司
スクラムマスター: 私(開発者と兼務)
開発チーム: 同僚、私

プロダクトオーナーは発注者側の責任者です。
ユーザーの代弁者となって判断を下す重要な役割です。
SIの現場ではしばしば開発チームと対立したりしますが、今回は社内システムなので開発チームと一体化しています。

スクラムマスターはプロダクトオーナーと開発チームの間に立って、開発を円滑に進めるために努力する役割を担います。
本来は兼務すべきではありませんが、人が少ないので私が兼務しました。

開発チームは5人くらいが理想といわれていますが、人がいないので仕方ありません。

スプリント

Scrumではスプリントという単位で開発とリリースを繰り返し行います。
今回は2週間を1スプリントとし、12スプリントで完成させることを目標にしました。
実際には10スプリント目で社内リリースすることができ、12スプリント目でGitHub上で公開することができました。

標準的なスプリントは以下のようになります。

スプリント1日目

スプリント1日目はスプリントの準備を行います。通常は開発を行いません。
後述するプロダクトバックログから、そのスプリントで実装するストーリーを決めます。
ストーリーをタスクに分割し、工数を見積もってバーンダウンチャートを作成します。

スプリント2〜8日目

開発を行う期間です。1日あたりの開発時間は6時間です。
開発者は2人ですが、今回は完全ペアプログラミングにしたので開発時間は1人のときと同じです。
したがって、1スプリントで開発可能な時間(ポテンシャル)は6時間かける7日間で42時間となります。

スプリント9日目

スプリントレビューを行います。
本来は1日かけてプロダクトオーナーが成果物をチェックし、プロダクトバックログをAcceptするかRejectするか判定する日です。
弊社ではほとんどスプリントレビューは行われなかったので、この日は開発以外の雑用に当てていました。

スプリント最終日

振り返りと次のスプリントの準備を行います。
振り返りではKeep(継続すること)、Problem(問題点)、Try(改善すること)を分析して次のスプリントに活かします。

プロダクトバックログ

プロダクトバックログにはそのプロダクトで実現したいことをすべて記載します。
Googleスプレッドシートで作成しました。
本当はプロダクトオーナーが記述するものですが、初めてだったので私が代わりに書きました。
下図は10スプリント目が終わったときの様子です。

image.png

ステータスはToDo, Doing, Done, Accept, Reject, Pendingを設定可能にしています。

ストーリーとストーリーポイント

ストーリーには実現したい機能をざっくり書きます。
ストーリーポイントは機能を実装するために必要な工数を相対的に見積もった値を入れます。
今回は「顧客情報の削除機能」のストーリーポイントを2として、これを基準に相対見積もりを行いました。

相対見積もりはプランニングポーカーで行いました。
1, 2, 3, 5, 8, 13の数字が書かれたカード(トランプを使いました)を使い、開発者全員で自分の考えるストーリーポイントを一斉に提示します。
数字が異なったらそれぞれの根拠を述べ合い、ポイントが同じになるまで繰り返します。
2人しかいないのでストーリーポイントの見積もりは毎回あっさりと終わりました。

タスク分割とバーンダウンチャート

ストーリーを実際のコードに落とし込むためのタスクを時間で見積もるのがタスク分割です。
ストーリーポイントの見積もりが相対値だったのに対し、タスク分割の見積もりは絶対値です。
1タスクが3時間以下になるように分割すると比較的精度良く見積もることができます。
普通は付箋に手書きしますが、私は書くのが面倒なのでWeb上のカンバンツールを使いました。
弊社ではJootoを使いましたが、情報共有ができれば何でもよいと思います。

タスク分割が終わったらバーンダウンチャートを作成します。
見積もった時間をすべて足し合わせて0日目にプロットし、毎日更新していきます。
下図はバーンダウンチャートの実例です。

image.png

これはほぼ理想的にタスクを消化できた例で、1日前倒しで開発が終わったので、最終日にストーリーをひとつ追加しました。

ペアプログラミング

開発者がペアを組み、ナビゲーターとドライバーに別れ、ナビゲーターの指示に従ってドライバーが作業を行うのがペアプログラミングです。

ペアプログラミングをすると以下のようなメリットがあります。

  • ドライバーは目の前の作業に集中するので効率が上がります。
  • ナビゲーターはドライバーが手を動かしている間にいろいろなことを考えることができるので品質が向上します。
  • コードレビューが不要になります。
  • チームメンバーのスキルに差がある場合、教育を兼ねることができます。

Scrumでは必ずしもペアプログラミングする必要はありませんが、同僚のWebアプリ開発経験が少なかったので、今回は完全ペアプログラミングにしました。
私は一人でプログラミングしていると、いろいろ考えすぎて手が止まる傾向があるので、ペアプログラミングすることで効率が上がりました。

ナビゲーター役とドライバー役は30分たったら交代します。
ペアで作業するといっても、作業自体はそれぞれ自分のマシンで行いたかったので、AWS Cloud9上で開発環境を共有できる仕組みを構築しました。7
環境構築の方法は以下の記事に載せています。

AWS Cloud9上でPython3 + Django + MySQL環境を構築する手順
https://qiita.com/sikkim/items/bb9ee5ef747660f84774

ベロシティ

ひとつのスプリントが終わったら、ステータスがAcceptになったストーリーのストーリーポイントを数え、合計します。
これをベロシティといいます。
1、2回ではよくわかりませんが、何回かスプリントを終えると、ベロシティの傾向がなんとなく見えてきます。
下図は今回のプロジェクトの実際のベロシティです。

image.png

最初のうちはDjangoに不慣れだったのが、後半になるほど開発効率が上がっていくのがグラフに表れています。

本プロジェクト全体のストーリーポイントは258で、現在の開発チームのベロシティはおよそ30前後なので、同じくらいの規模のプロジェクトを開発したら、今度は9スプリント程度で開発できるだろうと予測できます。
この見積もりができるのがScrumの強みだと思います。

継続的インテグレーション

継続的インテグレーションは、ビルドとテストを自動化し、リリースを安全かつ円滑に行う仕組みです。
今回のプロジェクトではBitbucketもしくはGitHubにpushしたら、CircleCIでビルドとテストを実行し、テストがOKならHerokuにリリースするようにしています。

下記のようなconfig.ymlを用意し、CircleCI側の環境変数にHerokuのApp名とAPIキーを設定しておきます。

.circleci/config.yml
# Python CircleCI 2.0 configuration file
#
# Check https://circleci.com/docs/2.0/language-python/ for more details
#
version: 2
jobs:
  build:
    docker:
      # specify the version you desire here
      # use `-browsers` prefix for selenium tests, e.g. `3.6.1-browsers`
      - image: circleci/python:3.6.5
        DATABASE_URL: postgresql://root@localhost/circle_test?sslmode=disable
      # Specify service dependencies here if necessary
      # CircleCI maintains a library of pre-built images
      # documented at https://circleci.com/docs/2.0/circleci-images/
      - image: circleci/postgres:9.6.2
        environment:
          POSTGRES_USER: root
          POSTGRES_DB: circle_test
    working_directory: ~/repo

    steps:
      - checkout

      # Download and cache dependencies
      - restore_cache:
          keys:
          - v1-dependencies-{{ checksum "requirements.txt" }}
          # fallback to using the latest cache if no exact match is found
          - v1-dependencies-

      - run:
          name: install dependencies
          command: |
            python3 -m venv venv
            . venv/bin/activate
            pip install -r requirements.txt

      - save_cache:
          paths:
            - ./venv
          key: v1-dependencies-{{ checksum "requirements.txt" }}

      # run tests!
      # this example uses Django's built-in test-runner
      # other common Python testing frameworks include pytest and nose
      # https://pytest.org
      # https://nose.readthedocs.io
      - run:
          name: run tests
          command: |
            . venv/bin/activate
            python3 manage.py test

      - store_artifacts:
          path: test-reports
          destination: test-reports
  deploy_staging:
    docker:
      - image: buildpack-deps:trusty
    steps:
      - checkout
      - run:
          name: Deploy Devel to Heroku Staging
          command: |
            git push https://heroku:$HEROKU_API_KEY@git.heroku.com/$HEROKU_APP_NAME_STAGING.git devel:master --force
  deploy:
    docker:
      - image: buildpack-deps:trusty
    steps:
      - checkout
      - run:
          name: Deploy Master to Heroku 
          command: |
            git push https://heroku:$HEROKU_API_KEY@git.heroku.com/$HEROKU_APP_NAME.git master --force


workflows:
  version: 2
  build-deploy:
    jobs:
      - build
      - deploy_staging:
          requires:
            - build
          filters:
            branches:
              only: devel
      - deploy:
          requires:
            - build
          filters:
            branches:
              only: master

ソースコードを修正したら、下記のようにgitでcommitpushを行います。

$ git commit -m "コミットコメント"
$ git push origin devel

すると自動でテストが行われ、テストがOKならステージング環境へのリリースまで行われます。

ステージング環境で動作を確認し、問題なければGitHubやBitbucket上でプルリクエストを作成します。
develブランチをmasterブランチにマージすると、自動でテストが行われ、本番環境へリリースされます。

要件の実装

最初の要件を再度確認します。

  • 外出先でも入力できるようにiPadからの入力を可能にする。
  • 顧客情報の公開範囲を柔軟に設定できるようにし、自分用の顧客情報とチームで共有する顧客情報を分けられるようにする。
  • 訪問予定リストと地図を連動し、訪問履歴の入力状況に応じて地図上のマーカーが変化するようにする。

これらを実現するために、以下のような作業を行いました。

iPadからの入力を可能にする

iPadでも入力できるようにするには、レスポンシブデザインにすればよいので、基本的にBootstrapを利用しました。
私はデザインの才能がないので、普通に作るととても野暮ったい画面になってしまいます。
そこでフリーのテンプレートを利用させてもらいました。

https://github.com/coreui/coreui-free-bootstrap-admin-template

このテンプレートを_base.htmlに適用して作ったのが以下の画面です。

image.png

レスポンシブデザインなので、iPadサイズまで縮小するとこうなります。

image.png

これでiPadからでも入力できるようになりました。
スマートフォンからでも入力できないことはありませんが、かなり読みにくくなるので実用上はタブレットサイズ以上の端末が必要になると思います。

顧客情報の公開範囲を柔軟に設定できるようする

この実装はかなり悩みましたが、Slackのユーザー管理を参考にして、最終的に以下のようにしました。

  • ユーザーは必ずひとつのワークスペースに所属する。
  • ユーザーはワークスペース内に作られた複数のグループに所属することができる。(任意)
  • あるワークスペース内で作られた顧客情報は別のワークスペースに所属するユーザーからは見えない。
  • ユーザーは顧客情報の公開範囲を以下のように設定できる。
    • 自分だけに見える
    • 特定のユーザーを指定して共有
    • 特定のグループ内で共有
    • 自分が所属するワークスペースの全員と共有
  • 顧客情報の共有時は閲覧権限と編集権限を設定できる。

グループはDjango標準のGroupではなくMyGroupというクラスを新たに追加しています。
下記はログインユーザーが参照可能なすべての顧客情報のリストを表示するViewの一部です。

    def get_queryset(self):
        """
        以下の条件に合致する顧客情報が処理の対象となる
         AND条件
         ・削除フラグが立っていない
         ・同一ワークスペース
         OR条件
         ・ワークスペースの公開ステータスが閲覧可能
         ・ワークスペースの公開ステータスが編集可能
         ・作成者が自分
         ・編集可能ユーザーが自分
         ・参照可能ユーザーが自分
         ・編集可能グループが自分が所属するグループと一致
         ・参照可能グループが自分が所属するグループと一致
        """
        return CustomerInfo.objects.filter(
            workspace=self.request.user.workspace, delete_flg='False').filter(
                Q(public_status='1')
                | Q(public_status='2')
                | Q(author=self.request.user.email)
                | Q(shared_edit_user=self.request.user)
                | Q(shared_view_user=self.request.user)
                | Q(shared_edit_group__in=self.request.user.my_group.all())
                | Q(shared_view_group__in=self.request.user.my_group.all())
            ).distinct().order_by('-created_timestamp')

複雑になってしまいましたが、このようなfilterを設定することで顧客情報の公開範囲を制御することができます。
このあたりの機能はまだDjangoに慣れていなかった最初期に作成したので、今見ると改良の余地がいろいろあります。
ただ、ここをいじると影響が大きすぎるので、直す勇気が出ないところです。

訪問予定リストと地図の連動

顧客情報には緯度と経度を持たせ、GoogleMapを表示する際に利用しています。
住所を直接用いないのは、住所だとまれに正しくない場所にマーカーが表示されることがあるためです。
地図の表示を緯度と経度で行うようにしておけば、住所を変えずにマーカーの位置を調整することが可能です。

緯度と経度を手で入力するのは面倒なので、顧客情報の新規作成時や更新時にGoogle Geocoding APIを用いて、住所から変換しています。

訪問予定リスト画面では、この緯度経度情報とMaps Javascript APIを組み合わせて地図を動的に表示しています。
訪問ステータス(未訪問、訪問済み)に合わせて、地図上のマーカーの色も変わるようにしています。

image.png

実装箇所は下記の通りで、DjangoのViewからTemplateに顧客情報を渡し、Javascriptで地図を生成するだけなので、比較的容易に実現することができました。

sfa/view.pyの一部
class VisitTargetMapView(VisitTargetFilterView):
    """訪問先リスト地図画面"""
    template_name = 'sfa/visit_target_map.html'

    def get_context_data(self, **kwargs):
        """
        地図の表示に必要な情報を生成する
        """
        ctx = super().get_context_data(**kwargs)
        contactinfo_list = ctx['contactinfo_list']

        markers = []

        for contactinfo in contactinfo_list:
            target = contactinfo.target_customer
            if not (target.latitude or target.longitude):
                continue
            marker = {
                'name': target.customer_name,
                'address': target.address1 + target.address2 + target.address3,
                'lat': target.latitude,
                'lng': target.longitude,
                'visited': contactinfo.visited_flg
            }
            markers.append(marker)
        ctx['markers'] = json.dumps(
            markers, ensure_ascii=False, default=DecimalDefaultProc)
        return ctx
visit_target_map.html
{% load static %}
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="initial-scale=1.0">
    <meta charset="utf-8">
    <title>FreeSFA Map</title>
    <style>
      /* Always set the map height explicitly to define the size of the div
       * element that contains the map. */
      #map {
        height: 100%;
      }
      /* Optional: Makes the sample page fill the window. */
      html, body {
        height: 100%;
        margin: 0;
        padding: 0;
      }
    </style>
  </head>
  <body>
    <div id="map"></div>
    <script>
      var markers = {{ markers|safe }};
      var map;
      function initMap() {
        // The location of Uluru
        // The map, centered at Uluru
        map = new google.maps.Map(document.getElementById('map'), {
          zoom: 16, 
          scaleControl: true ,
          center: {lat:markers[0].lat, lng:markers[0].lng}
        });
        var pinImage = new google.maps.MarkerImage('http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=|FFC107|');
        var pinImage_visited = new google.maps.MarkerImage('http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=|4DBD74|');
        markers.forEach(function(val, index, arr) {
          if (val.visited) {
            var myImage = pinImage_visited;
          } else {
            var myImage = pinImage;
          }
          var marker = new google.maps.Marker({
            position: {lat:val.lat, lng:val.lng},
            map: map,
            icon: myImage
          });
          google.maps.event.addListener(marker, 'click', function (event) {
            new google.maps.InfoWindow({
              content: '<p><strong>' + val.name + '</strong></p>' + '<p>' + val.address + '</p>'
            }).open(marker.getMap(), marker);
          });
        });
      }
    </script>
    <script async defer src="https://maps.googleapis.com/maps/api/js?key={{api_key}}&callback=initMap"></script>
  </body>
</html>

GoogleのMaps APIは従量課金でお金がかかるので、デモサイトを公開するときにどうしようか悩みました。8
最終的にMaps APIのAPIキーはsettings.pyではなく、ワークスペース毎に管理するDB内に格納することにしました。
デモサイトは特に機能制限はかけていませんが、動的に地図を表示する機能を試したい場合は、ユーザー登録後に自分でワークスペースを作って、下図のワークスペース環境設定画面でAPIキーを登録する必要があります。
デモサイトの敷居が高くなってしまったのは残念です。

image.png

外部サービスとの連携

IP電話との連携

弊社はBasixのIP電話サービスを利用しています。
上記の環境設定画面で「IP電話呼び出し用URL」にBasix用のURLを入力すると、画面上でクリックするだけで手元のビジネスホンから顧客に電話をかけることができます。
image.png

実装上は環境設定画面で入力されたURLの後ろに電話番号を結合して呼び出しているだけなので、Basix以外のサービスでも使うことができます。
一番簡単なのは「IP電話呼び出し用URL」にtel:と入力することで、そうするとブラウザの実行環境に応じた方法で電話をかけようとします。
私が試した範囲では、WindowsではSkypeが、MacではFaceTimeがそれぞれ起動しました。
スマートフォンでは標準の電話アプリが起動して電話がかかります。

既存システムとの連携

社内で公開して最初にもらったフィードバックのひとつに「既存システムのデータが見たい」というものがありました。
顧客情報のインポート機能は作りましたが、コンタクト情報のインポート機能は作らなかったので、過去にお客様とどんなやり取りをしたのか分からないのは困るということでした。

過去のコンタクト情報を全部インポートするのは面倒だったので、既存システムを呼び出せるようにしました。

まずFileMaker上で下記のようなスクリプトを作成します。「案件CD」という一意のコードを利用して顧客情報を表示するスクリプトです。案件CDはDjangoの営業支援システムに既存の顧客情報をインポートする際に「任意コード3」というカラムに取り込んでいます。
image.png

「外部連携用URL3」に以下のように入力します。

fmp://[ユーザーID]:[パスワード]@[FileMaker ServerのIPアドレス]/[ファイル名]?script=[スクリプト名]&param=

これで顧客情報一覧のアイコン(一応FileMaker風にしてみました)をクリックすると対応する顧客情報が既存システムで開くようになりました。
image.png

image.png

社内にいるときしか使えない機能ですが、これで課題はほぼ解決しました。

命名

名前が決まったのはスプリント8か9くらいの頃です。
オープンソースにする方針が固まった頃で、3、4個の案の中からFreeSFAに決まりました。
ちなみにOpenSFAという製品はすでに存在します。
営業活動をスムーズにするという意味を含んだ、Smoothie-SFA(スムージーSFA)という命名案も気に入っていたのですが、Smoothieというソフトウェア製品はいくつか存在したので見送りました。

命名は下記の資料を参考にしながら考えました。

それだ!感のあるネーミングのつくりかた(ver 2.0)
https://speakerdeck.com/kakukoki/soreda-gan-falsearunemingufalsetukurikata-ver-2-dot-0

「FreeSFA」という名前は直截的で覚えやすいので悪くないとは思っていますが、個人的には「それだ!」感をあまり感じないんですよね。

GitHubでソースコードを公開

ソースコードは当初Bitbucketのプライベートリポジトリで管理していました。
公開する方針が決まってから、2スプリントかけてソースコードを精査し、秘密にすべき情報を注意深く取り除いた上で、GitHubの公開リポジトリに載せ替えました。

ちなみに弊社の東京事業所はGitHubの日本法人と同じビルに入っています。9
interman.jpg

苦労したこと

基本的に開発は楽しかったのであまり苦労はしていません。
下記は、強いていえばという程度です。

日本語の情報が少ない

開発を始める前は、PythonもDjangoも人気だから、ちょっと検索すれば簡単に情報は見つかるだろうと思っていました。
実際にはちょっと複雑な実装になると、例が全然見つからなくて困りました。
特に日本語の情報は本当に少ないと感じました。
それから古い情報は見つかるけど、Django2.0以降だとどう書いたらよいのかわからないことも多々ありました。
検索してもわからない場合は、GitHubでDjangoのソースコードを読むと大抵は解決しました。

テストが難しい

最初はテスト駆動開発に挑戦したんですが、すぐに挫折しました。
どうやったらdjango.testでテストコードを上手く書けるようになるのか未だにわかりません。誰か教えてほしいです。
一応現在は、書ける範囲のテストコードをdjango.testで書いて、複雑なところはSeleniumで担保しています。
Selenium側のテストコードはメールアカウントのパスワードなどが入っているのでGitHubでは公開していません。

ソースコードを公開した理由

今回のプロジェクトは上司の許可をもらってオープンソースにすることができました。
ソースコードを公開しようと思った理由はいくつかあります。

公開しても損をしない

弊社はもともとソフトウェアやWEBサービスを販売している会社ではありません。
今回のプロジェクトは、弊社にとってはあくまでも社内システムの改修に過ぎません。
販売する予定だったものを無料で配るわけではないので、ソースコードを公開しても機会損失はほとんど発生しません。

開発費の回収は、業務効率の改善によって弊社の売上と利益が向上することでまかないます。
そもそも社内システムを自社開発するというのは、本質的にこういうことの繰り返しです。
FreeSFAの開発費はほとんどが私と同僚の人件費ですが、この程度なら仮に売上が1%向上すれば数年で回収できます。
実際にはもっと早く回収できるのではないかと見込んでいます。10

フィードバックへの期待

ソフトウェアは多くの人に使ってもらってフィードバックを受け取ることで成長していきます。
弊社の主な対象ユーザーはたったの3人しかいないので、フィードバックの絶対数が不足しがちです。

販売してフィードバックをもらうという手もありますが、商用の製品がたくさん出回っているSFA市場に参入しても、売れるとは思えません。

オープンソースのSFA製品は数が少なく、Djangoで書かれたものに至ってはおそらく皆無です。
無料なら使ってみようという人や、技術的な興味から使ってみたいと思う人は一定数いるだろうと想定しています。

Djangoの布教活動

PythonはQiitaでも一番人気の言語ですが、その割にDjangoはいまいち使われていない印象があります。
FreeSFAは今後もメンテナンスしていくので、Djangoの情報収集はこれからも必要になります。
Djangoの開発者人口がもっと増えれば、記述記事を書く人も増えて、情報を収集しやすい状況が生まれると期待しています。

FreeSFAはHerokuボタンや解説記事を用意して、導入の敷居はがんばって下げたつもりです。
これをきっかけにDjangoを使う人が増えてくれれば嬉しい限りです。

この先やってみたいこと

いずれ、こんなこともやってみたいと考えています。

機能面

  • CTIの実装(電話がかかってきたら顧客情報を画面に表示する)
  • ダッシュボード画面の充実
  • 営業目標を達成したら褒められる仕組み作り

技術面

  • Django REST APIの導入
  • vue.js対応
  • PWA化

謝辞

下記の記事がなければFreeSFAを作ることはできませんでした。
作者の皆様には心より感謝いたします。

@okoppe8
[Python] Djangoチュートリアル - 汎用業務Webアプリを最速で作る
https://qiita.com/okoppe8/items/54eb105c9c94c0960f14

Narito Takizawa 様
Djangoで、会員登録機能を自作する
https://narito.ninja/detail/38/

c_bata 様
Django における認証処理実装パターン
https://nwpct1.hatenablog.com/entry/django-auth-patterns


  1. 本当は「事業支援室」という部署名なんですが、自分たちも含めてだれもそう呼んでいません。 

  2. 例えば、エディタ内の検索ができないので、変数の使用箇所を探すのは自分の目だけが頼りです。ファイルはバイナリなのでバージョン管理はできません。複数人で同時に開発することはできません。正規表現は使えません。自動テストすることはできません。本番環境のファイルを直接編集しないとどうにもならない場面が頻繁に発生します。インポート順の調整では腱鞘炎になりそうなほど大量のドラッグアンドドロップを強いられます。 

  3. この人は鹿児島勤務です。東京と鹿児島を行ったり来たりしています。 

  4. 営業職にはノートPCとiPadが支給されていますが、ノートPCは重いのでほとんど会社に置きっぱなしだそうです。 

  5. 精査していないゴミデータが大量に混ざった顧客情報を何千件も共有のテーブルにインポートしようとしたそうなので「それはまあ怒られるよね」とは思いました。 

  6. 「GAS 制限」で検索してみましょう。 

  7. 同僚は日本語キーボードのWindows、私は英語キーボードのMacなので、お互いのマシンを操作すると無茶苦茶使いにくく感じます。 

  8. Google Maps APIの利用料は、結構高いんですよね。普通に使う分には問題ありませんが、デモサイトに不正アクセスされて大量に実行されたらたちまち破産します。https://cloud.google.com/maps-platform/pricing/sheet/?hl=ja 

  9. GitHubは無料アカウントしか持っていなくてすみません。 

  10. 営業チームの皆さん、期待してますよ。 

sikkim
サーバーサイドエンジニア。情報処理安全確保支援士(登録番号:020092)。PythonやNode.js、FileMaker、Nuxt.jsで開発することが多いです。
https://github.com/sikkimtemi
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした