mirror of
https://github.com/hmalik144/Farmr.git
synced 2026-01-31 02:41:49 +00:00
- room database migration in progress
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'org.jetbrains.kotlin.android'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
def relStorePassword = System.getenv("RELEASE_STORE_PASSWORD")
|
||||
def relKeyPassword = System.getenv("RELEASE_KEY_PASSWORD")
|
||||
@@ -17,6 +18,12 @@ android {
|
||||
versionName "2.1"
|
||||
testInstrumentationRunner 'com.appttude.h_mal.farmr.application.TestRunner'
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
javaCompileOptions {
|
||||
annotationProcessorOptions {
|
||||
arguments += ["room.schemaLocation":
|
||||
"$projectDir/schemas".toString()]
|
||||
}
|
||||
}
|
||||
}
|
||||
signingConfigs {
|
||||
release {
|
||||
@@ -33,6 +40,10 @@ android {
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
sourceSets {
|
||||
androidTest.assets.srcDirs +=
|
||||
files("$projectDir/schemas".toString())
|
||||
}
|
||||
useLibrary 'android.test.mock'
|
||||
}
|
||||
|
||||
@@ -77,6 +88,8 @@ dependencies {
|
||||
/ * Room database * /
|
||||
runtimeOnly "androidx.room:room-runtime:$ROOM_VERSION"
|
||||
implementation "androidx.room:room-ktx:$ROOM_VERSION"
|
||||
kapt "androidx.room:room-compiler:$ROOM_VERSION"
|
||||
androidTestImplementation "android.arch.persistence.room:testing:1.1.1"
|
||||
/ *Kodein Dependency Injection * /
|
||||
implementation "org.kodein.di:kodein-di-generic-jvm:$KODEIN_VERSION"
|
||||
implementation "org.kodein.di:kodein-di-framework-android-x:$KODEIN_VERSION"
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
package com.appttude.h_mal.farmr.application
|
||||
|
||||
import androidx.room.Room
|
||||
import androidx.test.espresso.IdlingRegistry
|
||||
import androidx.test.espresso.idling.CountingIdlingResource
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.appttude.h_mal.farmr.base.BaseApplication
|
||||
import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase
|
||||
import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider
|
||||
import com.appttude.h_mal.farmr.data.room.AppDatabase
|
||||
import com.appttude.h_mal.farmr.model.Shift
|
||||
|
||||
class TestAppClass : BaseApplication() {
|
||||
private val idlingResources = CountingIdlingResource("Data_loader")
|
||||
|
||||
lateinit var database: LegacyDatabase
|
||||
lateinit var database: AppDatabase
|
||||
lateinit var preferenceProvider: PreferenceProvider
|
||||
|
||||
override fun onCreate() {
|
||||
@@ -19,9 +21,9 @@ class TestAppClass : BaseApplication() {
|
||||
IdlingRegistry.getInstance().register(idlingResources)
|
||||
}
|
||||
|
||||
override fun createDatabase(): LegacyDatabase {
|
||||
database =
|
||||
LegacyDatabase(InstrumentationRegistry.getInstrumentation().context.contentResolver)
|
||||
override fun createDatabase(): AppDatabase {
|
||||
database = Room.inMemoryDatabaseBuilder(this, AppDatabase::class.java)
|
||||
.build()
|
||||
return database
|
||||
}
|
||||
|
||||
@@ -30,9 +32,9 @@ class TestAppClass : BaseApplication() {
|
||||
return preferenceProvider
|
||||
}
|
||||
|
||||
fun addToDatabase(shift: Shift) = database.insertShiftDataIntoDatabase(shift)
|
||||
fun addShiftsToDatabase(shifts: List<Shift>) = shifts.forEach { addToDatabase(it) }
|
||||
fun clearDatabase() = database.deleteAllShiftsInDatabase()
|
||||
fun addToDatabase(shift: Shift) = database.getShiftDao().upsertFullShift(shift.convertToShiftEntity())
|
||||
fun addShiftsToDatabase(shifts: List<Shift>) = shifts.map { it.convertToShiftEntity() }.let { database.getShiftDao().upsertListOfFullShift(it) }
|
||||
fun clearDatabase() = database.getShiftDao().deleteAllShifts()
|
||||
fun cleanPrefs() = preferenceProvider.clearPrefs()
|
||||
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.appttude.h_mal.farmr.data.room
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import androidx.room.testing.MigrationTestHelper
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract
|
||||
import com.appttude.h_mal.farmr.data.legacydb.ShiftsDbHelper
|
||||
import com.appttude.h_mal.farmr.data.legacydb.ShiftsDbHelper.Companion.DATABASE_NAME
|
||||
import com.appttude.h_mal.farmr.data.room.migrations.MIGRATION_4_5
|
||||
import com.appttude.h_mal.farmr.model.ShiftType
|
||||
import com.appttude.h_mal.farmr.ui.utils.getShifts
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.io.IOException
|
||||
|
||||
|
||||
class RoomMigrationTest {
|
||||
private val TEST_DB = "migration-test"
|
||||
|
||||
@get:Rule
|
||||
val testHelper = MigrationTestHelper(
|
||||
InstrumentationRegistry.getInstrumentation(),
|
||||
AppDatabase::class.java.canonicalName
|
||||
)
|
||||
|
||||
@Test
|
||||
@Throws(IOException::class)
|
||||
fun migrationFrom2To3_containsCorrectData() {
|
||||
// Create the database in version 4
|
||||
val db = testHelper.createDatabase(TEST_DB, 4)
|
||||
// Insert some data
|
||||
getShifts().forEach {
|
||||
db.insert(
|
||||
ShiftsContract.ShiftsEntry.TABLE_NAME, SQLiteDatabase.CONFLICT_REPLACE, ContentValues().apply {
|
||||
put(ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TYPE, it.type.type)
|
||||
put(ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DESCRIPTION, it.description)
|
||||
put(ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DATE, it.date)
|
||||
put(ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TIME_IN, it.timeIn ?: "00:00")
|
||||
put(ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TIME_OUT, it.timeOut ?: "00:00")
|
||||
put(ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DURATION, it.duration ?: 0.00f)
|
||||
put(ShiftsContract.ShiftsEntry.COLUMN_SHIFT_BREAK, it.breakMins ?: 0)
|
||||
put(ShiftsContract.ShiftsEntry.COLUMN_SHIFT_UNIT, it.units ?: 0.00f)
|
||||
put(ShiftsContract.ShiftsEntry.COLUMN_SHIFT_PAYRATE, it.rateOfPay)
|
||||
put(ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TOTALPAY, it.totalPay)
|
||||
})
|
||||
}
|
||||
//Prepare for the next version
|
||||
db.close()
|
||||
|
||||
// Re-open the database with version 5 and provide MIGRATION_4_5
|
||||
// and MIGRATION_4_5 as the migration process.
|
||||
testHelper.runMigrationsAndValidate(
|
||||
TEST_DB, 5,
|
||||
true, MIGRATION_4_5
|
||||
)
|
||||
|
||||
// MigrationTestHelper automatically verifies the schema
|
||||
//changes, but not the data validity
|
||||
// Validate that the data was migrated properly.
|
||||
val dbUser: User = getMigratedRoomDatabase().userDao().getUser()
|
||||
assertEquals(dbUser.getId(), USER.getId())
|
||||
assertEquals(dbUser.getUserName(), USER.getUserName())
|
||||
// The date was missing in version 2, so it should be null in
|
||||
//version 3
|
||||
assertEquals(dbUser.getDate(), null)
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import android.app.Application
|
||||
import com.appttude.h_mal.farmr.data.RepositoryImpl
|
||||
import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase
|
||||
import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider
|
||||
import com.appttude.h_mal.farmr.data.room.AppDatabase
|
||||
import com.appttude.h_mal.farmr.viewmodel.ApplicationViewModelFactory
|
||||
import org.kodein.di.Kodein
|
||||
import org.kodein.di.KodeinAware
|
||||
@@ -26,6 +27,6 @@ abstract class BaseApplication() : Application(), KodeinAware {
|
||||
bind() from provider { ApplicationViewModelFactory(instance()) }
|
||||
}
|
||||
|
||||
abstract fun createDatabase(): LegacyDatabase
|
||||
abstract fun createDatabase(): AppDatabase
|
||||
abstract fun createPrefs(): PreferenceProvider
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.appttude.h_mal.farmr.data
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import com.appttude.h_mal.farmr.data.legacydb.ShiftObject
|
||||
import com.appttude.h_mal.farmr.data.room.entity.ShiftEntity
|
||||
import com.appttude.h_mal.farmr.model.Order
|
||||
import com.appttude.h_mal.farmr.model.Shift
|
||||
import com.appttude.h_mal.farmr.model.Sortable
|
||||
@@ -8,8 +10,8 @@ import com.appttude.h_mal.farmr.model.Sortable
|
||||
interface Repository {
|
||||
fun insertShiftIntoDatabase(shift: Shift): Boolean
|
||||
fun updateShiftIntoDatabase(id: Long, shift: Shift): Boolean
|
||||
fun readShiftsFromDatabase(): List<ShiftObject>?
|
||||
fun readSingleShiftFromDatabase(id: Long): ShiftObject?
|
||||
fun readShiftsFromDatabase(): LiveData<List<ShiftEntity>>
|
||||
fun readSingleShiftFromDatabase(id: Long): ShiftEntity?
|
||||
fun deleteSingleShiftFromDatabase(id: Long): Boolean
|
||||
fun deleteAllShiftsFromDatabase(): Boolean
|
||||
fun retrieveSortAndOrderFromPref(): Pair<Sortable?, Order?>
|
||||
|
||||
@@ -1,50 +1,77 @@
|
||||
package com.appttude.h_mal.farmr.data
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.Room
|
||||
import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase
|
||||
import com.appttude.h_mal.farmr.data.legacydb.ShiftObject
|
||||
import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract
|
||||
import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider
|
||||
import com.appttude.h_mal.farmr.data.room.AppDatabase
|
||||
import com.appttude.h_mal.farmr.data.room.entity.ShiftEntity
|
||||
import com.appttude.h_mal.farmr.model.Order
|
||||
import com.appttude.h_mal.farmr.model.Shift
|
||||
import com.appttude.h_mal.farmr.model.Sortable
|
||||
import com.appttude.h_mal.farmr.utils.dateStringIsValid
|
||||
import com.appttude.h_mal.farmr.utils.timeStringIsValid
|
||||
|
||||
class RepositoryImpl(
|
||||
private val legacyDatabase: LegacyDatabase,
|
||||
roomDatabase: AppDatabase,
|
||||
private val preferenceProvider: PreferenceProvider
|
||||
): Repository {
|
||||
|
||||
private val shiftDao = roomDatabase.getShiftDao()
|
||||
|
||||
override fun insertShiftIntoDatabase(shift: Shift): Boolean {
|
||||
return legacyDatabase.insertShiftDataIntoDatabase(shift) != null
|
||||
val shiftEntity = shift.convertToShiftEntity()
|
||||
return shiftDao.upsertFullShift(shiftEntity) > 0
|
||||
}
|
||||
|
||||
override fun updateShiftIntoDatabase(id: Long, shift: Shift): Boolean {
|
||||
return legacyDatabase.updateShiftDataIntoDatabase(
|
||||
id = id,
|
||||
typeString = shift.type.type,
|
||||
descriptionString = shift.description,
|
||||
dateString = shift.date,
|
||||
timeInString = shift.timeIn ?: "",
|
||||
timeOutString = shift.timeOut ?: "",
|
||||
duration = shift.duration ?: 0f,
|
||||
breaks = shift.breakMins ?: 0,
|
||||
units = shift.units ?: 0f,
|
||||
payRate = shift.rateOfPay,
|
||||
totalPay = shift.totalPay
|
||||
) == 1
|
||||
if (shift.description.isBlank() || shift.description.trim().length < 3) {
|
||||
throw IllegalArgumentException("description required")
|
||||
}
|
||||
if (!shift.date.dateStringIsValid()) {
|
||||
throw IllegalArgumentException("date required")
|
||||
}
|
||||
shift.timeIn?.takeIf { !it.timeStringIsValid() }?.let {
|
||||
throw IllegalArgumentException("time in required")
|
||||
}
|
||||
shift.timeOut?.takeIf { !it.timeStringIsValid() }?.let {
|
||||
throw IllegalArgumentException("time out required")
|
||||
}
|
||||
shift.breakMins?.takeIf { it < 0 }?.let {
|
||||
throw IllegalArgumentException("break required")
|
||||
}
|
||||
if (shift.timeIn != null || shift.timeOut != null) {
|
||||
if (shift.duration == null) throw IllegalArgumentException("Duration required")
|
||||
}
|
||||
shift.units?.takeIf { it < 0 }?.let {
|
||||
throw IllegalArgumentException("Units required")
|
||||
}
|
||||
if (shift.rateOfPay < 0) {
|
||||
throw IllegalArgumentException("Rate of pay required")
|
||||
}
|
||||
if (shift.totalPay < 0) {
|
||||
throw IllegalArgumentException("Total pay required")
|
||||
}
|
||||
val shiftEntity = shift.convertToShiftEntity(id)
|
||||
return shiftDao.upsertFullShift(shiftEntity) > 0
|
||||
}
|
||||
|
||||
override fun readShiftsFromDatabase(): List<ShiftObject>? {
|
||||
return legacyDatabase.readShiftsFromDatabase()
|
||||
override fun readShiftsFromDatabase(): LiveData<List<ShiftEntity>> {
|
||||
return shiftDao.getAllFullShift()
|
||||
}
|
||||
|
||||
override fun readSingleShiftFromDatabase(id: Long): ShiftObject? {
|
||||
return legacyDatabase.readSingleShiftWithId(id)
|
||||
override fun readSingleShiftFromDatabase(id: Long): ShiftEntity? {
|
||||
return shiftDao.getCurrentFullShiftSingle(id)
|
||||
}
|
||||
|
||||
override fun deleteSingleShiftFromDatabase(id: Long): Boolean {
|
||||
return legacyDatabase.deleteSingleShift(id) == 1
|
||||
return shiftDao.deleteShift(id) == 1
|
||||
}
|
||||
|
||||
override fun deleteAllShiftsFromDatabase(): Boolean {
|
||||
return legacyDatabase.deleteAllShiftsInDatabase() > 0
|
||||
return shiftDao.deleteAllShifts() > 0
|
||||
}
|
||||
|
||||
override fun retrieveSortAndOrderFromPref(): Pair<Sortable?, Order?> {
|
||||
|
||||
@@ -33,10 +33,10 @@ class ShiftsDbHelper(context: Context?) : SQLiteOpenHelper(context, DATABASE_NAM
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DATABASE_NAME = "shifts.db"
|
||||
const val DATABASE_NAME = "shifts.db"
|
||||
private const val DATABASE_VERSION = 4
|
||||
private const val DEFAULT_TEXT = "Hourly"
|
||||
private const val SQL_CREATE_PRODUCTS_TABLE_2 = ("CREATE TABLE " + ShiftsEntry.TABLE_NAME_EXPORT + " ("
|
||||
const val SQL_CREATE_PRODUCTS_TABLE_2 = ("CREATE TABLE " + ShiftsEntry.TABLE_NAME_EXPORT + " ("
|
||||
+ ShiftsEntry.COLUMN_SHIFT_DESCRIPTION + " TEXT NOT NULL, "
|
||||
+ ShiftsEntry.COLUMN_SHIFT_DATE + " DATE NOT NULL, "
|
||||
+ ShiftsEntry.COLUMN_SHIFT_TIME_IN + " TIME NOT NULL, "
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.appttude.h_mal.farmr.data.room
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import com.appttude.h_mal.farmr.data.legacydb.ShiftsDbHelper
|
||||
import com.appttude.h_mal.farmr.data.legacydb.ShiftsDbHelper.Companion.DATABASE_NAME
|
||||
import com.appttude.h_mal.farmr.data.room.converters.DateConverter
|
||||
import com.appttude.h_mal.farmr.data.room.converters.TimeConverter
|
||||
import com.appttude.h_mal.farmr.data.room.entity.ShiftEntity
|
||||
import com.appttude.h_mal.farmr.data.room.migrations.MIGRATION_4_5
|
||||
|
||||
@Database(
|
||||
entities = [ShiftEntity::class],
|
||||
version = 5,
|
||||
exportSchema = true
|
||||
)
|
||||
@TypeConverters(DateConverter::class, TimeConverter::class)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
|
||||
abstract fun getShiftDao(): ShiftDao
|
||||
|
||||
companion object {
|
||||
|
||||
@Volatile
|
||||
private var instance: AppDatabase? = null
|
||||
private val LOCK = Any()
|
||||
|
||||
// create an instance of room database or use previously created instance
|
||||
operator fun invoke(context: Context) = instance ?: synchronized(LOCK) {
|
||||
instance ?: buildDatabase(context).also {
|
||||
instance = it
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildDatabase(context: Context) =
|
||||
Room.databaseBuilder(
|
||||
context.applicationContext,
|
||||
AppDatabase::class.java,
|
||||
DATABASE_NAME
|
||||
).addMigrations(MIGRATION_4_5)
|
||||
.addTypeConverter(DateConverter())
|
||||
.addTypeConverter(TimeConverter())
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.appttude.h_mal.farmr.data.room
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.DeleteTable
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Update
|
||||
import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry
|
||||
import com.appttude.h_mal.farmr.data.room.entity.ShiftEntity
|
||||
|
||||
@Dao
|
||||
interface ShiftDao {
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun upsertFullShift(item: ShiftEntity): Long
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun upsertListOfFullShift(items: List<ShiftEntity>)
|
||||
|
||||
@Query("SELECT * FROM shifts WHERE ${ShiftsEntry._ID} = :shiftId LIMIT 1")
|
||||
fun getCurrentFullShift(shiftId: Long): LiveData<ShiftEntity>
|
||||
|
||||
@Query("SELECT * FROM shifts WHERE ${ShiftsEntry._ID} = :shiftId LIMIT 1")
|
||||
fun getCurrentFullShiftSingle(shiftId: Long): ShiftEntity?
|
||||
|
||||
@Query("SELECT * FROM shifts")
|
||||
fun getAllFullShift(): LiveData<List<ShiftEntity>>
|
||||
|
||||
@Query("DELETE FROM shifts WHERE ${ShiftsEntry._ID} = :shiftId")
|
||||
fun deleteShift(shiftId: Long): Int
|
||||
|
||||
@Query("DELETE FROM shifts")
|
||||
fun deleteAllShifts(): Int
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.appttude.h_mal.farmr.data.room.converters
|
||||
|
||||
import androidx.room.ProvidedTypeConverter
|
||||
import androidx.room.TypeConverter
|
||||
import java.sql.Date
|
||||
|
||||
@ProvidedTypeConverter
|
||||
class DateConverter {
|
||||
@TypeConverter
|
||||
fun toDate(dateString: String): Date {
|
||||
// Convert yyyy-MM-dd into date
|
||||
return Date.valueOf(dateString)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun fromDate(date: Date): String {
|
||||
return date.toString()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.appttude.h_mal.farmr.data.room.converters
|
||||
|
||||
import androidx.room.ProvidedTypeConverter
|
||||
import androidx.room.TypeConverter
|
||||
import java.sql.Date
|
||||
import java.sql.Time
|
||||
import java.time.Instant
|
||||
|
||||
@ProvidedTypeConverter
|
||||
class TimeConverter {
|
||||
@TypeConverter
|
||||
fun toTime(timeString: String?): Time {
|
||||
// Convert HH:mm into date
|
||||
timeString?.let { return Time.valueOf("$timeString:00") }
|
||||
return Time.valueOf("00:00:00")
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun fromTime(time: Time): String {
|
||||
// Return string in format of HH:mm
|
||||
return time.toString().substring(0,5)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.appttude.h_mal.farmr.data.room.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.appttude.h_mal.farmr.data.legacydb.ShiftObject
|
||||
import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry
|
||||
import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.TABLE_NAME
|
||||
import com.appttude.h_mal.farmr.model.ShiftType
|
||||
import java.sql.Date
|
||||
import java.sql.Time
|
||||
|
||||
|
||||
@Entity(tableName = TABLE_NAME)
|
||||
data class ShiftEntity(
|
||||
@ColumnInfo(name = ShiftsEntry.COLUMN_SHIFT_DESCRIPTION) val description: String,
|
||||
@ColumnInfo(name = ShiftsEntry.COLUMN_SHIFT_DATE, typeAffinity = ColumnInfo.UNDEFINED) val date: Date,
|
||||
@ColumnInfo(name = ShiftsEntry.COLUMN_SHIFT_TIME_IN, typeAffinity = ColumnInfo.TEXT) val timeIn: Time,
|
||||
@ColumnInfo(name = ShiftsEntry.COLUMN_SHIFT_TIME_OUT, typeAffinity = ColumnInfo.TEXT) val timeOut: Time,
|
||||
@ColumnInfo(name = ShiftsEntry.COLUMN_SHIFT_BREAK) val breakMins: Int? = 0,
|
||||
@ColumnInfo(name = ShiftsEntry.COLUMN_SHIFT_DURATION, typeAffinity = ColumnInfo.REAL, defaultValue = "0") val duration: Float = 0f,
|
||||
@ColumnInfo(name = ShiftsEntry.COLUMN_SHIFT_TYPE) val type: String,
|
||||
@ColumnInfo(name = ShiftsEntry.COLUMN_SHIFT_UNIT, typeAffinity = ColumnInfo.REAL) val units: Float? = 0f,
|
||||
@ColumnInfo(name = ShiftsEntry.COLUMN_SHIFT_PAYRATE, typeAffinity = ColumnInfo.REAL) val payRate: Float? = 0f,
|
||||
@ColumnInfo(name = ShiftsEntry.COLUMN_SHIFT_TOTALPAY, typeAffinity = ColumnInfo.REAL) val totalPay: Float? = 0f,
|
||||
@PrimaryKey
|
||||
@ColumnInfo(name = ShiftsEntry._ID) val id: Long? = 0,
|
||||
) {
|
||||
fun convertToShiftObject(): ShiftObject {
|
||||
return ShiftObject(
|
||||
id ?: 0, type, description, date.toString(), timeIn.toString(), timeOut.toString(), duration, breakMins ?: 0, units ?: 0f, payRate ?: 0f, totalPay ?: 0f
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.appttude.h_mal.farmr.data.room.migrations
|
||||
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import com.appttude.h_mal.farmr.data.legacydb.ShiftsDbHelper
|
||||
|
||||
val MIGRATION_4_5 = object : Migration(4, 5) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("DROP TABLE IF EXISTS '" + ShiftsDbHelper.SQL_CREATE_PRODUCTS_TABLE_2 + "'");
|
||||
database.execSQL("ALTER TABLE `shifts` ADD COLUMN `date` TEXT NOT NULL")
|
||||
|
||||
// CREATE TABLE IF NOT EXISTS (`description` TEXT NOT NULL, , `timein` TEXT NOT NULL, `timeout` TEXT NOT NULL, `break` INTEGER, `duration` REAL NOT NULL DEFAULT 0, `shifttype` TEXT NOT NULL, `unit` REAL, `payrate` REAL, `totalpay` REAL, `_id` INTEGER, PRIMARY KEY(`_id`))
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
package com.appttude.h_mal.farmr.di
|
||||
|
||||
import androidx.room.RoomDatabase
|
||||
import com.appttude.h_mal.farmr.base.BaseApplication
|
||||
import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase
|
||||
import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider
|
||||
import com.appttude.h_mal.farmr.data.room.AppDatabase
|
||||
|
||||
class ShiftApplication: BaseApplication() {
|
||||
|
||||
override fun createDatabase(): LegacyDatabase {
|
||||
return LegacyDatabase(contentResolver)
|
||||
override fun createDatabase(): AppDatabase {
|
||||
return AppDatabase(this)
|
||||
}
|
||||
|
||||
override fun createPrefs() = PreferenceProvider(this)
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
package com.appttude.h_mal.farmr.model
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity
|
||||
data class EntityItem(
|
||||
@PrimaryKey(autoGenerate = false)
|
||||
val id: String,
|
||||
val shift: Shift
|
||||
)
|
||||
@@ -1,5 +1,8 @@
|
||||
package com.appttude.h_mal.farmr.model
|
||||
|
||||
import com.appttude.h_mal.farmr.data.room.converters.DateConverter
|
||||
import com.appttude.h_mal.farmr.data.room.converters.TimeConverter
|
||||
import com.appttude.h_mal.farmr.data.room.entity.ShiftEntity
|
||||
import com.appttude.h_mal.farmr.utils.calculateDuration
|
||||
import com.appttude.h_mal.farmr.utils.formatToTwoDp
|
||||
|
||||
@@ -15,6 +18,39 @@ data class Shift(
|
||||
val rateOfPay: Float,
|
||||
val totalPay: Float
|
||||
) {
|
||||
|
||||
fun convertToShiftEntity(): ShiftEntity {
|
||||
val timeConverter = TimeConverter()
|
||||
return ShiftEntity(
|
||||
description = description,
|
||||
type = type.type,
|
||||
date = DateConverter().toDate(date),
|
||||
timeIn = timeConverter.toTime(timeIn),
|
||||
timeOut = timeConverter.toTime(timeOut),
|
||||
duration = duration ?: 0f,
|
||||
breakMins = breakMins ?: 0,
|
||||
units = units ?: 0f,
|
||||
payRate = rateOfPay,
|
||||
totalPay = totalPay
|
||||
)
|
||||
}
|
||||
|
||||
fun convertToShiftEntity(id: Long): ShiftEntity {
|
||||
val timeConverter = TimeConverter()
|
||||
return ShiftEntity(
|
||||
id = id,
|
||||
description = description,
|
||||
type = type.type,
|
||||
date = DateConverter().toDate(date),
|
||||
timeIn = timeConverter.toTime(timeIn),
|
||||
timeOut = timeConverter.toTime(timeOut),
|
||||
duration = duration ?: 0f,
|
||||
breakMins = breakMins ?: 0,
|
||||
units = units ?: 0f,
|
||||
payRate = rateOfPay,
|
||||
totalPay = totalPay
|
||||
)
|
||||
}
|
||||
companion object {
|
||||
// Invocation for Hourly
|
||||
operator fun invoke(
|
||||
@@ -60,6 +96,4 @@ data class Shift(
|
||||
(units * rateOfPay).formatToTwoDp()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -13,6 +13,8 @@ import androidx.core.widget.doAfterTextChanged
|
||||
import com.appttude.h_mal.farmr.R
|
||||
import com.appttude.h_mal.farmr.base.BackPressedListener
|
||||
import com.appttude.h_mal.farmr.base.BaseFragment
|
||||
import com.appttude.h_mal.farmr.data.room.converters.DateConverter
|
||||
import com.appttude.h_mal.farmr.data.room.converters.TimeConverter
|
||||
import com.appttude.h_mal.farmr.model.ShiftType
|
||||
import com.appttude.h_mal.farmr.model.Success
|
||||
import com.appttude.h_mal.farmr.utils.ID
|
||||
@@ -31,6 +33,9 @@ import com.appttude.h_mal.farmr.viewmodel.SubmissionViewModel
|
||||
class FragmentAddItem : BaseFragment<SubmissionViewModel>(R.layout.fragment_add_item),
|
||||
RadioGroup.OnCheckedChangeListener, BackPressedListener {
|
||||
|
||||
private val dateConverter = DateConverter()
|
||||
private val timeConverter = TimeConverter()
|
||||
|
||||
private lateinit var mHourlyRadioButton: RadioButton
|
||||
private lateinit var mPieceRadioButton: RadioButton
|
||||
private lateinit var mLocationEditText: EditText
|
||||
@@ -126,41 +131,41 @@ class FragmentAddItem : BaseFragment<SubmissionViewModel>(R.layout.fragment_add_
|
||||
// Since we are editing a shift lets load the shift data into the views
|
||||
viewModel.getCurrentShift(arguments!!.getLong(ID))?.run {
|
||||
mLocationEditText.setText(description)
|
||||
mDateEditText.setText(date)
|
||||
mDateEditText.setText(dateConverter.fromDate(date))
|
||||
|
||||
// Set types
|
||||
mType = ShiftType.getEnumByType(type)
|
||||
mDescription = description
|
||||
mDate = date
|
||||
mPayRate = rateOfPay
|
||||
mDate = dateConverter.fromDate(date)
|
||||
mPayRate = payRate!!
|
||||
|
||||
when (ShiftType.getEnumByType(type)) {
|
||||
ShiftType.HOURLY -> {
|
||||
mHourlyRadioButton.isChecked = true
|
||||
mPieceRadioButton.isChecked = false
|
||||
mTimeInEditText.setText(timeIn)
|
||||
mTimeOutEditText.setText(timeOut)
|
||||
mTimeInEditText.setText(timeConverter.fromTime(timeIn))
|
||||
mTimeOutEditText.setText(timeConverter.fromTime(timeOut))
|
||||
mBreakEditText.setText(breakMins.toString())
|
||||
val durationText = "${duration.formatToTwoDpString()} Hours"
|
||||
mDurationTextView.text = durationText
|
||||
|
||||
// Set fields
|
||||
mTimeIn = timeIn
|
||||
mTimeOut = timeOut
|
||||
mTimeIn = timeConverter.fromTime(timeIn)
|
||||
mTimeOut = timeConverter.fromTime(timeOut)
|
||||
mBreaks = breakMins
|
||||
}
|
||||
|
||||
ShiftType.PIECE -> {
|
||||
mHourlyRadioButton.isChecked = false
|
||||
mPieceRadioButton.isChecked = true
|
||||
mUnitEditText.setText(units.formatToTwoDpString())
|
||||
mUnitEditText.setText(units?.formatToTwoDpString())
|
||||
|
||||
// Set piece rate units
|
||||
mUnits = units
|
||||
}
|
||||
}
|
||||
mPayRateEditText.setText(rateOfPay.formatAsCurrencyString())
|
||||
mTotalPayTextView.text = totalPay.formatAsCurrencyString()
|
||||
mPayRateEditText.setText(payRate.formatAsCurrencyString())
|
||||
mTotalPayTextView.text = totalPay?.formatAsCurrencyString()
|
||||
|
||||
calculateTotalPay()
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.appttude.h_mal.farmr.viewmodel
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import com.appttude.h_mal.farmr.data.Repository
|
||||
import com.appttude.h_mal.farmr.data.legacydb.ShiftObject
|
||||
@@ -16,6 +15,7 @@ import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_
|
||||
import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TYPE
|
||||
import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_UNIT
|
||||
import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry._ID
|
||||
import com.appttude.h_mal.farmr.data.room.entity.ShiftEntity
|
||||
import com.appttude.h_mal.farmr.model.Order
|
||||
import com.appttude.h_mal.farmr.model.ShiftType
|
||||
import com.appttude.h_mal.farmr.model.Sortable
|
||||
@@ -37,25 +37,28 @@ class MainViewModel(
|
||||
private val repository: Repository
|
||||
) : ShiftViewModel(repository) {
|
||||
|
||||
private val _shiftLiveData = MutableLiveData<List<ShiftObject>>()
|
||||
private val shiftLiveData: LiveData<List<ShiftObject>> = _shiftLiveData
|
||||
private val shiftLiveData: LiveData<List<ShiftEntity>> = repository.readShiftsFromDatabase()
|
||||
|
||||
private var mSort: Sortable = Sortable.ID
|
||||
private var mOrder: Order = Order.ASCENDING
|
||||
|
||||
private val observer = Observer<List<ShiftObject>> {
|
||||
it?.let {
|
||||
val result = it.applyFilters().sortList(mSort, mOrder)
|
||||
onSuccess(result)
|
||||
}
|
||||
private val observer = Observer<List<ShiftEntity>> {
|
||||
it?.let { updateFiltrationAndPostResults(it) }
|
||||
}
|
||||
|
||||
init {
|
||||
// Load shifts into live data when view model has been instantiated
|
||||
refreshLiveData()
|
||||
shiftLiveData.observeForever(observer)
|
||||
}
|
||||
|
||||
private fun updateFiltrationAndPostResults(entities: List<ShiftEntity>) {
|
||||
val result = entities.mapToShiftObjects().applyFilters().sortList(mSort, mOrder)
|
||||
onSuccess(result)
|
||||
}
|
||||
|
||||
private fun List<ShiftEntity>.mapToShiftObjects(): List<ShiftObject> {
|
||||
return map { i -> i.convertToShiftObject() }
|
||||
}
|
||||
|
||||
private fun List<ShiftObject>.applyFilters(): List<ShiftObject> {
|
||||
val filter = getFiltrationDetails()
|
||||
|
||||
@@ -75,14 +78,21 @@ class MainViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun comparedStringsContains(first: String?, second: String?): Boolean {
|
||||
first?.let {
|
||||
(second?.contains(it))?.let { c -> return c }
|
||||
/*
|
||||
* Check if string compareWith contains compareAgainst
|
||||
*/
|
||||
private fun comparedStringsContains(compareWith: String?, compareAgainst: String?): Boolean {
|
||||
compareWith?.let {
|
||||
(compareAgainst?.contains(it))?.let { c -> return c }
|
||||
}
|
||||
|
||||
return comparedStrings(first, second)
|
||||
return comparedStrings(compareWith, compareAgainst)
|
||||
}
|
||||
|
||||
/*
|
||||
* check if date [compareWith] fall between two dates
|
||||
* if fromDate or toDate is null then it will take today's date for comparison
|
||||
*/
|
||||
private fun isBetween(fromDate: String?, toDate: String?, compareWith: String): Boolean? {
|
||||
val first = fromDate?.convertDateString()
|
||||
val second = toDate?.convertDateString()
|
||||
@@ -96,7 +106,9 @@ class MainViewModel(
|
||||
return compareDate.after(first) && compareDate.before(second)
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* When view-model is cleared we stop observing any livedata
|
||||
*/
|
||||
override fun onCleared() {
|
||||
shiftLiveData.removeObserver(observer)
|
||||
super.onCleared()
|
||||
@@ -132,7 +144,7 @@ class MainViewModel(
|
||||
var totalUnits = 0f
|
||||
var totalPay = 0f
|
||||
var lines = 0
|
||||
_shiftLiveData.value?.applyFilters()?.forEach {
|
||||
shiftLiveData.value?.mapToShiftObjects()?.applyFilters()?.forEach {
|
||||
lines += 1
|
||||
totalDuration += it.duration
|
||||
when (ShiftType.getEnumByType(it.type)) {
|
||||
@@ -156,16 +168,12 @@ class MainViewModel(
|
||||
fun deleteShift(id: Long) {
|
||||
if (!repository.deleteSingleShiftFromDatabase(id)) {
|
||||
onError("Failed to delete shift")
|
||||
} else {
|
||||
refreshLiveData()
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteAllShifts() {
|
||||
if (!repository.deleteAllShiftsFromDatabase()) {
|
||||
onError("Failed to delete all shifts from database")
|
||||
} else {
|
||||
refreshLiveData()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,8 +202,12 @@ class MainViewModel(
|
||||
return stringBuilder.toString()
|
||||
}
|
||||
|
||||
/*
|
||||
* After operations such as updating filtering or sorting
|
||||
* we update the data we sent to the ui
|
||||
*/
|
||||
fun refreshLiveData() {
|
||||
repository.readShiftsFromDatabase()?.let { _shiftLiveData.postValue(it) }
|
||||
shiftLiveData.value?.let { updateFiltrationAndPostResults(it) }
|
||||
}
|
||||
|
||||
fun clearFilters() {
|
||||
@@ -204,6 +216,9 @@ class MainViewModel(
|
||||
refreshLiveData()
|
||||
}
|
||||
|
||||
/*
|
||||
* Build a .csv file to be exported
|
||||
*/
|
||||
fun createExcelSheet(file: File): File? {
|
||||
val wbSettings = WorkbookSettings().apply {
|
||||
locale = Locale("en", "EN")
|
||||
@@ -232,7 +247,7 @@ class MainViewModel(
|
||||
return null
|
||||
}
|
||||
val sortAndOrder = getSortAndOrder()
|
||||
val data = shiftLiveData.value!!.applyFilters()
|
||||
val data = shiftLiveData.value!!.mapToShiftObjects().applyFilters()
|
||||
.sortList(sortAndOrder.first, sortAndOrder.second)
|
||||
var currentRow = 0
|
||||
val cells = data.map { shift ->
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.appttude.h_mal.farmr.viewmodel
|
||||
|
||||
import com.appttude.h_mal.farmr.data.Repository
|
||||
import com.appttude.h_mal.farmr.data.room.converters.DateConverter
|
||||
import com.appttude.h_mal.farmr.data.room.converters.TimeConverter
|
||||
import com.appttude.h_mal.farmr.model.Shift
|
||||
import com.appttude.h_mal.farmr.model.ShiftType
|
||||
import com.appttude.h_mal.farmr.model.Success
|
||||
@@ -17,6 +19,9 @@ class SubmissionViewModel(
|
||||
private val repository: Repository
|
||||
) : ShiftViewModel(repository) {
|
||||
|
||||
private val dateConverter = DateConverter()
|
||||
private val timeConverter = TimeConverter()
|
||||
|
||||
fun insertHourlyShift(
|
||||
description: String,
|
||||
date: String,
|
||||
@@ -26,7 +31,7 @@ class SubmissionViewModel(
|
||||
breakMins: Int?,
|
||||
) {
|
||||
// Validate inputs from the edit texts
|
||||
(description.length > 3).validateField {
|
||||
(description.trim().length > 3).validateField {
|
||||
onError("Description length should be longer")
|
||||
return
|
||||
}
|
||||
@@ -53,7 +58,7 @@ class SubmissionViewModel(
|
||||
|
||||
val result = insertShiftIntoDatabase(
|
||||
ShiftType.HOURLY,
|
||||
description,
|
||||
description.trim(),
|
||||
date,
|
||||
rateOfPay.formatToTwoDp(),
|
||||
timeIn,
|
||||
@@ -73,7 +78,7 @@ class SubmissionViewModel(
|
||||
rateOfPay: Float
|
||||
) {
|
||||
// Validate inputs from the edit texts
|
||||
(description.length > 3).validateField {
|
||||
(description.trim().length > 3).validateField {
|
||||
onError("Description length should be longer")
|
||||
return
|
||||
}
|
||||
@@ -92,7 +97,7 @@ class SubmissionViewModel(
|
||||
|
||||
val result = insertShiftIntoDatabase(
|
||||
type = ShiftType.PIECE,
|
||||
description = description,
|
||||
description = description.trim(),
|
||||
date = date,
|
||||
rateOfPay = rateOfPay.formatToTwoDp(),
|
||||
null,
|
||||
@@ -177,72 +182,72 @@ class SubmissionViewModel(
|
||||
breakMins: Int? = null,
|
||||
units: Float? = null,
|
||||
): Boolean {
|
||||
val currentShift = repository.readSingleShiftFromDatabase(id)?.copyToShift()
|
||||
val currentShift = repository.readSingleShiftFromDatabase(id)
|
||||
?: throw IOException("Cannot update shift as it does not exist")
|
||||
|
||||
val mDate = date ?: dateConverter.fromDate(currentShift.date)
|
||||
val mTimeIn = timeIn ?: timeConverter.fromTime(currentShift.timeIn)
|
||||
val mTimeOut = timeOut ?: timeConverter.fromTime(currentShift.timeIn)
|
||||
|
||||
val shift = when (type) {
|
||||
ShiftType.HOURLY -> {
|
||||
// Shift type has changed so mandatory fields for hourly shift are now required as well
|
||||
val insertTimeIn =
|
||||
(timeIn ?: currentShift.timeIn) ?: throw IOException("No time in inserted")
|
||||
val insertTimeOut =
|
||||
(timeOut ?: currentShift.timeOut) ?: throw IOException("No time out inserted")
|
||||
Shift(
|
||||
description = description ?: currentShift.description,
|
||||
date = date ?: currentShift.date,
|
||||
timeIn = insertTimeIn,
|
||||
timeOut = insertTimeOut,
|
||||
date = mDate,
|
||||
timeIn = mTimeIn,
|
||||
timeOut = mTimeOut,
|
||||
breakMins = breakMins ?: currentShift.breakMins,
|
||||
rateOfPay = rateOfPay ?: currentShift.rateOfPay
|
||||
rateOfPay = (rateOfPay ?: currentShift.payRate) ?: 0f
|
||||
)
|
||||
}
|
||||
|
||||
ShiftType.PIECE -> {
|
||||
// Shift type has changed so mandatory fields for piece rate shift are now required as well
|
||||
val insertUnits = (units ?: currentShift.units)
|
||||
?: throw IOException("Units must be inserted for piece rate shifts")
|
||||
Shift(
|
||||
description = description ?: currentShift.description,
|
||||
date = date ?: currentShift.date,
|
||||
units = insertUnits,
|
||||
rateOfPay = rateOfPay ?: currentShift.rateOfPay
|
||||
date = mDate,
|
||||
units = (units ?: currentShift.units) ?: 0f,
|
||||
rateOfPay = (rateOfPay ?: currentShift.payRate) ?: 0f
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
if (timeIn == null && timeOut == null && units == null && breakMins == null && rateOfPay == null) {
|
||||
// Updates to description or date field
|
||||
currentShift.copy(
|
||||
description = description ?: currentShift.description,
|
||||
date = date ?: currentShift.date,
|
||||
Shift(
|
||||
ShiftType.getEnumByType(currentShift.type),
|
||||
description ?: currentShift.description,
|
||||
mDate,
|
||||
mTimeIn,
|
||||
mTimeOut,
|
||||
currentShift.duration,
|
||||
currentShift.breakMins,
|
||||
currentShift.units,
|
||||
currentShift.payRate ?: 0f,
|
||||
currentShift.totalPay ?: 0f
|
||||
)
|
||||
} else {
|
||||
// Updating shifts where shift type has remained the same
|
||||
when (currentShift.type) {
|
||||
when (ShiftType.getEnumByType(currentShift.type)) {
|
||||
ShiftType.HOURLY -> {
|
||||
val insertTimeIn = (timeIn ?: currentShift.timeIn) ?: throw IOException(
|
||||
"No time in inserted"
|
||||
)
|
||||
val insertTimeOut = (timeOut ?: currentShift.timeOut)
|
||||
?: throw IOException("No time out inserted")
|
||||
Shift(
|
||||
description = description ?: currentShift.description,
|
||||
date = date ?: currentShift.date,
|
||||
timeIn = insertTimeIn,
|
||||
timeOut = insertTimeOut,
|
||||
date = mDate,
|
||||
timeIn = mTimeIn,
|
||||
timeOut = mTimeOut,
|
||||
breakMins = breakMins ?: currentShift.breakMins,
|
||||
rateOfPay = rateOfPay ?: currentShift.rateOfPay
|
||||
rateOfPay = (rateOfPay ?: currentShift.payRate) ?: 0f
|
||||
)
|
||||
}
|
||||
|
||||
ShiftType.PIECE -> {
|
||||
val insertUnits = (units ?: currentShift.units)
|
||||
?: throw IOException("Units must be inserted for piece rate shifts")
|
||||
Shift(
|
||||
description = description ?: currentShift.description,
|
||||
date = date ?: currentShift.date,
|
||||
units = insertUnits,
|
||||
rateOfPay = rateOfPay ?: currentShift.rateOfPay
|
||||
date = mDate,
|
||||
units = (insertUnits) ?: 0f,
|
||||
rateOfPay = (rateOfPay ?: currentShift.payRate) ?: 0f
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,38 @@
|
||||
package com.appttude.h_mal.farmr.data
|
||||
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase
|
||||
import com.appttude.h_mal.farmr.data.legacydb.ShiftObject
|
||||
import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider
|
||||
import com.appttude.h_mal.farmr.data.room.AppDatabase
|
||||
import com.appttude.h_mal.farmr.data.room.ShiftDao
|
||||
import com.appttude.h_mal.farmr.data.room.entity.ShiftEntity
|
||||
import com.appttude.h_mal.farmr.model.Shift
|
||||
import io.mockk.MockKAnnotations
|
||||
import io.mockk.every
|
||||
import io.mockk.impl.annotations.MockK
|
||||
import io.mockk.impl.annotations.RelaxedMockK
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.mockito.ArgumentMatchers.anyLong
|
||||
import java.io.IOException
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertIs
|
||||
|
||||
class RepositoryImplTest {
|
||||
@get:Rule
|
||||
val rule = InstantTaskExecutorRule()
|
||||
|
||||
private lateinit var repository: RepositoryImpl
|
||||
|
||||
@MockK
|
||||
lateinit var db: LegacyDatabase
|
||||
@RelaxedMockK
|
||||
lateinit var db: AppDatabase
|
||||
|
||||
@MockK
|
||||
lateinit var prefs: PreferenceProvider
|
||||
@@ -32,20 +46,123 @@ class RepositoryImplTest {
|
||||
@Test
|
||||
fun readDatabase_validResponse() {
|
||||
// Arrange
|
||||
val elements = listOf<ShiftObject>(
|
||||
mockk { every { id } returns anyLong() },
|
||||
mockk { every { id } returns anyLong() },
|
||||
mockk { every { id } returns anyLong() },
|
||||
mockk { every { id } returns anyLong() }
|
||||
)
|
||||
val liveData = mockk<LiveData<List<ShiftEntity>>>()
|
||||
|
||||
//Act
|
||||
every { db.readShiftsFromDatabase() } returns elements
|
||||
every { db.getShiftDao().getAllFullShift() } returns liveData
|
||||
|
||||
// Assert
|
||||
val result = repository.readShiftsFromDatabase()
|
||||
assertIs<List<ShiftObject>>(result)
|
||||
assertEquals(result.first().id, anyLong())
|
||||
assertIs<LiveData<List<ShiftEntity>>>(result)
|
||||
assertEquals(result, liveData)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun updateShift_invalidDescription_validThrow() {
|
||||
// Arrange
|
||||
val id = anyLong()
|
||||
val shift = mockk<Shift>()
|
||||
|
||||
//Act
|
||||
val emptyDescExceptionReturned = assertFailsWith<IllegalArgumentException> {
|
||||
every { shift.description } returns ""
|
||||
repository.updateShiftIntoDatabase(id, shift)
|
||||
}
|
||||
val untrimmedDescExceptionReturned = assertFailsWith<IllegalArgumentException> {
|
||||
every { shift.description } returns "DQ "
|
||||
repository.updateShiftIntoDatabase(id, shift)
|
||||
}
|
||||
|
||||
// Assert
|
||||
assertEquals(emptyDescExceptionReturned.message, "description required")
|
||||
assertEquals(untrimmedDescExceptionReturned.message, "description required")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun updateShift_invalidDate_validThrow() {
|
||||
// Arrange
|
||||
val id = anyLong()
|
||||
val shift = mockk<Shift>()
|
||||
|
||||
//Act
|
||||
every { shift.description } returns "Valid desc"
|
||||
val emptyDateExceptionReturned = assertFailsWith<IllegalArgumentException> {
|
||||
every { shift.date } returns ""
|
||||
repository.updateShiftIntoDatabase(id, shift)
|
||||
}
|
||||
val untrimmedDateExceptionReturned = assertFailsWith<IllegalArgumentException> {
|
||||
every { shift.description } returns "2022-03-02 "
|
||||
repository.updateShiftIntoDatabase(id, shift)
|
||||
}
|
||||
val wrongFormatDateExceptionReturned = assertFailsWith<IllegalArgumentException> {
|
||||
every { shift.description } returns "02-03-2020"
|
||||
repository.updateShiftIntoDatabase(id, shift)
|
||||
}
|
||||
val wrongFormatDateExceptionReturned2 = assertFailsWith<IllegalArgumentException> {
|
||||
every { shift.description } returns "2022/03/02"
|
||||
repository.updateShiftIntoDatabase(id, shift)
|
||||
}
|
||||
|
||||
// Assert
|
||||
assertEquals(emptyDateExceptionReturned.message, "date required")
|
||||
assertEquals(untrimmedDateExceptionReturned.message, "date required")
|
||||
assertEquals(wrongFormatDateExceptionReturned.message, "date required")
|
||||
assertEquals(wrongFormatDateExceptionReturned2.message, "date required")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun updateShift_invalidTimeInAndTimeOut_validThrow() {
|
||||
// Arrange
|
||||
val id = anyLong()
|
||||
val shift = mockk<Shift>()
|
||||
|
||||
//Act
|
||||
every { shift.description } returns "Valid desc"
|
||||
every { shift.date } returns "2020-06-05"
|
||||
val emptyTimeInExceptionReturned = assertFailsWith<IllegalArgumentException> {
|
||||
every { shift.timeIn } returns ""
|
||||
repository.updateShiftIntoDatabase(id, shift)
|
||||
}
|
||||
val untrimmedTimeInExceptionReturned = assertFailsWith<IllegalArgumentException> {
|
||||
every { shift.timeIn } returns "14:04 "
|
||||
repository.updateShiftIntoDatabase(id, shift)
|
||||
}
|
||||
val wrongFormatTimeInExceptionReturned = assertFailsWith<IllegalArgumentException> {
|
||||
every { shift.timeIn } returns "14 04"
|
||||
repository.updateShiftIntoDatabase(id, shift)
|
||||
}
|
||||
val wrongFormatTimeInExceptionReturned2 = assertFailsWith<IllegalArgumentException> {
|
||||
every { shift.timeIn } returns "1404"
|
||||
repository.updateShiftIntoDatabase(id, shift)
|
||||
}
|
||||
every { shift.timeIn } returns "14:00"
|
||||
val emptyTimeOutExceptionReturned = assertFailsWith<IllegalArgumentException> {
|
||||
every { shift.timeOut } returns ""
|
||||
repository.updateShiftIntoDatabase(id, shift)
|
||||
}
|
||||
val untrimmedTimeOutExceptionReturned = assertFailsWith<IllegalArgumentException> {
|
||||
every { shift.timeOut } returns "14:04 "
|
||||
repository.updateShiftIntoDatabase(id, shift)
|
||||
}
|
||||
val wrongFormatTimeOutExceptionReturned = assertFailsWith<IllegalArgumentException> {
|
||||
every { shift.timeOut } returns "14 04"
|
||||
repository.updateShiftIntoDatabase(id, shift)
|
||||
}
|
||||
val wrongFormatTimeOutExceptionReturned2 = assertFailsWith<IllegalArgumentException> {
|
||||
every { shift.timeOut } returns "1404"
|
||||
repository.updateShiftIntoDatabase(id, shift)
|
||||
}
|
||||
|
||||
// Assert
|
||||
assertEquals(emptyTimeInExceptionReturned.message, "time in required")
|
||||
assertEquals(untrimmedTimeInExceptionReturned.message, "time in required")
|
||||
assertEquals(wrongFormatTimeInExceptionReturned.message, "time in required")
|
||||
assertEquals(wrongFormatTimeInExceptionReturned2.message, "time in required")
|
||||
|
||||
assertEquals(emptyTimeOutExceptionReturned.message, "time out required")
|
||||
assertEquals(untrimmedTimeOutExceptionReturned.message, "time out required")
|
||||
assertEquals(wrongFormatTimeOutExceptionReturned.message, "time out required")
|
||||
assertEquals(wrongFormatTimeOutExceptionReturned2.message, "time out required")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.appttude.h_mal.farmr.data.room.converters
|
||||
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Before
|
||||
|
||||
import org.junit.Test
|
||||
import java.sql.Time
|
||||
|
||||
class TimeConverterTest {
|
||||
|
||||
private val converter = TimeConverter()
|
||||
|
||||
@Test
|
||||
fun toTime() {
|
||||
val str = "16:04"
|
||||
val time = converter.toTime(str)
|
||||
assertEquals(time.toString(), "$str:00")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fromTime() {
|
||||
val str = "16:04"
|
||||
val time = converter.fromTime(Time.valueOf("16:04:00"))
|
||||
assertEquals(time, str)
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package com.appttude.h_mal.farmr.utils
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import com.appttude.h_mal.farmr.data.legacydb.ShiftObject
|
||||
import com.appttude.h_mal.farmr.data.room.entity.ShiftEntity
|
||||
import com.appttude.h_mal.farmr.model.ShiftType
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -42,108 +42,108 @@ fun sleep(millis: Long = 1000) {
|
||||
}
|
||||
|
||||
fun getShifts() = listOf(
|
||||
ShiftObject(
|
||||
ArgumentMatchers.anyLong(),
|
||||
ShiftType.HOURLY.type,
|
||||
ShiftEntity(
|
||||
"Day one",
|
||||
"2023-08-01",
|
||||
"12:00",
|
||||
"13:00",
|
||||
1f,
|
||||
ArgumentMatchers.anyInt(),
|
||||
1f,
|
||||
ShiftType.HOURLY.type,
|
||||
ArgumentMatchers.anyFloat(),
|
||||
10f,
|
||||
10f
|
||||
),
|
||||
ShiftObject(
|
||||
10f,
|
||||
ArgumentMatchers.anyLong(),
|
||||
ShiftType.HOURLY.type,
|
||||
),
|
||||
ShiftEntity(
|
||||
"Day two",
|
||||
"2023-08-02",
|
||||
"12:00",
|
||||
"13:00",
|
||||
1f,
|
||||
ArgumentMatchers.anyInt(),
|
||||
1f,
|
||||
ShiftType.HOURLY.type,
|
||||
ArgumentMatchers.anyFloat(),
|
||||
10f,
|
||||
10f
|
||||
),
|
||||
ShiftObject(
|
||||
10f,
|
||||
ArgumentMatchers.anyLong(),
|
||||
ShiftType.HOURLY.type,
|
||||
),
|
||||
ShiftEntity(
|
||||
"Day three",
|
||||
"2023-08-03",
|
||||
"12:00",
|
||||
"13:00",
|
||||
1f,
|
||||
"14:30",
|
||||
30,
|
||||
2f,
|
||||
ShiftType.HOURLY.type,
|
||||
ArgumentMatchers.anyFloat(),
|
||||
10f,
|
||||
5f
|
||||
),
|
||||
ShiftObject(
|
||||
20f,
|
||||
ArgumentMatchers.anyLong(),
|
||||
ShiftType.HOURLY.type,
|
||||
),
|
||||
ShiftEntity(
|
||||
"Day four",
|
||||
"2023-08-04",
|
||||
"12:00",
|
||||
"13:00",
|
||||
1f,
|
||||
"14:30",
|
||||
30,
|
||||
2f,
|
||||
ShiftType.HOURLY.type,
|
||||
ArgumentMatchers.anyFloat(),
|
||||
10f,
|
||||
5f
|
||||
),
|
||||
ShiftObject(
|
||||
20f,
|
||||
ArgumentMatchers.anyLong(),
|
||||
ShiftType.PIECE.type,
|
||||
),
|
||||
ShiftEntity(
|
||||
"Day five",
|
||||
"2023-08-05",
|
||||
ArgumentMatchers.anyString(),
|
||||
ArgumentMatchers.anyString(),
|
||||
ArgumentMatchers.anyFloat(),
|
||||
ArgumentMatchers.anyInt(),
|
||||
ArgumentMatchers.anyFloat(),
|
||||
ShiftType.PIECE.type,
|
||||
1f,
|
||||
10f,
|
||||
10f
|
||||
),
|
||||
ShiftObject(
|
||||
10f,
|
||||
ArgumentMatchers.anyLong(),
|
||||
ShiftType.PIECE.type,
|
||||
),
|
||||
ShiftEntity(
|
||||
"Day six",
|
||||
"2023-08-06",
|
||||
ArgumentMatchers.anyString(),
|
||||
ArgumentMatchers.anyString(),
|
||||
ArgumentMatchers.anyFloat(),
|
||||
ArgumentMatchers.anyInt(),
|
||||
ArgumentMatchers.anyFloat(),
|
||||
ShiftType.PIECE.type,
|
||||
1f,
|
||||
10f,
|
||||
10f
|
||||
),
|
||||
ShiftObject(
|
||||
10f,
|
||||
ArgumentMatchers.anyLong(),
|
||||
ShiftType.PIECE.type,
|
||||
),
|
||||
ShiftEntity(
|
||||
"Day seven",
|
||||
"2023-08-07",
|
||||
ArgumentMatchers.anyString(),
|
||||
ArgumentMatchers.anyString(),
|
||||
ArgumentMatchers.anyFloat(),
|
||||
ArgumentMatchers.anyInt(),
|
||||
ArgumentMatchers.anyFloat(),
|
||||
ShiftType.PIECE.type,
|
||||
1f,
|
||||
10f,
|
||||
10f
|
||||
),
|
||||
ShiftObject(
|
||||
10f,
|
||||
ArgumentMatchers.anyLong(),
|
||||
ShiftType.PIECE.type,
|
||||
),
|
||||
ShiftEntity(
|
||||
"Day eight",
|
||||
"2023-08-08",
|
||||
ArgumentMatchers.anyString(),
|
||||
ArgumentMatchers.anyString(),
|
||||
ArgumentMatchers.anyFloat(),
|
||||
ArgumentMatchers.anyInt(),
|
||||
ArgumentMatchers.anyFloat(),
|
||||
ShiftType.PIECE.type,
|
||||
1f,
|
||||
10f,
|
||||
10f
|
||||
10f,
|
||||
ArgumentMatchers.anyLong(),
|
||||
),
|
||||
)
|
||||
@@ -2,6 +2,7 @@ package com.appttude.h_mal.farmr.viewmodel
|
||||
|
||||
import android.os.Bundle
|
||||
import com.appttude.h_mal.farmr.data.legacydb.ShiftObject
|
||||
import com.appttude.h_mal.farmr.data.room.entity.ShiftEntity
|
||||
import com.appttude.h_mal.farmr.utils.ID
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
@@ -16,7 +17,7 @@ class InfoViewModelTest : ShiftViewModelTest<InfoViewModel>() {
|
||||
fun retrieveData_validBundleAndId_successfulRetrieval() {
|
||||
// Arrange
|
||||
val id = anyLong()
|
||||
val shift = mockk<ShiftObject>()
|
||||
val shift = mockk<ShiftEntity>()
|
||||
val bundle = mockk<Bundle>()
|
||||
|
||||
// Act
|
||||
@@ -25,7 +26,7 @@ class InfoViewModelTest : ShiftViewModelTest<InfoViewModel>() {
|
||||
viewModel.retrieveData(bundle)
|
||||
|
||||
// Assert
|
||||
assertIs<ShiftObject>(retrieveCurrentData())
|
||||
assertIs<ShiftEntity>(retrieveCurrentData())
|
||||
assertEquals(
|
||||
retrieveCurrentData(),
|
||||
shift
|
||||
@@ -36,7 +37,7 @@ class InfoViewModelTest : ShiftViewModelTest<InfoViewModel>() {
|
||||
fun retrieveData_noValidBundleAndId_unsuccessfulRetrieval() {
|
||||
// Arrange
|
||||
val id = anyLong()
|
||||
val shift = mockk<ShiftObject>()
|
||||
val shift = mockk<ShiftEntity>()
|
||||
val bundle = mockk<Bundle>()
|
||||
|
||||
// Act
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
package com.appttude.h_mal.farmr.viewmodel
|
||||
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.appttude.h_mal.farmr.data.Repository
|
||||
import com.appttude.h_mal.farmr.data.legacydb.ShiftObject
|
||||
import com.appttude.h_mal.farmr.data.prefs.DATE_IN
|
||||
import com.appttude.h_mal.farmr.data.prefs.DATE_OUT
|
||||
import com.appttude.h_mal.farmr.data.prefs.DESCRIPTION
|
||||
import com.appttude.h_mal.farmr.data.prefs.TYPE
|
||||
import com.appttude.h_mal.farmr.data.room.entity.ShiftEntity
|
||||
import com.appttude.h_mal.farmr.model.ShiftType
|
||||
import com.appttude.h_mal.farmr.model.ViewState
|
||||
import com.appttude.h_mal.farmr.utils.getOrAwaitValue
|
||||
import com.appttude.h_mal.farmr.utils.getShifts
|
||||
import io.mockk.MockKAnnotations
|
||||
import io.mockk.every
|
||||
import io.mockk.impl.annotations.InjectMockKs
|
||||
import io.mockk.impl.annotations.RelaxedMockK
|
||||
import io.mockk.mockk
|
||||
import org.junit.Assert.assertThrows
|
||||
import org.junit.Before
|
||||
@@ -31,7 +37,10 @@ class MainViewModelTest {
|
||||
@Before
|
||||
fun setUp() {
|
||||
repository = mockk()
|
||||
every { repository.readShiftsFromDatabase() }.returns(null)
|
||||
|
||||
val mutableLiveData = MutableLiveData<List<ShiftEntity>>()
|
||||
val liveData: LiveData<List<ShiftEntity>> = mutableLiveData
|
||||
every { repository.readShiftsFromDatabase() }.returns(liveData)
|
||||
every { repository.retrieveFilteringDetailsInPrefs() }.returns(getFilter())
|
||||
viewModel = MainViewModel(repository)
|
||||
}
|
||||
@@ -45,47 +54,60 @@ class MainViewModelTest {
|
||||
@Test
|
||||
fun getShiftsFromRepository_liveDataIsShown() {
|
||||
// Arrange
|
||||
val listOfShifts = anyList<ShiftObject>()
|
||||
val mutableLiveData = MutableLiveData<List<ShiftEntity>>()
|
||||
val shifts = anyList<ShiftEntity>()
|
||||
mutableLiveData.postValue(shifts)
|
||||
val liveData: LiveData<List<ShiftEntity>> = mutableLiveData
|
||||
|
||||
// Act
|
||||
every { repository.readShiftsFromDatabase() }.returns(listOfShifts)
|
||||
every { repository.readShiftsFromDatabase() }.returns(liveData)
|
||||
viewModel = MainViewModel(repository)
|
||||
viewModel.refreshLiveData()
|
||||
|
||||
// Assert
|
||||
assertEquals(retrieveCurrentData(), listOfShifts)
|
||||
assertEquals(retrieveCurrentData(), shifts)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getShiftsFromRepository_liveDataIsShown_defaultFiltersAndSortsValid() {
|
||||
// Arrange
|
||||
val listOfShifts = getShifts()
|
||||
val mutableLiveData = MutableLiveData<List<ShiftEntity>>()
|
||||
val shifts = getShifts()
|
||||
mutableLiveData.postValue(shifts)
|
||||
val liveData: LiveData<List<ShiftEntity>> = mutableLiveData
|
||||
|
||||
// Act
|
||||
every { repository.readShiftsFromDatabase() }.returns(listOfShifts)
|
||||
every { repository.readShiftsFromDatabase() }.returns(liveData)
|
||||
viewModel = MainViewModel(repository)
|
||||
viewModel.refreshLiveData()
|
||||
val retrievedShifts = retrieveCurrentData()
|
||||
val description = viewModel.getInformation()
|
||||
|
||||
// Assert
|
||||
assertEquals(retrievedShifts, listOfShifts)
|
||||
assertEquals(retrievedShifts, shifts.map { it.convertToShiftObject() })
|
||||
assertEquals(
|
||||
description, "8 Shifts\n" +
|
||||
" (4 Hourly/4 Piece Rate)\n" +
|
||||
"Total Hours: 4.0\n" +
|
||||
"Total Hours: 6.0\n" +
|
||||
"Total Units: 4.0\n" +
|
||||
"Total Pay: £70.00"
|
||||
"Total Pay: £100.00"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getShiftsFromRepository_applyFiltersThenClearFilters_descriptionIsValid() {
|
||||
// Arrange
|
||||
val listOfShifts = getShifts()
|
||||
val mutableLiveData = MutableLiveData<List<ShiftEntity>>()
|
||||
val shifts = getShifts()
|
||||
mutableLiveData.postValue(shifts)
|
||||
val liveData: LiveData<List<ShiftEntity>> = mutableLiveData
|
||||
|
||||
val filteredShifts = getShifts().filter { it.type == ShiftType.HOURLY.type }
|
||||
|
||||
// Act
|
||||
every { repository.readShiftsFromDatabase() }.returns(listOfShifts)
|
||||
every { repository.readShiftsFromDatabase() }.returns(liveData)
|
||||
every { repository.retrieveFilteringDetailsInPrefs() }.returns(getFilter(type = ShiftType.HOURLY.type))
|
||||
viewModel = MainViewModel(repository)
|
||||
viewModel.refreshLiveData()
|
||||
val retrievedShifts = retrieveCurrentData()
|
||||
val description = viewModel.getInformation()
|
||||
@@ -96,18 +118,18 @@ class MainViewModelTest {
|
||||
val descriptionAfterClearedFilter = viewModel.getInformation()
|
||||
|
||||
// Assert
|
||||
assertEquals(retrievedShifts, filteredShifts)
|
||||
assertEquals(retrievedShifts, filteredShifts.map { it.convertToShiftObject() })
|
||||
assertEquals(
|
||||
description, "4 Shifts\n" +
|
||||
"Total Hours: 4.0\n" +
|
||||
"Total Pay: £30.00"
|
||||
"Total Hours: 6.0\n" +
|
||||
"Total Pay: £60.00"
|
||||
)
|
||||
assertEquals(
|
||||
descriptionAfterClearedFilter, "8 Shifts\n" +
|
||||
" (4 Hourly/4 Piece Rate)\n" +
|
||||
"Total Hours: 4.0\n" +
|
||||
"Total Hours: 6.0\n" +
|
||||
"Total Units: 4.0\n" +
|
||||
"Total Pay: £70.00"
|
||||
"Total Pay: £100.00"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user