hanabiapp.views.survey

  1import json
  2from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify
  3from flask_login import login_required, current_user
  4from ..models.pre_survey import PreSurvey
  5from ..models.game_survey import GameSurvey
  6from ..models.game_info import GameInfo
  7from ..models.participant import Participant
  8
  9from .. import db
 10
 11bp = Blueprint('survey', __name__)
 12
 13with open('./hanabiapp/config/pre_survey_default.json', 'r', encoding='utf-8') as f:
 14    pre_survey_json = json.load(f)
 15
 16CONSENT_REQUIRED_MESSAGE = '実験への同意が必要です'
 17PRE_SURVEY_COMPLETED_MESSAGE = 'このアンケートは既に回答済みです'
 18
 19def validate_survey_response(received_data, question, form_data):
 20    """
 21    アンケートの回答が有効かどうかを検証する関数.
 22
 23    Args:
 24        received_data (dict): 受信したデータを格納する辞書
 25        question (dict): 検証対象の質問データ
 26        form_data (ImmutableMultiDict): フォームから送信されたデータ
 27
 28    Returns:
 29        bool: 回答が有効なら True,無効なら False
 30    """
 31
 32    if question['type'] == 'text':
 33        question_id = question['id']
 34        question_value = form_data.get(question_id, None)
 35        trigger = question.get('trigger')
 36        if trigger:
 37            trigger_id = trigger['id']
 38            trigger_values = trigger['value']
 39            if trigger_id not in received_data or received_data[trigger_id] not in question['trigger']['value']:
 40                return False
 41        if question_value:
 42            received_data[question_id] = question_value
 43            return True
 44        else:
 45            return False
 46
 47    valid_options = [ option['value'] for option in question['options'] ]
 48    question_id = question['id']
 49    question_value = form_data.get(question_id)
 50    if question_value is None:
 51        return True
 52
 53    if question_value not in valid_options:
 54        return False
 55
 56    trigger = question.get('trigger')
 57    if trigger:
 58        trigger_id = trigger['id']
 59        trigger_values = trigger['value']
 60        if trigger_id not in received_data or received_data[trigger_id] not in question['trigger']['value']:
 61            return False
 62    received_data[question_id] = question_value
 63
 64    if 'custom' in question:
 65        custom = question.get('custom')
 66        if custom:
 67            custom_trigger = custom['trigger']
 68            custom_trigger_value = custom_trigger['value']
 69            if question_value in custom_trigger_value:
 70                custom_id = custom['id']
 71                custom_value = form_data.get(custom_id)
 72                if custom_value:
 73                    received_data[custom_id] = custom_value
 74                else:
 75                    return False
 76    return True
 77
 78@bp.route('/pre_survey', methods=['GET', 'POST'])
 79@login_required
 80def pre_survey():
 81    """
 82    事前アンケートを処理する関数.
 83
 84    - ユーザーが同意済みでない場合はホームへリダイレクト.
 85    - すでに回答済みの場合は警告を表示し,ホームへリダイレクト.
 86    - POSTリクエストの場合,回答を検証し,データベースに保存.
 87
 88    Returns:
 89        Response: `survey.html` のレンダリング,またはリダイレクト
 90    """
 91    if not current_user.consent:
 92        flash(CONSENT_REQUIRED_MESSAGE, 'consent')
 93        return redirect(url_for('home.home'))
 94
 95    if current_user.pre_survey:
 96        flash(PRE_SURVEY_COMPLETED_MESSAGE, 'pre_survey_answerd')
 97        return redirect(url_for('home.home'))
 98
 99    if request.method == 'POST':
