Mail(User Interfacesも同じ問題)

メール

電子メールを送受信するためのAPI呼び出しを行う電子メールクライアントのフロントエンドを設計します。

Inbox page
Email page

はじめに

  1. 配布コードをhttps://cdn.cs50.net/web/2020/spring/projects/3/mail.zip からダウンロードし、解凍します。
  2. ターミナルで、project3 ディレクトリに cd します。
  3. python manage.py makemigrations mail を実行し、mail アプリのマイグレーションを行います。
  4. python manage.py migrate を実行して、データベースにマイグレーションを適用します。

理解を深める

配布コードには、mail という1つのアプリケーションを含む project3 というDjangoプロジェクトがあります。

まず、プロジェクトのマイグレーションの作成と適用を行い、python manage.py runserver を実行してWebサーバを起動します。ブラウザでWebサーバを開き、 「Register」 リンクを使用して新しいアカウントを登録します。このプロジェクトで送受信する電子メールは、すべてデータベースに保存されます(実際のメールサーバには送信されません)。このプロジェクトで使用する電子メールアドレス (例 foo@example.com ) とパスワードを選択してください。資格情報は、実際の電子メールアドレスに対して有効な資格情報である必要はありません。

サインインすると、メールクライアントのInboxページが表示されますが、このページはほとんど空白です (今のところ)。ボタンをクリックして [送信済み] および [アーカイブ済み] メールボックスに移動し、これらのメールボックスも現在空白になっていることを確認します。「作成」 ボタンをクリックすると、新しいメールを作成するためのフォームが表示されます。ただし、ボタンをクリックするたびに、新しいルートに移動したり、新しいWebリクエストを作成したりするわけではありません。代わりに、このアプリケーション全体が1つのページになり、ユーザインターフェースの制御にJavaScriptが使用されます。では、配布コードを詳しく見てみましょう。

mail/urls.py を見ると、デフォルトルートは views.py にインデックス関数をロードしていることがわかります。では、views.py を開いて index 関数を見てみましょう。ユーザがサインインしている限り、この関数は mail/inbox.html テンプレートをレンダリングします。mail/templates/mail/inbox.html に保存されているテンプレートを見てみましょう。ページの本文では、ユーザの電子メールアドレスが最初に h2 要素で表示されます。その後、ページには、アプリケーションのさまざまなページ間を移動するための一連のボタンがあります。その下に、このページには2つのメインセクションがあり、各セクションは div 要素で定義されています。最初( emails-viewid )は、電子メールメールボックスの内容が含まれます(初めは空です)。次( compose-viewid )に、ユーザが新しい電子メールを作成できるフォームが含まれています。上に並んでいるボタンは、これらのビューを選択的に表示/非表示にする必要があります。例えば、メール作成ボタンは emails-view を非表示にし、compose-view を表示します。受信トレイボタンは compose-view を非表示にし、emails-view を表示します。

どのように行うのでしょうか? inbox.html の下部には、JavaScriptファイルmail/inbox.jsが含まれています。mail/static/mail/inbox.js に保存されているファイルを開き、見てください。ページのDOMコンテンツがロードされると、各ボタンにイベントリスナーが付加されます。例えば、'inbox' ボタンがクリックされたときには、引数 'inbox' を指定して load_mailbox 関数を呼び出します。一方、作成ボタンをクリックすると、compose_email 関数が呼び出されます。これらの関数の機能は何でしょうか。compose_email 関数は、まず emails-view ( style.display プロパティを none に設定する)を隠し、compose-view ( style.display プロパティを block に設定する)を表示します。その後、この関数はすべてのフォーム入力フィールド(ユーザが受信者の電子メールアドレス、件名、および電子メール本文を入力できる場所)を取得し、その値を空の文字列「 ' ' 」に設定してクリアします。これは、 「作成」 ボタンをクリックするたびに空の電子メールフォームが表示されることを意味します。フォームに値を入力し、ビューを受信トレイに切り替えてから、作成ビューに戻ってテストできます。

