Skip to content

Commit 7c64adf

Browse files
authored
Use the orderNumber field for sorting (#222)
* Use the orderNumber field for sorting * Remove duplication for sonar
1 parent b881b50 commit 7c64adf

File tree

11 files changed

+785
-41
lines changed

11 files changed

+785
-41
lines changed

app/schemas/ro.code4.monitorizarevot.data.AppDatabase/4.json

Lines changed: 661 additions & 0 deletions
Large diffs are not rendered by default.

app/src/androidTest/java/ro/code4/monitorizarevot/MigrationTest.kt

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,51 @@ class MigrationTest {
9797
}
9898
}
9999

100+
@Test
101+
fun migrate3To4() {
102+
check3To4MigrationFor(ContentValues().apply {
103+
put("uniqueId", "unique_section")
104+
put("formId", 100)
105+
}, "section", 5, "SELECT * FROM section")
106+
check3To4MigrationFor(ContentValues().apply {
107+
put("id", 100)
108+
put("text", "question_text")
109+
put("code", "question_code")
110+
put("questionType", 0)
111+
put("sectionId", "section_id")
112+
put("hasNotes", false)
113+
}, "question", 7, "SELECT * FROM question")
114+
check3To4MigrationFor(ContentValues().apply {
115+
put("idOption", 100)
116+
put("text", "answer_text")
117+
put("isFreeText", false)
118+
put("questionId", 0)
119+
}, "answer", 5, "SELECT * FROM answer")
120+
}
121+
122+
private fun check3To4MigrationFor(
123+
testValues: ContentValues,
124+
tableName: String,
125+
nrOfColumns: Int,
126+
query: String
127+
) {
128+
helper.createDatabase(TEST_DB, 3).use {
129+
val rowId = it.insert(tableName, SQLiteDatabase.CONFLICT_FAIL, testValues)
130+
assertTrue(rowId > 0)
131+
}
132+
val db = helper.runMigrationsAndValidate(TEST_DB, 4, true, Migrations.MIGRATION_3_4)
133+
val sectionsCursor = db.query(query)
134+
assertNotNull(sectionsCursor)
135+
sectionsCursor.use {
136+
// we have a single row, previously inserted
137+
assertEquals(1, it.count)
138+
assertTrue(it.moveToFirst())
139+
assertEquals(nrOfColumns, it.columnCount)
140+
// check for the new column "orderNumber" and that it has the default value of 0
141+
assertEquals(0, it.getInt(it.getColumnIndex("orderNumber")))
142+
}
143+
}
144+
100145
@Test
101146
@Throws(IOException::class)
102147
fun migrateAll() {

app/src/main/java/ro/code4/monitorizarevot/data/AppDatabase.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import ro.code4.monitorizarevot.data.model.answers.SelectedAnswer
1616

1717
@Database(
1818
entities = [County::class, PollingStation::class, FormDetails::class, Section::class, Question::class, Answer::class, AnsweredQuestion::class, SelectedAnswer::class, Note::class],
19-
version = 3
19+
version = 4
2020
)
2121
@TypeConverters(DateConverter::class)
2222
abstract class AppDatabase : RoomDatabase() {

app/src/main/java/ro/code4/monitorizarevot/data/Migrations.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,16 @@ object Migrations {
2424
}
2525
}
2626

27-
val ALL: Array<Migration> = arrayOf(MIGRATION_1_2, MIGRATION_2_3)
27+
/**
28+
* This migration changes the database to add the new orderNumber for sections, questions and answers.
29+
*/
30+
val MIGRATION_3_4 = object : Migration(3, 4) {
31+
override fun migrate(database: SupportSQLiteDatabase) {
32+
database.execSQL("ALTER TABLE section ADD COLUMN `orderNumber` INTEGER NOT NULL DEFAULT 0")
33+
database.execSQL("ALTER TABLE question ADD COLUMN `orderNumber` INTEGER NOT NULL DEFAULT 0")
34+
database.execSQL("ALTER TABLE answer ADD COLUMN `orderNumber` INTEGER NOT NULL DEFAULT 0")
35+
}
36+
}
37+
38+
val ALL: Array<Migration> = arrayOf(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4)
2839
}

