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'>
CONSENT_REQUIRED_MESSAGE =
'実験への同意が必要です'
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'])
@login_required
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'])
@login_required
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: 回答済みのアンケート数