package org.adaptlab.chpir.android.survey.Models; import java.util.ArrayList; import java.util.List; import org.adaptlab.chpir.android.activerecordcloudsync.ReceiveModel; import org.adaptlab.chpir.android.survey.AppUtil; import org.adaptlab.chpir.android.survey.FormatUtils; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import android.content.Context; import android.util.Log; import com.activeandroid.annotation.Column; import com.activeandroid.annotation.Table; import com.activeandroid.query.Select; @Table(name = "Questions") public class Question extends ReceiveModel { private static final String TAG = "QuestionModel"; public static final String FOLLOW_UP_TRIGGER_STRING = "\\[followup\\]"; public static enum QuestionType { SELECT_ONE, SELECT_MULTIPLE, SELECT_ONE_WRITE_OTHER, SELECT_MULTIPLE_WRITE_OTHER, FREE_RESPONSE, SLIDER, FRONT_PICTURE, REAR_PICTURE, DATE, RATING, TIME, LIST_OF_TEXT_BOXES, INTEGER, EMAIL_ADDRESS, DECIMAL_NUMBER, INSTRUCTIONS, MONTH_AND_YEAR, YEAR, PHONE_NUMBER, ADDRESS, SELECT_ONE_IMAGE, SELECT_MULTIPLE_IMAGE, LIST_OF_INTEGER_BOXES, LABELED_SLIDER; } @Column(name = "Text") private String mText; @Column(name = "QuestionType") private QuestionType mQuestionType; @Column(name = "QuestionIdentifier") private String mQuestionIdentifier; @Column(name = "Instrument") private Instrument mInstrument; @Column(name = "FollowingUpQuestion") private Question mFollowingUpQuestion; @Column(name = "FollowUpPosition") private int mFollowUpPosition; @Column(name = "RegExValidation") private String mRegExValidation; @Column(name = "RegExValidationMessage") private String mRegExValidationMessage; @Column(name = "OptionCount") private int mOptionCount; @Column(name = "InstrumentVersion") private int mInstrumentVersion; @Column(name = "NumberInInstrument") private int mNumberInInstrument; @Column(name = "IdentifiesSurvey") private boolean mIdentifiesSurvey; // https://github.com/pardom/ActiveAndroid/issues/22 @Column(name = "RemoteId", unique = true, onUniqueConflict = Column.ConflictAction.REPLACE) private Long mRemoteId; @Column(name ="ImageCount") private int mImageCount; @Column(name = "Instructions") private String mInstructions; @Column(name = "QuestionVersion") private int mQuestionVersion; @Column(name = "Grid") private Grid mGrid; @Column(name = "FirstInGrid") private boolean mFirstInGrid; @Column(name = "Deleted") private boolean mDeleted; public Question() { super(); } /* * If the language of the instrument is the same as the language setting on the * device (or through the admin settings), then return the question text. * * If another language is requested, iterate through question translations to * find translated text. * * If the language requested is not available as a translation, return the non-translated * text for the question. */ public String getText() { if (getInstrument().getLanguage().equals(Instrument.getDeviceLanguage())) return mText; for(QuestionTranslation translation : translations()) { if (translation.getLanguage().equals(Instrument.getDeviceLanguage())) { return translation.getText(); } } // Fall back to default return mText; } public String getRegExValidationMessage() { if (getInstrument().getLanguage().equals(Instrument.getDeviceLanguage())) return mRegExValidationMessage; for(QuestionTranslation translation : translations()) { if (translation.getLanguage().equals(Instrument.getDeviceLanguage())) { return translation.getRegExValidationMessage(); } } // Fall back to default return mRegExValidationMessage; } public boolean hasSkipPattern() { for (Option option : options()) { if (option.getNextQuestion() != null && !option.getNextQuestion().getQuestionIdentifier().equals("")) { return true; } } return false; } public boolean hasMultiSkipPattern() { for (Option option: options()) { if (option.skips() != null && !option.skips().isEmpty()) { return true; } } return false; } /* * Map a response represented as an index to its corresponding * option text. If this is an "other" response, return the * text specified in the other response. */ public String getOptionTextByResponse(Response response, Context context) { String text = response.getText(); try { if (hasMultipleResponses()) { return FormatUtils.unformatMultipleResponses(options(), text, context); } else if (Integer.parseInt(text) == options().size()) { return response.getOtherResponse(); } else { return options().get(Integer.parseInt(text)).getText(); } } catch (NumberFormatException nfe) { Log.e(TAG, text + " is not an option number"); return text; } catch (IndexOutOfBoundsException iob) { Log.e(TAG, text + " is an out of range option number"); return text; } } /* * Return the processed string for a following up question. * * Replace the follow up trigger string token with the appropriate * response. If this is a question with options, then map the option * number to the option text. If not, then return the text response. * * If the question that is being followed up on was skipped by the user, * then return nothing. This question will be skipped in that case. */ public String getFollowingUpText(Survey survey, Context context) { Response followUpResponse = survey.getResponseByQuestion(getFollowingUpQuestion()); if (followUpResponse == null || followUpResponse.getText().equals("") || followUpResponse.hasSpecialResponse()) { return null; } if (followUpWithOptionText()) { return getText().replaceAll( FOLLOW_UP_TRIGGER_STRING, getFollowingUpQuestion().getOptionTextByResponse(followUpResponse, context) ); } else { return getText().replaceAll(FOLLOW_UP_TRIGGER_STRING, followUpResponse.getText()); } } /* * Question types which must have their responses (represented as indices) * mapped to the original option text. */ public boolean followUpWithOptionText() { return getFollowingUpQuestion().getQuestionType().equals(QuestionType.SELECT_MULTIPLE) || getFollowingUpQuestion().getQuestionType().equals(QuestionType.SELECT_ONE) || getFollowingUpQuestion().getQuestionType().equals(QuestionType.SELECT_ONE_WRITE_OTHER) || getFollowingUpQuestion().getQuestionType().equals(QuestionType.SELECT_MULTIPLE_WRITE_OTHER); } /* * Return true if this response can be an array of multiple options. */ public boolean hasMultipleResponses() { return getQuestionType().equals(QuestionType.SELECT_MULTIPLE) || getQuestionType().equals(QuestionType.SELECT_MULTIPLE_WRITE_OTHER); } /* * Find an existing translation, or return a new QuestionTranslation * if a translation does not yet exist. */ public QuestionTranslation getTranslationByLanguage(String language) { for(QuestionTranslation translation : translations()) { if (translation.getLanguage().equals(language)) { return translation; } } QuestionTranslation translation = new QuestionTranslation(); translation.setLanguage(language); return translation; } /* * Check that all of the options are loaded and that the instrument version * numbers of the question components match the expected instrument version * number. */ public boolean loaded() { return getOptionCount() == options().size() && getImageCount() == images().size(); } @Override public void createObjectFromJSON(JSONObject jsonObject) { try { Long remoteId = jsonObject.getLong("id"); // If a question already exists, update it from the remote Question question = Question.findByRemoteId(remoteId); if (question == null) { question = this; } if (AppUtil.DEBUG) Log.i(TAG, "Creating object from JSON Object: " + jsonObject); question.setText(jsonObject.getString("text")); question.setQuestionType(jsonObject.getString("question_type")); question.setQuestionIdentifier(jsonObject.getString("question_identifier")); question.setInstrument(Instrument.findByRemoteId(jsonObject.getLong("instrument_id"))); question.setRegExValidation(jsonObject.getString("reg_ex_validation")); question.setRegExValidationMessage(jsonObject.getString("reg_ex_validation_message")); question.setOptionCount(jsonObject.getInt("option_count")); question.setImageCount(jsonObject.getInt("image_count")); question.setInstrumentVersion(jsonObject.getInt("instrument_version")); if (!jsonObject.isNull("number_in_instrument")) { question.setNumberInInstrument(jsonObject.getInt("number_in_instrument")); } question.setFollowUpPosition(jsonObject.getInt("follow_up_position")); question.setIdentifiesSurvey(jsonObject.getBoolean("identifies_survey")); question.setInstructions(jsonObject.getString("instructions")); question.setQuestionVersion(jsonObject.getInt("question_version")); question.setFollowingUpQuestion(Question.findByQuestionIdentifier( jsonObject.getString("following_up_question_identifier") ) ); if (!jsonObject.isNull("grid_id")) { question.setGrid(Grid.findByRemoteId(jsonObject.getLong("grid_id"))); } question.setFirstInGrid(jsonObject.getBoolean("first_in_grid")); question.setRemoteId(remoteId); if (!jsonObject.isNull("deleted_at")) { question.setDeleted(true); } question.save(); // Generate translations JSONArray translationsArray = jsonObject.getJSONArray("translations"); for(int i = 0; i < translationsArray.length(); i++) { JSONObject translationJSON = translationsArray.getJSONObject(i); QuestionTranslation translation = question.getTranslationByLanguage(translationJSON.getString("language")); translation.setQuestion(question); translation.setText(translationJSON.getString("text")); translation.setRegExValidationMessage(translationJSON.getString("reg_ex_validation_message")); translation.save(); } } catch (JSONException je) { Log.e(TAG, "Error parsing object json", je); } } /* * Finders */ public static List<Question> getAll() { return new Select().from(Question.class).where("Deleted != ?", 1).orderBy("Id ASC").execute(); } public static Question findByRemoteId(Long id) { return new Select().from(Question.class).where("RemoteId = ?", id).executeSingle(); } public static Question findByQuestionIdentifier(String identifier) { return new Select().from(Question.class).where("QuestionIdentifier = ?", identifier).executeSingle(); } public static Question findByNumberInInstrument(Integer questionNumber, Long instrumentId) { return new Select().from(Question.class).where("NumberInInstrument = ? AND Instrument = ?", questionNumber, instrumentId).executeSingle(); } public boolean isFollowUpQuestion() { return (getFollowingUpQuestion() != null); } public List<Question> questionsToSkip() { List<Question> toBeSkipped = new ArrayList<Question>(); for (Option option : options()) { for (Question question : option.questionsToSkip()) { toBeSkipped.add(question); } } return toBeSkipped; } /* * Relationships */ public boolean hasOptions() { return !options().isEmpty(); } public List<Option> options() { return new Select().from(Option.class) .where("Question = ? AND Deleted != ?", getId(), 1) .orderBy("NumberInQuestion ASC") .execute(); } public List<Image> images() { return new Select().from(Image.class).where("Question = ?", getId()).execute(); } public List<QuestionTranslation> translations() { return getMany(QuestionTranslation.class, "Question"); } /* * Getters/Setters */ public void setText(String text) { mText = text; } public QuestionType getQuestionType() { return mQuestionType; } public void setQuestionType(String questionType) { if (validQuestionType(questionType)) { mQuestionType = QuestionType.valueOf(questionType); } else { // This should never happen // We should prevent syncing data unless app is up to date Log.wtf(TAG, "Received invalid question type: " + questionType); } } public void setRegExValidation(String validation) { mRegExValidation = validation; } public String getRegExValidation() { if (mRegExValidation.equals("") || mRegExValidation.equals("null")) return null; else return mRegExValidation; } public String getQuestionIdentifier() { return mQuestionIdentifier; } public void setQuestionIdentifier(String questionIdentifier) { mQuestionIdentifier = questionIdentifier; } public Instrument getInstrument() { return mInstrument; } public void setInstrument(Instrument instrument) { mInstrument = instrument; } public Question getFollowingUpQuestion() { return mFollowingUpQuestion; } public void setFollowingUpQuestion(Question question) { mFollowingUpQuestion = question; } public Long getRemoteId() { return mRemoteId; } public void setRemoteId(Long id) { mRemoteId = id; } public int getInstrumentVersion() { return mInstrumentVersion; } public int getNumberInInstrument() { return mNumberInInstrument; } public int getFollowUpPosition() { return mFollowUpPosition; } public boolean identifiesSurvey() { return mIdentifiesSurvey; } public String getInstructions() { if (mInstructions == null || mInstructions.equals("") || mInstructions.equals("null")) return null; else return mInstructions; } public int getQuestionVersion() { return mQuestionVersion; } public void setInstrumentVersion(int version) { mInstrumentVersion = version; } public void setOptionCount(int num) { mOptionCount = num; } public void setImageCount(int count) { mImageCount = count; } public boolean firstInGrid() { return mFirstInGrid; } public Grid getGrid() { return mGrid; } public boolean belongsToGrid() { if (getGrid() == null) { return false; } else { return true; } } /* * Private */ private static boolean validQuestionType(String questionType) { for (QuestionType type : QuestionType.values()) { if (type.name().equals(questionType)) { return true; } } return false; } private int getOptionCount() { return mOptionCount; } private int getImageCount() { return mImageCount; } private void setNumberInInstrument(int number) { mNumberInInstrument = number; } private void setRegExValidationMessage(String message) { if (message.equals("null") || message.equals("")) mRegExValidationMessage = null; else mRegExValidationMessage = message; } private void setFollowUpPosition(int position) { mFollowUpPosition = position; } private void setIdentifiesSurvey(boolean identifiesSurvey) { mIdentifiesSurvey = identifiesSurvey; } private void setInstructions(String instructions) { mInstructions = instructions; } private void setQuestionVersion(int version) { mQuestionVersion = version; } private void setGrid(Grid grid) { mGrid = grid; } private void setFirstInGrid(boolean firstInGrid) { mFirstInGrid = firstInGrid; } private void setDeleted(boolean deleted) { mDeleted = deleted; } }