一方、load_mailbox 関数は最初に emails-view を表示し、compose-view を非表示にします。load_mailbox 関数は、ユーザが表示しようとしているメールボックスの名前となる引数も取ります。このプロジェクトでは、inbox (受信ボックス)、sent (すべての送信済みメールを補完する送信済みボックス)、archive (受信ボックス内からアーカイブされたメール) の3つのメールボックスを持つ電子メールクライアントを設計します。load_mailbox 関数への引数はこれら3つの値のひとつとなり、load_mailbox 関数は選択されたメールボックスの名前を emails-viewinnerHTML を更新して表示します(最初の文字を大文字にした後で)。ブラウザでメールボックス名を選択すると、そのメールボックスの名前 (大文字) がDOMに表示されるのはこのためです。load_mailbox 関数は、適切なテキストを含めるように emails-view を更新しています。

もちろん、このアプリは未完成です。すべてのメールボックスにはメールボックスの名前(受信トレイ、送信済み、アーカイブ)が表示されますが、実際にはメールは表示されません。メールの内容を実際に見ることができるビューはまだありません。作成フォームではメールの内容を入力することができますが、メールを送信するボタンは実際には何も行いません。そこであなたの出番です!

API

このアプリケーションのAPIを使用して、メールの受信、メールの送信、および電子メールの更新を行います。JavaScriptコードで使用できるように、API全体を作成しました (以下に記述があります)。(実際には、このプロジェクト用にすべてのPythonコードを作成しました。HTMLとJavaScriptを記述するだけで、このプロジェクトを完了できるはずです。) 

このアプリケーションは、以下のAPIルートをサポートしています。

GET /emails/<str:mailbox>

<mailbox>inboxsent , archive のいずれかである /emails/<mailbox>GET リクエストを送信すると、その mailbox 内の全てのメールのリストが古い順に(JSON形式で)返ってきます。例えば、GET リクエストを /emails/inbox に送ると、以下のようなJSONレスポンスを受け取るでしょう (2通の電子メールの表示)。

[
    {
        "id": 100,
        "sender": "foo@example.com",
        "recipients": ["bar@example.com"],
        "subject": "Hello!",
        "body": "Hello, world!",
        "timestamp": "Jan 2 2020, 12:00 AM",
        "read": false,
        "archived": false
    },
    {
        "id": 95,
        "sender": "baz@example.com",
        "recipients": ["bar@example.com"],
        "subject": "Meeting Tomorrow",
        "body": "What time are we meeting?",
        "timestamp": "Jan 1 2020, 12:00 AM",
        "read": true,
        "archived": false
    }
]

各電子メールには、id  (固有の識別子)、送信者 sender の電子メールアドレス、受信者 recipients の配列、件名 subject 、本文 body 、タイムスタンプ timestamp の文字列、電子メールが読み取られたかどうか read 、および電子メールがアーカイブされたかどうか archived を示すブール値が指定されます。

JavaScriptでこのような値にアクセスするにはどうすればよいでしょうか。JavaScriptでは、fetch を使用してWeb要求を作成できます。したがって、次のJavaScriptコードを使用します。

fetch('/emails/inbox')
.then(response => response.json())
.then(emails => {
    // Print emails
    console.log(emails);

    // ... do something else with emails ...
});

このコードは、/emails/inbox に対して GET リクエストを行い、結果のレスポンスをJSONに変換してから、変数 emails 内の一連のメールを提供します。console.log (受信トレイに電子メールがない場合は、空の配列になります)を使用して、その値をブラウザーのコンソールに出力することも、その配列に対して別の操作を行うこともできます。

また、無効なメールボックスを要求した場合(受信トレイ inbox 、送信済み sent 、またはアーカイブ archive 以外のもの)、JSONの応答が返されることにも注意してください {"error": "Invalid mailbox."}