100        if current_user.pre_survey:
101            return jsonify({'error': PRE_SURVEY_COMPLETED_MESSAGE}), 400
102
103        received_data = {}
104        for question in pre_survey_json['questions']:
105            is_valid = validate_survey_response(received_data, question, request.form)
106            if not is_valid:
107                return jsonify({'error': '不正な回答です'}), 400
108            
109        for question, answer in received_data.items():
110            presurvey = PreSurvey(
111                user_id = current_user.id,
112                question_id=question,
113                answer_id=answer
114            )
115            db.session.add(presurvey)
116        current_user.pre_survey = True
117        db.session.commit()
118    
119        return redirect(url_for('home.home'))
120
121    return render_template('survey.html', survey_name=pre_survey_json['name'], questions=pre_survey_json['questions'])
122
123
124with open('./hanabiapp/config/game_survey_default.json', 'r', encoding='utf-8') as f:
125    game_survey_json = json.load(f)
126
127QUESTIONS = game_survey_json['questions']
128if game_survey_json.get('milestone_questions', None):
129    tmp = game_survey_json['milestone_questions']
130    try:
131        MILESTONES = tmp['milestones']
132    except Exception as e:
133        print(f'\'milestones\' is not found. : {e}')
134
135    try:
136        MILESTONE_QUESTIONS = tmp['questions']
137    except Exception as e:
138        print(f'\'questions\' is not found in \'milestone_questions\'. : {e}')
139
140else:
141    MILESTONES = None
142    MILESTONE_QUESTIONS = None
143
144@bp.route('/game_survey/<int:game_id>', methods=['GET', 'POST'])
145@login_required
146def game_survey(game_id):
147    """
148    ゲーム後のアンケートを処理する関数.
149
150    - ユーザーが当該ゲームの参加者であることを確認.
151    - すでに回答済みの場合はエラーメッセージを返す.
152    - POSTリクエストの場合,回答を検証し,データベースに保存.
153
154    Args:
155        game_id (int): アンケート対象のゲーム ID
156
157    Returns:
158        Response: `survey.html` のレンダリング,またはリダイレクト
159    """
160    # アンケート回答回数を取得
161    answered_survey_count = count_answered_survey(current_user.id) + 1
162    questions = QUESTIONS
163
164    # アンケート回答回数で分岐
165    if MILESTONES and  answered_survey_count in MILESTONES:
166        questions = questions + MILESTONE_QUESTIONS
167
168    if request.method == 'POST':
169        form_game_id = int(request.form.get('game_id'))
170        if not(game_id or form_game_id == game_id):
171            return jsonify({'error': '不正な回答です'}), 400
172
173        # ユーザーが当該ゲーム参加者か確認
174        valid_survey = db.session.query(
175            db.exists().where(
176                Participant.game_id == game_id,
177                Participant.user_id == current_user.id
178            )
179        ).scalar()
180        if not valid_survey:
181            return jsonify({'error': '存在しないゲームIDです'}), 400
182
183        # ユーザーが回答済みか確認
184        answered_survey = db.session.query(
185            db.exists().where(
186                GameSurvey.game_id == game_id,
187                GameSurvey.user_id == current_user.id
188            )
189        ).scalar()
190        if answered_survey:
191            return jsonify({'error': '回答済みです'})
192
193        received_data = {}
194        for question in questions:
195            is_valid = validate_survey_response(received_data, question, request.form)
196            if not is_valid:
197                return jsonify({'error': '不正な回答です'}), 400
198        
199        for question, answer in received_data.items():
200            game_survey = GameSurvey(
201                user_id = current_user.id,
202                game_id = game_id,
203                question_id=question,
204                answer_id=answer
205            )
206            db.session.add(game_survey)
207        db.session.commit()
208    
209        return redirect(url_for('home.home'))
210
211    not_answered_survey = db.session.query(Participant.game_id).join(
212        GameInfo,
213        Participant.game_id == GameInfo.id
214    ).filter(
215        Participant.user_id == current_user.id,
216        Participant.game_id == game_id,
217        ~db.exists().where(
218            GameSurvey.game_id == Participant.game_id,
219            GameSurvey.user_id == current_user.id
220        ),
221        GameInfo.game_end_reason != 'DISCONNECTED'
222    ).first()
223    if not not_answered_survey:
224        return redirect(url_for('home.home'))
225
226    return render_template('survey.html', game_id=game_id, survey_name=game_survey_json['name'], questions=questions)
227
228
229def count_answered_survey(user_id):
230    """
231    指定したユーザーが回答したアンケートの数を取得する関数.
232
233    Args:
234        user_id (int): ユーザーの ID
235
236    Returns:
237        int: 回答済みのアンケート数
238    """
239    answered_survey_count = db.session.query(
240        GameSurvey.game_id
241    ).join(
242        Participant,
243        GameSurvey.game_id == Participant.game_id
244    ).filter(
245        Participant.user_id == user_id,
246        GameSurvey.user_id == user_id
247    ).group_by(
248        GameSurvey.game_id
249    ).count()    
250    
251    return answered_survey_count
bp = <Blueprint 'survey'>
PRE_SURVEY_COMPLETED_MESSAGE = 'このアンケートは既に回答済みです'
def validate_survey_response(received_data, question, form_data):
20def validate_survey_response(received_data, question, form_data):
21    """
22    アンケートの回答が有効かどうかを検証する関数.
23
24    Args:
25        received_data (dict): 受信したデータを格納する辞書
26        question (dict): 検証対象の質問データ
27        form_data (ImmutableMultiDict): フォームから送信されたデータ
28
29    Returns:
30        bool: 回答が有効なら True,無効なら False
31    """
32
33    if question['type'] == 'text':
34        question_id = question['id']
35        question_value = form_data.get(question_id, None)
36        trigger = question.get('trigger')
37        if trigger:
38            trigger_id = trigger['id']
39            trigger_values = trigger['value']
40            if trigger_id not in received_data or received_data[trigger_id] not in question['trigger']['value']:
41                return False
42        if question_value:
43            received_data[question_id] = question_value
44            return True
45        else:
46            return False
47
48    valid_options = [ option['value'] for option in question['options'] ]
49    question_id = question['id']
50    question_value = form_data.get(question_id)
51    if question_value is None:
52        return True
53
54    if question_value not in valid_options:
55        return False
56
57    trigger = question.get('trigger')
58    if trigger:
59        trigger_id = trigger['id']
60        trigger_values = trigger['value']
61        if trigger_id not in received_data or received_data[trigger_id] not in question['trigger']['value']:
62            return False
63    received_data[question_id] = question_value
64
65    if 'custom' in question:
66        custom = question.get('custom')
67        if custom:
68            custom_trigger = custom['trigger']
69            custom_trigger_value = custom_trigger['value']
70            if question_value in custom_trigger_value:
71                custom_id = custom['id']
72                custom_value = form_data.get(custom_id)
73                if custom_value:
74                    received_data[custom_id] = custom_value
75                else:
76                    return False
77    return True