app/src/main/java/ro/code4/monitorizarevot/data/dao/FormsDao.kt

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import androidx.room.*
55
import androidx.room.OnConflictStrategy.REPLACE
66
import io.reactivex.Completable
77
import io.reactivex.Maybe
8+
import io.reactivex.Observable
89
import ro.code4.monitorizarevot.data.model.Answer
910
import ro.code4.monitorizarevot.data.model.FormDetails
1011
import ro.code4.monitorizarevot.data.model.Question
@@ -14,7 +15,7 @@ import ro.code4.monitorizarevot.data.model.answers.SelectedAnswer
1415
import ro.code4.monitorizarevot.data.pojo.AnsweredQuestionPOJO
1516
import ro.code4.monitorizarevot.data.pojo.FormWithSections
1617
import ro.code4.monitorizarevot.data.pojo.SectionWithQuestions
17-
import java.util.*
18+
1819

1920
@Dao
2021
interface FormsDao {
@@ -64,21 +65,21 @@ interface FormsDao {
6465
fun getAnswersFor(
6566
countyCode: String,
6667
pollingStationNumber: Int
67-
): LiveData<List<AnsweredQuestionPOJO>>
68+
): Observable<List<AnsweredQuestionPOJO>>
6869

6970

7071
@Query("SELECT * FROM form_details ORDER BY `order`")
71-
fun getFormsWithSections(): LiveData<List<FormWithSections>>
72+
fun getFormsWithSections(): Observable<List<FormWithSections>>
7273

73-
@Query("SELECT * FROM section where formId=:formId")
74-
fun getSectionsWithQuestions(formId: Int): LiveData<List<SectionWithQuestions>>
74+
@Query("SELECT * FROM section where formId=:formId ORDER BY orderNumber")
75+
fun getSectionsWithQuestions(formId: Int): Observable<List<SectionWithQuestions>>
7576

7677
@Query("SELECT * FROM answered_question WHERE countyCode=:countyCode AND pollingStationNumber=:pollingStationNumber AND formId=:formId")
7778
fun getAnswersForForm(
7879
countyCode: String?,
7980
pollingStationNumber: Int,
8081
formId: Int
81-
): LiveData<List<AnsweredQuestionPOJO>>
82+
): Observable<List<AnsweredQuestionPOJO>>
8283