GET /emails/<int:email_id>

/emails/email_id ( email_id は電子メールの整数ID) に GET リクエストを送ると、以下のように電子メールのJSON表現を返します。

{
        "id": 100,
        "sender": "foo@example.com",
        "recipients": ["bar@example.com"],
        "subject": "Hello!",
        "body": "Hello, world!",
        "timestamp": "Jan 2 2020, 12:00 AM",
        "read": false,
        "archived": false
}

電子メールが存在しない場合、またはユーザが電子メールにアクセスできない場合は、代わりに404 Not Found エラーが返され、JSONで {"error": "Email not found."} という応答が返されます。

例えば、電子メール番号100を取得するには、次のようなJavaScriptコードを記述します。

fetch('/emails/100')
.then(response => response.json())
.then(email => {
    // Print email
    console.log(email);

    // ... do something else with email ...
});

POST /emails

ここまでで、メールボックス内のすべてのメールを取得する方法と、1つのメールのみを取得する方法について説明しました。電子メールを送信するには、POST 要求を /emails ルートに送信します。このルートでは、送信されるデータとして、recipients 値(電子メールを送信するすべてのユーザをコンマで区切った文字列)、subject 文字列、および body 文字列が必要です。たとえば、次のようなJavaScriptコードを作成できます。

fetch('/emails', {
  method: 'POST',
  body: JSON.stringify({
      recipients: 'baz@example.com',
      subject: 'Meeting time',
      body: 'How about we meet tomorrow at 3pm?'
  })
})
.then(response => response.json())
.then(result => {
    // Print result
    console.log(result);
});

電子メールが正常に送信されると、ルートは201ステータスコードと {"message": "Email sent successfully."} ({「メッセージ」 「電子メールが正常に送信されました」}) というJSONレスポンスで応答します。

電子メールの受信者が少なくとも1人はいる必要があることに注意してください。受信者が指定されていない場合、ルートは400ステータスコードと {"error": "At least one recipient required."} ({「エラー」 「少なくとも1人の受信者が必要です。」}) というJSONレスポンスで応答します。すべての受信者は、この特定のWebアプリケーションに登録した有効なユーザである必要があります。baz@example.com に電子メールを送信しようとしても、その電子メールアドレスを持つユーザがいない場合は、{"error": "User with email baz@example.com does not exist."} ({「エラー」 「電子メールbaz@example.comのユーザは存在しません。」}) というJSONの応答が返されます。

PUT /emails/<int:email_id>

最後に、メールを 「既読/未読」 または 「アーカイブ/未アーカイブ」 としてマークする機能が必要になります。そのためには、PUT リクエスト( GET の代わりに)を /emails/<email_id> に送信します。email_id は変更しようとしているメールのIDです。たとえば、次のようなJavaScriptコードです。

fetch('/emails/100', {
  method: 'PUT',
  body: JSON.stringify({
      archived: true
  })
})

このコードは、メール番号100をアーカイブ済みとしてマークします。PUT リクエストの本体は、メッセージのアーカイブを解除するために {archived: false} することもでき、同様に、電子メールをそれぞれ既読または未読としてマークするために {read: true} または read: false} のいずれかにすることもできます。

これらの4つのAPIルート(メールボックス内のすべての電子メールの取得、単一の電子メールの取得、電子メールの送信、および既存の電子メールの更新)を使用すると、このプロジェクトを完了するために必要なツールがすべて揃っているはずです。

プロジェクトの仕様