アンケートの回答が有効かどうかを検証する関数.

Arguments:
  • received_data (dict): 受信したデータを格納する辞書
  • question (dict): 検証対象の質問データ
  • form_data (ImmutableMultiDict): フォームから送信されたデータ
Returns:

bool: 回答が有効なら True,無効なら False

@bp.route('/pre_survey', methods=['GET', 'POST'])
def pre_survey():
 79@bp.route('/pre_survey', methods=['GET', 'POST'])
 80@login_required
 81def pre_survey():
 82    """
 83    事前アンケートを処理する関数.
 84
 85    - ユーザーが同意済みでない場合はホームへリダイレクト.
 86    - すでに回答済みの場合は警告を表示し,ホームへリダイレクト.
 87    - POSTリクエストの場合,回答を検証し,データベースに保存.
 88
 89    Returns:
 90        Response: `survey.html` のレンダリング,またはリダイレクト
 91    """
 92    if not current_user.consent:
 93        flash(CONSENT_REQUIRED_MESSAGE, 'consent')
 94        return redirect(url_for('home.home'))
 95
 96    if current_user.pre_survey:
 97        flash(PRE_SURVEY_COMPLETED_MESSAGE, 'pre_survey_answerd')
 98        return redirect(url_for('home.home'))
 99
100    if request.method == 'POST':
101        if current_user.pre_survey:
102            return jsonify({'error': PRE_SURVEY_COMPLETED_MESSAGE}), 400
103
104        received_data = {}
105        for question in pre_survey_json['questions']:
106            is_valid = validate_survey_response(received_data, question, request.form)
107            if not is_valid:
108                return jsonify({'error': '不正な回答です'}), 400
109            
110        for question, answer in received_data.items():
111            presurvey = PreSurvey(
112                user_id = current_user.id,
113                question_id=question,
114                answer_id=answer
115            )
116            db.session.add(presurvey)
117        current_user.pre_survey = True
118        db.session.commit()
119    
120        return redirect(url_for('home.home'))
121
122    return render_template('survey.html', survey_name=pre_survey_json['name'], questions=pre_survey_json['questions'])

事前アンケートを処理する関数.

  • ユーザーが同意済みでない場合はホームへリダイレクト.
  • すでに回答済みの場合は警告を表示し,ホームへリダイレクト.
  • POSTリクエストの場合,回答を検証し,データベースに保存.
Returns:

Response: survey.html のレンダリング,またはリダイレクト