8384
@Query("SELECT * FROM answered_question WHERE countyCode=:countyCode AND pollingStationNumber=:pollingStationNumber AND formId=:formId AND synced=:synced")
8485
fun getNotSyncedQuestionsForForm(

app/src/main/java/ro/code4/monitorizarevot/data/model/Answer.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ class Answer {
3535
@Ignore
3636
var value: String? = null
3737

38+
@Expose
39+
var orderNumber = 0
40+
3841
override fun equals(other: Any?): Boolean =
3942
other is Answer && idOption == other.idOption && text == other.text &&
4043
isFreeText == other.isFreeText && questionId == other.questionId &&

app/src/main/java/ro/code4/monitorizarevot/data/model/Question.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ class Question {
4747

4848
var hasNotes = false
4949

50+
@Expose
51+
var orderNumber = 0
52+
5053
override fun equals(other: Any?): Boolean =
5154
other is Question && id == other.id && text == other.text && code == other.code &&
5255
questionType == other.questionType &&

app/src/main/java/ro/code4/monitorizarevot/data/model/Section.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ class Section {
3535

3636
var formId: Int = -1
3737

38+
@Expose
39+
var orderNumber = 0
40+
3841
override fun equals(other: Any?): Boolean {
3942
if (other !is Section) {
4043
return false

app/src/main/java/ro/code4/monitorizarevot/repositories/Repository.kt

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,9 @@ class Repository : KoinComponent {
6262
observableApi.onErrorReturnItem(emptyList()),
6363
BiFunction<List<County>, List<County>, List<County>> { dbCounties, apiCounties ->
6464
val areAllApiCountiesInDb = apiCounties.all(dbCounties::contains)
65-
apiCounties.forEach { it.name = it.name.toLowerCase(Locale.getDefault()).capitalize() }
65+
apiCounties.forEach {
66+
it.name = it.name.toLowerCase(Locale.getDefault()).capitalize()
67+
}
6668
return@BiFunction when {
6769
apiCounties.isNotEmpty() && !areAllApiCountiesInDb -> {
6870
db.countyDao().deleteAll()
@@ -113,27 +115,23 @@ class Repository : KoinComponent {
113115
fun getAnswers(
114116
countyCode: String,
115117
pollingStationNumber: Int
116-
): LiveData<List<AnsweredQuestionPOJO>> =
118+
): Observable<List<AnsweredQuestionPOJO>> =
117119
db.formDetailsDao().getAnswersFor(countyCode, pollingStationNumber)
118120

119-
fun getFormsWithQuestions(): LiveData<List<FormWithSections>> =
121+
fun getFormsWithQuestions(): Observable<List<FormWithSections>> =
120122
db.formDetailsDao().getFormsWithSections()
121123

122-
fun getSectionsWithQuestions(formId: Int): LiveData<List<SectionWithQuestions>> =
124+
fun getSectionsWithQuestions(formId: Int): Observable<List<SectionWithQuestions>> =
123125
db.formDetailsDao().getSectionsWithQuestions(formId)
124126

125127
fun getForms(): Observable<Unit> {
126-
127-
val observableDb = db.formDetailsDao().getAllForms().toObservable()
128-
128+
val observableDb = db.formDetailsDao().getFormsWithSections()
129129
val observableApi = apiInterface.getForms()
130-
131130
return Observable.zip(
132131
observableDb.onErrorReturn { null },
133132
observableApi.onErrorReturn { null },
134-
BiFunction<List<FormDetails>?, VersionResponse?, Unit> { dbFormDetails, response ->
133+
BiFunction<List<FormWithSections>?, VersionResponse?, Unit> { dbFormDetails, response ->
135134
processFormDetailsData(dbFormDetails, response)
136-
137135
})
138136
}
139137

@@ -164,7 +162,7 @@ class Repository : KoinComponent {
164162
}
165163

166164
private fun processFormDetailsData(
167-
dbFormDetails: List<FormDetails>?,
165+
dbFormDetails: List<FormWithSections>?,
168166
response: VersionResponse?
169167
) {
170168
if (response == null) {
@@ -176,17 +174,17 @@ class Repository : KoinComponent {
176174
return
177175
}
178176
if (apiFormDetails.size < dbFormDetails.size) {
179-
dbFormDetails.minus(apiFormDetails).also { diff ->
177+
dbFormDetails.map { it.form }.minus(apiFormDetails).also { diff ->
180178
if (diff.isNotEmpty()) {
181179
deleteFormDetails(*diff.map { it }.toTypedArray())
182180
}
183181
}
184182
}
185183
apiFormDetails.forEach { apiForm ->
186-
val dbForm = dbFormDetails.find { it.id == apiForm.id }
187-
if (dbForm != null && (apiForm.formVersion != dbForm.formVersion ||
188-
apiForm.order != dbForm.order)) {
189-
deleteFormDetails(dbForm)
184+
val dbForm = dbFormDetails.find { it.form.id == apiForm.id }
185+
if (dbForm != null && (apiForm.formVersion != dbForm.form.formVersion ||
186+
apiForm.order != dbForm.form.order)) {
187+
deleteFormDetails(dbForm.form)
190188
saveFormDetails(apiForm)
191189
}
192190
if (dbForm == null) {
@@ -219,7 +217,7 @@ class Repository : KoinComponent {
219217
countyCode: String?,
220218
pollingStationNumber: Int,
221219
formId: Int
222-
): LiveData<List<AnsweredQuestionPOJO>> {
220+
): Observable<List<AnsweredQuestionPOJO>> {
223221
return db.formDetailsDao().getAnswersForForm(countyCode, pollingStationNumber, formId)
224222
}
225223

app/src/main/java/ro/code4/monitorizarevot/ui/forms/FormsViewModel.kt

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import androidx.lifecycle.LiveData
66
import androidx.lifecycle.MediatorLiveData
77
import androidx.lifecycle.MutableLiveData
88
import com.google.firebase.remoteconfig.FirebaseRemoteConfig
9+
import io.reactivex.Observable
910
import io.reactivex.android.schedulers.AndroidSchedulers
11+
import io.reactivex.functions.BiFunction
1012
import io.reactivex.schedulers.Schedulers
1113
import ro.code4.monitorizarevot.adapters.helper.AddNoteListItem
1214
import ro.code4.monitorizarevot.adapters.helper.FormListItem
@@ -18,7 +20,6 @@ import ro.code4.monitorizarevot.data.pojo.FormWithSections
1820
import ro.code4.monitorizarevot.data.pojo.PollingStationInfo
1921
import ro.code4.monitorizarevot.helper.Constants.REMOTE_CONFIG_FILTER_DIASPORA_FORMS
2022
import ro.code4.monitorizarevot.helper.completedPollingStationConfig
21-
import ro.code4.monitorizarevot.helper.zipLiveData
2223
import ro.code4.monitorizarevot.ui.base.BaseFormViewModel
2324

2425
class FormsViewModel : BaseFormViewModel() {
@@ -55,12 +56,15 @@ class FormsViewModel : BaseFormViewModel() {
5556
update()
5657
}
5758

58-
zipLiveData(
59+
disposables.add(Observable.combineLatest(
5960
repository.getAnswers(countyCode, pollingStationNumber),
60-
repository.getFormsWithQuestions()
61-
).observeForever {
61+
repository.getFormsWithQuestions(),
62+
BiFunction<List<AnsweredQuestionPOJO>, List<FormWithSections>, Pair<List<AnsweredQuestionPOJO>, List<FormWithSections>>> {
63+
t1, t2 -> Pair(t1, t2)
64+
}
65+
).subscribe {
6266
processList(it.first, it.second)
63-
}
67+
})
6468
}
6569

6670
fun forms(): LiveData<ArrayList<ListItem>> = formsLiveData

app/src/main/java/ro/code4/monitorizarevot/ui/forms/questions/BaseQuestionViewModel.kt

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,48 @@ package ro.code4.monitorizarevot.ui.forms.questions
22

33
import androidx.lifecycle.LiveData
44
import androidx.lifecycle.MutableLiveData
5+
import io.reactivex.Observable
6+
import io.reactivex.android.schedulers.AndroidSchedulers
7+
import io.reactivex.functions.BiFunction
8+
import io.reactivex.schedulers.Schedulers
59
import ro.code4.monitorizarevot.adapters.helper.ListItem
610
import ro.code4.monitorizarevot.data.model.FormDetails
711
import ro.code4.monitorizarevot.data.pojo.AnsweredQuestionPOJO
812
import ro.code4.monitorizarevot.data.pojo.SectionWithQuestions
9-
import ro.code4.monitorizarevot.helper.zipLiveData
1013
import ro.code4.monitorizarevot.ui.base.BaseFormViewModel
1114

1215
abstract class BaseQuestionViewModel : BaseFormViewModel() {
13-
1416
val questionsLiveData = MutableLiveData<ArrayList<ListItem>>()
15-
1617
var selectedFormId: Int = -1
17-
1818
fun questions(): LiveData<ArrayList<ListItem>> = questionsLiveData
1919

2020
private fun getQuestions(formId: Int) {
21-
2221
selectedFormId = formId
23-
zipLiveData(
22+
disposables.add(Observable.combineLatest(
2423
repository.getSectionsWithQuestions(formId),
25-
repository.getAnswersForForm(countyCode, pollingStationNumber, formId)
26-
).observeForever {
27-
processList(it.first, it.second)
28-
}
29-
24+
repository.getAnswersForForm(countyCode, pollingStationNumber, formId),
25+
BiFunction<List<SectionWithQuestions>, List<AnsweredQuestionPOJO>, Pair<List<SectionWithQuestions>, List<AnsweredQuestionPOJO>>> { t1, t2 ->
26+
Pair(t1, t2)
27+
}
28+
).subscribeOn(Schedulers.computation())
29+
.map { dataPair ->
30+
// sort on orderNumber the sections along with their questions and answers
31+
val sortedSections = dataPair.first.sortedBy { it.section.orderNumber }
32+
for (sortedSection in sortedSections) {
33+
val sortedQuestions =
34+
sortedSection.questions.sortedBy { it.question.orderNumber }
35+
for (sortedQuestion in sortedQuestions) {
36+
sortedQuestion.answers = sortedQuestion.answers?.sortedBy { it.orderNumber }
37+
}
38+
sortedSection.questions = sortedQuestions
39+
}
40+
Pair(sortedSections, dataPair.second)
41+
}.observeOn(AndroidSchedulers.mainThread())
42+
.subscribe { result -> processList(result.first, result.second) })
3043
}
3144

45+
// TODO this method should also run on a background thread to avoid doing lengthy
46+
// operations(like sorting or iterating over large collections) on the main thread
3247
abstract fun processList(
3348
sections: List<SectionWithQuestions>,
3449
answersForForm: List<AnsweredQuestionPOJO>

0 commit comments

Comments
 (0)