JavaScript、HTML、およびCSSを使用して、単一ページアプリケーションの電子メールクライアントの実装を完了します。次の要件を満たしている必要があります。

  • メールの送信:ユーザがメール作成フォームを送信するときに、実際にメールを送信するJavaScriptコードを追加します。
    • /emails に対して POST リクエストを行い、受信者 recipients、件名 subject、および本文 body の値を渡すことができます。
    • メールが送信されたら、ユーザの送信済みメールボックスをロードします。
  • メールボックス:ユーザが [受信トレイ] 、 [送信済みメールボックス] 、または [アーカイブ] にアクセスしたときに、適切なメールボックスを読み込みます。
    • 特定のメールボックスのEメールを要求するには、/emails/<mailbox> に対して GET リクエストを発行します。
    • メールボックスにアクセスする場合、アプリケーションはまずそのメールボックス内の最新の電子メールをAPIに問い合わせます。
    • メールボックスにアクセスすると、ページの先頭にメールボックスの名前が表示されます(この部分はあなたのために既に実装されています)。
    • 各電子メールは、それぞれ専用のボックスに表示されます (たとえば、<div> で区切られたボックス)。 電子メールの送信元、件名、電子メールのタイムスタンプが表示されます。
    • 電子メールが未読の場合は、白い背景で表示されます。電子メールが開封されている場合は、背景がグレーで表示されます。
  • 電子メールの表示:ユーザが電子メールをクリックすると、その電子メールの内容が表示されます。
    • GET リクエストを /emails/<email_id> に送信して、電子メールをリクエストすることをお勧めします。
    • アプリケーションには、電子メールの送信者、受信者、件名、タイムスタンプ、および本文が表示されます。
    • 多くの場合、電子メールを表示するための divinbox.html (メールビュー emails-view とコンポジットビュー compose-view に加えて)に追加します。ナビゲーションオプションをクリックしたときに右側のビューの表示/非表示を切り替えるように、コードを更新してください。
    • DOMに追加したHTML要素にイベントリスナーを追加する方法については、 「ヒント」 セクションのヒントを参照してください。
    • Eメールをクリックしたら、Eメールを既読としてマークします。PUT リクエストを /emails/<email_id> に送信して、電子メールが読まれているかどうかを更新できることを思い出してください。
  • アーカイブおよびアーカイブ解除:ユーザは、受信したメールをアーカイブおよびアーカイブ解除できます。
    • 受信トレイの電子メールを表示すると、ユーザに電子メールをアーカイブするためのボタンが表示されます。アーカイブされたメールを表示すると、ユーザにメールのアーカイブを解除するためのボタンが表示されます。この要件は、送信済みメールボックス内の電子メールには適用されません。
    • PUT リクエストを /emails/<email_id> に送信して、メールをアーカイブ済みまたは未アーカイブとしてマークできることを思い出してください。
    • メールがアーカイブまたはアーカイブ解除されたら、ユーザの受信トレイをロードします。
  • 返信:ユーザが電子メールに返信できるようにします。
    • Eメールを表示すると、 「返信」 ボタンが表示され、Eメールに返信できます。
    • ユーザが 「返信」 ボタンをクリックすると、メール作成フォームに移動します。
    • 作成フォームに、元の電子メールの送信者に設定された受信者 recipient フィールドを事前に入力します。
    • 件名 subject を入力します。元の電子メールの件名が foo の場合、新しい件名は Re: foo になります。(件名が既に Re: で始まっている場合は、再度追加する必要はありません。)
    • 電子メールの本文に "On Jan 1 2020, 12:00 AM foo@example.com wrote:" のような行を挿入し、その後に電子メールの元のテキストを入力します。

ヒント

  • HTML要素を作成してイベントハンドラを追加するには、次のようなJavaScriptコードを使用します。
const element = document.createElement('div');
element.innerHTML = 'This is the content of the div.';
element.addEventListener('click', function() {
    console.log('This element has been clicked!')
});
document.querySelector('#container').append(element);