QUESTIONS = [{'id': 'useful_ai_hint', 'type': 'radio', 'text': 'このAIの出すヒントは有益でしたか?', 'options': [{'value': 'not_useful', 'text': '全く有益ではなかった'}, {'value': 'little_useful', 'text': 'あまり有益ではなかった'}, {'value': 'neutral', 'text': 'どちらともいえない'}, {'value': 'some_useful', 'text': 'ある程度有益だった'}, {'value': 'very_useful', 'text': '非常に有益だった'}]}, {'id': 'understanding_my_hint', 'type': 'radio', 'text': ' こちらのヒントを十分に理解して役立ててくれましたか?', 'options': [{'value': 'not_helpful', 'text': '全く役立ててくれなかった'}, {'value': 'little_helpful', 'text': 'あまり役立ててくれなかった'}, {'value': 'neutral', 'text': 'どちらともいえない'}, {'value': 'some_helpful', 'text': 'ある程度役立ててくれた'}, {'value': 'very_helpful', 'text': '十分に役立ててくれた'}]}, {'id': 'next_potential', 'type': 'radio', 'text': 'このAIと次にゲームをプレイしたとき,さらにスコアを伸ばせそうですか?', 'options': [{'value': 'not_possible', 'text': '全く伸ばせそうにない'}, {'value': 'little_possible', 'text': 'あまり伸ばせそうにない'}, {'value': 'neutral', 'text': 'どちらともいえない'}, {'value': 'some_possible', 'text': 'ある程度伸ばせそう'}, {'value': 'very_possible', 'text': '非常に伸ばせそう'}]}]
@bp.route('/game_survey/<int:game_id>', methods=['GET', 'POST'])
def game_survey(game_id):
145@bp.route('/game_survey/<int:game_id>', methods=['GET', 'POST'])
146@login_required
147def game_survey(game_id):
148    """
149    ゲーム後のアンケートを処理する関数.
150
151    - ユーザーが当該ゲームの参加者であることを確認.
152    - すでに回答済みの場合はエラーメッセージを返す.
153    - POSTリクエストの場合,回答を検証し,データベースに保存.
154
155    Args:
156        game_id (int): アンケート対象のゲーム ID
157
158    Returns:
159        Response: `survey.html` のレンダリング,またはリダイレクト
160    """
161    # アンケート回答回数を取得
162    answered_survey_count = count_answered_survey(current_user.id) + 1
163    questions = QUESTIONS
164
165    # アンケート回答回数で分岐
166    if MILESTONES and  answered_survey_count in MILESTONES:
167        questions = questions + MILESTONE_QUESTIONS
168
169    if request.method == 'POST':
170        form_game_id = int(request.form.get('game_id'))
171        if not(game_id or form_game_id == game_id):
172            return jsonify({'error': '不正な回答です'}), 400
173
174        # ユーザーが当該ゲーム参加者か確認
175        valid_survey = db.session.query(
176            db.exists().where(
177                Participant.game_id == game_id,
178                Participant.user_id == current_user.id
179            )
180        ).scalar()
181        if not valid_survey:
182            return jsonify({'error': '存在しないゲームIDです'}), 400
183
184        # ユーザーが回答済みか確認
185        answered_survey = db.session.query(
186            db.exists().where(
187                GameSurvey.game_id == game_id,
188                GameSurvey.user_id == current_user.id
189            )
190        ).scalar()
191        if answered_survey:
192            return jsonify({'error': '回答済みです'})
193
194        received_data = {}
195        for question in questions:
196            is_valid = validate_survey_response(received_data, question, request.form)
197            if not is_valid:
198                return jsonify({'error': '不正な回答です'}), 400
199        
200        for question, answer in received_data.items():
201            game_survey = GameSurvey(
202                user_id = current_user.id,
203                game_id = game_id,
204                question_id=question,
205                answer_id=answer
206            )
207            db.session.add(game_survey)
208        db.session.commit()
209    
210        return redirect(url_for('home.home'))
211
212    not_answered_survey = db.session.query(Participant.game_id).join(
213        GameInfo,
214        Participant.game_id == GameInfo.id
215    ).filter(
216        Participant.user_id == current_user.id,
217        Participant.game_id == game_id,
218        ~db.exists().where(
219            GameSurvey.game_id == Participant.game_id,
220            GameSurvey.user_id == current_user.id
221        ),
222        GameInfo.game_end_reason != 'DISCONNECTED'
223    ).first()
224    if not not_answered_survey:
225        return redirect(url_for('home.home'))
226
227    return render_template('survey.html', game_id=game_id, survey_name=game_survey_json['name'], questions=questions)

ゲーム後のアンケートを処理する関数.

  • ユーザーが当該ゲームの参加者であることを確認.
  • すでに回答済みの場合はエラーメッセージを返す.
  • POSTリクエストの場合,回答を検証し,データベースに保存.
Arguments:
  • game_id (int): アンケート対象のゲーム ID
Returns:

Response: survey.html のレンダリング,またはリダイレクト

def count_answered_survey(user_id):
230def count_answered_survey(user_id):
231    """
232    指定したユーザーが回答したアンケートの数を取得する関数.
233
234    Args:
235        user_id (int): ユーザーの ID
236
237    Returns:
238        int: 回答済みのアンケート数
239    """
240    answered_survey_count = db.session.query(
241        GameSurvey.game_id
242    ).join(
243        Participant,
244        GameSurvey.game_id == Participant.game_id
245    ).filter(
246        Participant.user_id == user_id,
247        GameSurvey.user_id == user_id
248    ).group_by(
249        GameSurvey.game_id
250    ).count()    
251    
252    return answered_survey_count

指定したユーザーが回答したアンケートの数を取得する関数.

Arguments:
  • user_id (int): ユーザーの ID
Returns:

int: 回答済みのアンケート数