このコードは、新しい div 要素を作成し、その innerHTML を設定し、その div 要素がクリックされたときに特定の関数を実行するイベントハンドラを追加してから、container (このコードでは、container という id を持つHTML要素があることを前提としています。要素を追加する要素になるように引数を querySelector に変更します) というIDを持つHTML要素に追加します。

  • mail/static/mail/styles.css を編集して、アプリケーションに必要なCSSを追加すると便利です。
  • JavaScript配列がある場合は、forEach を使用してその配列の各要素をループすることができます。
  • 通常、POST および PUT リクエストの場合、DjangoはCSRFトークンを必要とし、サイト間のリクエスト偽造攻撃の可能性を防ぎます。このプロジェクトでは、意図的にAPIルートをCSRF免除にしているため、トークンは必要ありません。しかし、実際のプロジェクトでは、このような潜在的な脆弱性を防ぐのが常に最善です。

提出方法

ちょっと待ってください!以前のバージョンのProject 2 (Flack) を既に提出し、合格点を受け取っている場合は、ここで中断してください。あなたは既にこのプロジェクトの単位を取得しています。この課題を提出してはいけません。提出しても、コースの進行には影響がないため、採点者の負荷を増やすだけです!

  1. このリンクにアクセスして、GitHubアカウントでログインし、 「cs 50の承認」 をクリックします。次に、コースのスタッフに提出課題へのアクセス権を付与することを示すボックスにチェックを入れ、 「コースに参加」 をクリックします。
  2. Gitをインストールし、オプションでsubmit 50をインストール します。

プロジェクトを送信するときに、web50/projects/2020/x/mail ブランチの内容が、最初にダウンロードしてzip解凍した時の配布コードのファイル構造と一致している必要があります。つまり、自分で作成した他のディレクトリの中にファイルをネストしてはいけません。ブランチには、ほかのプロジェクトのコードは含まれず、このプロジェクトのコードだけが含まれるようにします。このファイル構造に従わないと、採点が拒否される可能性があります。

たとえば、このプロジェクトでは、採点者が https://github.com/me50/USERNAME/tree/web50/projects/2020/x/mail (ここで、USERNAME は、以下のフォームに記載されているユーザ独自のGitHubユーザ名です) にアクセスした場合 ( mailproject3 )というサブディレクトリと manage.py ファイルが表示されます。チェックするときにコードがこのように編成されていない場合は、この規則に合わせて必要に応じてリポジトリを再編成します。

  1. submit50 をインストールしている場合は、次のコマンドを実行します。
submit50 web 50/projects/2020/x/mail

それ以外の場合は、Gitを使用して、https://github.com/me50/USERNAME.git ( USERNAME はGitHubのユーザ名) の web50/projects/2020/x/mail というブランチに作業をプッシュします。

  1. 長さが5分を超えないようにスクリーンキャストを記録し、プロジェクトの機能を実演します。上記の仕様のすべての要素がビデオで示されていることを確認してください。このビデオでは、コードを表示する必要はありません。実行中のアプリケーションのみを表示します。GitHubでコードをレビューします。そのビデオをYouTubeなど (リストに非公開あるいはパブリック設定で、プライベート設定にはせずに) にアップロードしてください。ビデオの説明では、それぞれの5つの仕様を示している場所にタイムスタンプを付ける必要があります。これはオプションではなく、説明にタイムスタンプがないビデオは自動的に拒否されます。
  2. このフォームを送信します。

その後、https://cs50.me/cs50w にアクセスして、現在の進捗状況を確認できます。

cs50.me/cs50wの成績に関する重要な注意事項:通常、各暦年の開始直後 (2021年1月11日の週を予定) に、すべての成績を更新します。2020年からのすべての課題は保存されてそのまま続けることができますが、2021年に課題を送信してCS 50 Botにより評価・返却されるまで、成績は一時的に空または使用不可の状態で表示されます。だから心配しないでください!

CS50Wを2020年の最終日に終了した場合、この成績のリセットが行われる前に無料のCS50証明書を申請することが非常に重要ですから、遅れないようにしてください (認証済の証明書はこの影響を受けません) 。