Microsoft recurrence sync

Limit recurrence options for Microsoft tasks
pull/2352/head
Alex Baker 10 months ago
parent a2ef184c7d
commit f62de8b7f3

@ -155,7 +155,7 @@ data class Task(
}
val isRecurring: Boolean
get() = !Strings.isNullOrEmpty(recurrence)
get() = recurrence?.isNotBlank() == true
fun setRecurrence(rrule: Recur?) {
recurrence = rrule?.toString()

@ -13,12 +13,17 @@ import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.lifecycleScope
import com.google.android.material.composethemeadapter.MdcTheme
import com.todoroo.astrid.api.CaldavFilter
import com.todoroo.astrid.api.GtasksFilter
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import net.fortuna.ical4j.model.Recur
import net.fortuna.ical4j.model.WeekDay
import org.tasks.R
import org.tasks.compose.collectAsStateLifecycleAware
import org.tasks.compose.edit.RepeatRow
import org.tasks.data.CaldavAccount
import org.tasks.data.CaldavDao
import org.tasks.repeats.BasicRecurrenceDialog
import org.tasks.repeats.RecurrenceUtils.newRecur
import org.tasks.repeats.RepeatRuleToString
@ -30,6 +35,7 @@ import javax.inject.Inject
@AndroidEntryPoint
class RepeatControlSet : TaskEditControlFragment() {
@Inject lateinit var repeatRuleToString: RepeatRuleToString
@Inject lateinit var caldavDao: CaldavDao
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_RECURRENCE) {
@ -86,13 +92,27 @@ class RepeatControlSet : TaskEditControlFragment() {
},
repeatAfterCompletion = viewModel.repeatAfterCompletion.collectAsStateLifecycleAware().value,
onClick = {
BasicRecurrenceDialog.newBasicRecurrenceDialog(
this@RepeatControlSet,
REQUEST_RECURRENCE,
viewModel.recurrence.value,
viewModel.dueDate.value.let { if (it > 0) it else currentTimeMillis() }
)
.show(parentFragmentManager, FRAG_TAG_BASIC_RECURRENCE)
lifecycleScope.launch {
val accountType = viewModel.selectedList.value
.let {
when (it) {
is CaldavFilter -> it.account
is GtasksFilter -> it.account
else -> null
}
}
?.let { caldavDao.getAccountByUuid(it) }
?.accountType
?: CaldavAccount.TYPE_LOCAL
BasicRecurrenceDialog.newBasicRecurrenceDialog(
target = this@RepeatControlSet,
rc = REQUEST_RECURRENCE,
rrule = viewModel.recurrence.value,
dueDate = viewModel.dueDate.value,
accountType = accountType,
)
.show(parentFragmentManager, FRAG_TAG_BASIC_RECURRENCE)
}
},
onRepeatFromChanged = { viewModel.repeatAfterCompletion.value = it }
)

@ -156,7 +156,7 @@ fun CustomRecurrence(
selected = state.selectedDays,
toggle = toggleDay,
)
} else if (state.frequency == Recur.Frequency.MONTHLY) {
} else if (state.frequency == Recur.Frequency.MONTHLY && !state.isMicrosoftTask) {
MonthlyPicker(
monthDay = state.monthDay,
dayNumber = state.dueDayOfMonth,
@ -167,18 +167,20 @@ fun CustomRecurrence(
onSelected = setMonthSelection,
)
}
Divider(
modifier = Modifier.padding(vertical = if (state.frequency == Recur.Frequency.WEEKLY) 11.dp else 16.dp),
color = border()
)
EndsPicker(
selection = state.endSelection,
endDate = state.endDate,
endOccurrences = state.endCount,
setEndDate = setEndDate,
setSelection = setSelectedEndType,
setOccurrences = setOccurrences,
)
if (!state.isMicrosoftTask) {
Divider(
modifier = Modifier.padding(vertical = if (state.frequency == Recur.Frequency.WEEKLY) 11.dp else 16.dp),
color = border()
)
EndsPicker(
selection = state.endSelection,
endDate = state.endDate,
endOccurrences = state.endCount,
setEndDate = setEndDate,
setSelection = setSelectedEndType,
setOccurrences = setOccurrences,
)
}
}
}
}

@ -185,7 +185,7 @@ class CaldavAccount : Parcelable {
}
override fun toString(): String {
return "CaldavAccount(id=$id, uuid=$uuid, name=$name, url=$url, username=$username, password=$password, error=$error, accountType=$accountType, isCollapsed=$isCollapsed, serverType=$serverType)"
return "CaldavAccount(id=$id, uuid=$uuid, name=$name, url=$url, username=$username, password=<redacted>>, error=$error, accountType=$accountType, isCollapsed=$isCollapsed, serverType=$serverType)"
}
fun isTasksSubscription(context: Context): Boolean {

@ -15,6 +15,7 @@ import org.tasks.R
import org.tasks.calendars.CalendarPicker
import org.tasks.calendars.CalendarPicker.Companion.newCalendarPicker
import org.tasks.calendars.CalendarProvider
import org.tasks.data.CaldavAccount
import org.tasks.data.LocationDao
import org.tasks.data.Place
import org.tasks.data.TagData
@ -84,7 +85,13 @@ class TaskDefaults : InjectingPreferenceFragment() {
.getStringValue(R.string.p_default_recurrence)
?.takeIf { it.isNotBlank() }
BasicRecurrenceDialog
.newBasicRecurrenceDialog(this, REQUEST_RECURRENCE, rrule, -1)
.newBasicRecurrenceDialog(
target = this,
rc = REQUEST_RECURRENCE,
rrule = rrule,
dueDate = 0,
accountType = CaldavAccount.TYPE_LOCAL
)
.show(parentFragmentManager, FRAG_TAG_BASIC_RECURRENCE)
false
}

@ -3,7 +3,6 @@ package org.tasks.repeats
import android.app.Activity
import android.app.Activity.RESULT_OK
import android.app.Dialog
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import androidx.activity.result.contract.ActivityResultContracts
@ -13,11 +12,10 @@ import com.google.common.collect.Lists
import dagger.hilt.android.AndroidEntryPoint
import net.fortuna.ical4j.model.Recur
import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
import org.tasks.dialogs.DialogBuilder
import org.tasks.repeats.CustomRecurrenceActivity.Companion.EXTRA_ACCOUNT_TYPE
import org.tasks.repeats.CustomRecurrenceActivity.Companion.newIntent
import org.tasks.repeats.RecurrenceUtils.newRecur
import org.tasks.time.DateTimeUtils.currentTimeMillis
import org.tasks.ui.SingleCheckedArrayAdapter
import timber.log.Timber
import javax.inject.Inject
@ -35,17 +33,19 @@ class BasicRecurrenceDialog : DialogFragment() {
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val arguments = arguments
val dueDate = arguments!!.getLong(EXTRA_DATE, currentTimeMillis())
val rule = arguments.getString(EXTRA_RRULE)
var rrule: Recur? = null
try {
if (!isNullOrEmpty(rule)) {
rrule = newRecur(rule!!)
}
} catch (e: Exception) {
Timber.e(e)
}
val args = requireArguments()
val rule = args.getString(EXTRA_RRULE)
val rrule =
rule
.takeIf { !it.isNullOrBlank() }
?.let {
try {
newRecur(it)
} catch (e: Exception) {
Timber.e(e)
null
}
}
val customPicked = isCustomValue(rrule)
val repeatOptions: List<String> =
Lists.newArrayList(*requireContext().resources.getStringArray(R.array.repeat_options))
@ -64,40 +64,47 @@ class BasicRecurrenceDialog : DialogFragment() {
}
return dialogBuilder
.newDialog()
.setSingleChoiceItems(
adapter,
selected
) { dialogInterface: DialogInterface, i: Int ->
var i = i
if (customPicked) {
if (i == 0) {
dialogInterface.dismiss()
.setSingleChoiceItems(adapter, selected) { dialog, selectedIndex: Int ->
val i = if (customPicked) {
if (selectedIndex == 0) {
dialog.dismiss()
return@setSingleChoiceItems
}
i--
selectedIndex - 1
} else {
selectedIndex
}
val result: Recur?
when (i) {
0 -> result = null
val result = when (i) {
0 -> null
5 -> {
customRecurrence.launch(newIntent(requireContext(), rule, dueDate))
customRecurrence.launch(
newIntent(
context = requireContext(),
rrule = rule,
dueDate = args.getLong(EXTRA_DATE),
accountType = args.getInt(EXTRA_ACCOUNT_TYPE)
)
)
return@setSingleChoiceItems
}
else -> {
result = newRecur()
result.interval = 1
when (i) {
1 -> result.setFrequency(Recur.Frequency.DAILY.name)
2 -> result.setFrequency(Recur.Frequency.WEEKLY.name)
3 -> result.setFrequency(Recur.Frequency.MONTHLY.name)
4 -> result.setFrequency(Recur.Frequency.YEARLY.name)
val frequency = when(i) {
1 -> Recur.Frequency.DAILY
2 -> Recur.Frequency.WEEKLY
3 -> Recur.Frequency.MONTHLY
4 -> Recur.Frequency.YEARLY
else -> throw IllegalArgumentException()
}
newRecur().apply {
interval = 1
setFrequency(frequency.name)
}
}
}
val intent = Intent()
intent.putExtra(EXTRA_RRULE, result?.toString())
targetFragment!!.onActivityResult(targetRequestCode, RESULT_OK, intent)
dialogInterface.dismiss()
dialog.dismiss()
}
.show()
}
@ -114,16 +121,19 @@ class BasicRecurrenceDialog : DialogFragment() {
const val EXTRA_RRULE = "extra_rrule"
private const val EXTRA_DATE = "extra_date"
fun newBasicRecurrenceDialog(
target: Fragment?, rc: Int, rrule: String?, dueDate: Long
target: Fragment?,
rc: Int,
rrule: String?,
dueDate: Long,
accountType: Int,
): BasicRecurrenceDialog {
val dialog = BasicRecurrenceDialog()
dialog.setTargetFragment(target, rc)
val arguments = Bundle()
if (rrule != null) {
arguments.putString(EXTRA_RRULE, rrule)
dialog.arguments = Bundle().apply {
rrule?.let { putString(EXTRA_RRULE, it) }
putLong(EXTRA_DATE, dueDate)
putInt(EXTRA_ACCOUNT_TYPE, accountType)
}
arguments.putLong(EXTRA_DATE, dueDate)
dialog.arguments = arguments
return dialog
}
}

@ -5,8 +5,6 @@ import android.content.Intent
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.fragment.app.FragmentActivity
import com.google.android.material.composethemeadapter.MdcTheme
import dagger.hilt.android.AndroidEntryPoint
@ -43,11 +41,13 @@ class CustomRecurrenceActivity : FragmentActivity() {
companion object {
const val EXTRA_RRULE = "extra_rrule"
const val EXTRA_DATE = "extra_date"
const val EXTRA_ACCOUNT_TYPE = "extra_account_type"
@JvmStatic
fun newIntent(context: Context, rrule: String?, dueDate: Long) =
fun newIntent(context: Context, rrule: String?, dueDate: Long, accountType: Int) =
Intent(context, CustomRecurrenceActivity::class.java)
.putExtra(EXTRA_DATE, dueDate)
.putExtra(EXTRA_RRULE, rrule)
.putExtra(EXTRA_ACCOUNT_TYPE, accountType)
}
}

@ -17,7 +17,9 @@ import net.fortuna.ical4j.model.Recur.Frequency.YEARLY
import net.fortuna.ical4j.model.WeekDay
import net.fortuna.ical4j.model.WeekDayList
import net.fortuna.ical4j.model.property.RRule
import org.tasks.data.CaldavAccount.Companion.TYPE_MICROSOFT
import org.tasks.date.DateTimeUtils.toDateTime
import org.tasks.repeats.CustomRecurrenceActivity.Companion.EXTRA_ACCOUNT_TYPE
import org.tasks.repeats.CustomRecurrenceActivity.Companion.EXTRA_DATE
import org.tasks.repeats.CustomRecurrenceActivity.Companion.EXTRA_RRULE
import org.tasks.time.DateTime
@ -43,11 +45,12 @@ class CustomRecurrenceViewModel @Inject constructor(
val endSelection: Int = 0,
val endDate: Long = dueDate.toDateTime().plusMonths(1).startOfDay().millis,
val endCount: Int = 1,
val frequencyOptions: List<Recur.Frequency> = DEFAULT_FREQUENCIES,
val frequencyOptions: List<Recur.Frequency> = FREQ_ALL,
val daysOfWeek: List<DayOfWeek> = Locale.getDefault().daysOfWeek(),
val selectedDays: List<DayOfWeek> = emptyList(),
val locale: Locale = Locale.getDefault(),
val monthDay: WeekDay? = null,
val isMicrosoftTask: Boolean = false,
) {
val dueDayOfWeek: DayOfWeek
get() = Instant.ofEpochMilli(dueDate).atZone(ZoneId.systemDefault()).dayOfWeek
@ -78,17 +81,15 @@ class CustomRecurrenceViewModel @Inject constructor(
.get<Long>(EXTRA_DATE)
?.takeIf { it > 0 }
?: System.currentTimeMillis().startOfDay()
val selectedDays = if (recur?.frequency == WEEKLY) {
recur.dayList?.toDaysOfWeek()
} else {
emptyList()
}
val isMicrosoftTask = savedStateHandle.get<Int>(EXTRA_ACCOUNT_TYPE) == TYPE_MICROSOFT
val frequencies = if (isMicrosoftTask) FREQ_MICROSOFT else FREQ_ALL
_state.update { state ->
state.copy(
interval = recur?.interval?.takeIf { it > 0 } ?: 1,
frequency = recur?.frequency ?: WEEKLY,
frequency = recur?.frequency?.takeIf { frequencies.contains(it) } ?: WEEKLY,
dueDate = dueDate,
endSelection = when {
isMicrosoftTask -> 0
recur == null -> 0
recur.until != null -> 1
recur.count >= 0 -> 2
@ -103,7 +104,12 @@ class CustomRecurrenceViewModel @Inject constructor(
?.toDaysOfWeek()
?: emptyList(),
locale = locale,
monthDay = recur?.dayList?.takeIf { recur.frequency == MONTHLY }?.firstOrNull(),
monthDay = recur
?.dayList
?.takeIf { recur.frequency == MONTHLY && !isMicrosoftTask }
?.firstOrNull(),
isMicrosoftTask = isMicrosoftTask,
frequencyOptions = frequencies,
)
}
}
@ -182,7 +188,8 @@ class CustomRecurrenceViewModel @Inject constructor(
}
companion object {
val DEFAULT_FREQUENCIES = listOf(MINUTELY, HOURLY, DAILY, WEEKLY, MONTHLY, YEARLY)
val FREQ_ALL = listOf(MINUTELY, HOURLY, DAILY, WEEKLY, MONTHLY, YEARLY)
val FREQ_MICROSOFT = listOf(DAILY, WEEKLY, MONTHLY, YEARLY)
private fun Locale.daysOfWeek(): List<DayOfWeek> {
val values = DayOfWeek.values()

@ -1,10 +1,14 @@
package org.tasks.sync.microsoft
import com.todoroo.astrid.data.Task
import net.fortuna.ical4j.model.Recur
import net.fortuna.ical4j.model.WeekDay
import net.fortuna.ical4j.model.WeekDayList
import org.tasks.data.CaldavTask
import org.tasks.data.TagData
import org.tasks.sync.microsoft.Tasks.Task.RecurrenceDayOfWeek
import org.tasks.sync.microsoft.Tasks.Task.RecurrenceType
import org.tasks.time.DateTime
import org.tasks.time.DateTimeUtils.startOfDay
import java.text.SimpleDateFormat
import java.time.ZonedDateTime
import java.util.Locale
@ -13,6 +17,8 @@ import java.util.TimeZone
object MicrosoftConverter {
private const val TYPE_TEXT = "text"
private const val DATE_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS0000"
private const val DATE_TIME_UTC_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS0000'Z'"
fun Task.applyRemote(
remote: Tasks.Task,
@ -30,8 +36,34 @@ object MicrosoftConverter {
dueDate = remote.dueDateTime.toLong(0L)
creationDate = remote.createdDateTime.parseDateTime()
modificationDate = remote.lastModifiedDateTime.parseDateTime()
recurrence = remote.recurrence?.let { recurrence ->
val pattern = recurrence.pattern
val frequency = when (pattern.type) {
RecurrenceType.daily -> Recur.Frequency.DAILY
RecurrenceType.weekly -> Recur.Frequency.WEEKLY
RecurrenceType.absoluteMonthly -> Recur.Frequency.MONTHLY
RecurrenceType.absoluteYearly -> Recur.Frequency.YEARLY
else -> return@let null
}
val dayList = pattern.daysOfWeek.mapNotNull {
when (it) {
RecurrenceDayOfWeek.sunday -> WeekDay.SU
RecurrenceDayOfWeek.monday -> WeekDay.MO
RecurrenceDayOfWeek.tuesday -> WeekDay.TU
RecurrenceDayOfWeek.wednesday -> WeekDay.WE
RecurrenceDayOfWeek.thursday -> WeekDay.TH
RecurrenceDayOfWeek.friday -> WeekDay.FR
RecurrenceDayOfWeek.saturday -> WeekDay.SA
}
}
Recur.Builder()
.frequency(frequency)
.interval(pattern.interval.takeIf { it > 1 })
.dayList(WeekDayList(*dayList.toTypedArray()))
.build()
.toString()
}
// checklist to subtasks
// repeat
// sync reminders
// sync files
}
@ -59,24 +91,72 @@ object MicrosoftConverter {
categories = tags.map { it.name!! }.takeIf { it.isNotEmpty() },
dueDateTime = if (hasDueDate()) {
Tasks.Task.DateTime(
dateTime = DateTime(dueDate.startOfDay()).toUTC().toString("yyyy-MM-dd'T'HH:mm:ss.SSS0000"),
timeZone = "UTC"
dateTime = DateTime(dueDate).startOfDay().toUTC().toString(DATE_TIME_FORMAT),
timeZone = "UTC"
)
} else if (isRecurring) {
Tasks.Task.DateTime(
dateTime = DateTime().startOfDay().toUTC().toString(DATE_TIME_FORMAT),
timeZone = "UTC"
)
} else {
null
},
lastModifiedDateTime = DateTime(modificationDate).toUTC().toString("yyyy-MM-dd'T'HH:mm:ss.SSS0000'Z'"),
createdDateTime = DateTime(creationDate).toUTC().toString("yyyy-MM-dd'T'HH:mm:ss.SSS0000'Z'"),
lastModifiedDateTime = DateTime(modificationDate).toUTC().toString(DATE_TIME_UTC_FORMAT),
createdDateTime = DateTime(creationDate).toUTC().toString(DATE_TIME_UTC_FORMAT),
completedDateTime = if (isCompleted) {
Tasks.Task.DateTime(
dateTime = DateTime(completionDate).toUTC()
.toString("yyyy-MM-dd'T'HH:mm:ss.SSSSSSS"),
dateTime = DateTime(completionDate).toUTC().toString(DATE_TIME_FORMAT),
timeZone = "UTC",
)
} else {
null
},
recurrence = if (isRecurring) {
val recur = Recur(recurrence)
when (recur.frequency) {
Recur.Frequency.DAILY -> RecurrenceType.daily
Recur.Frequency.WEEKLY -> RecurrenceType.weekly
Recur.Frequency.MONTHLY -> RecurrenceType.absoluteMonthly
Recur.Frequency.YEARLY -> RecurrenceType.absoluteYearly
else -> null
}?.let { frequency ->
val dueDateTime = if (hasDueDate()) DateTime(dueDate) else DateTime()
Tasks.Task.Recurrence(
pattern = Tasks.Task.Pattern(
type = frequency,
interval = recur.interval.coerceAtLeast(1),
daysOfWeek = recur.dayList.mapNotNull {
when (it) {
WeekDay.SU -> RecurrenceDayOfWeek.sunday
WeekDay.MO -> RecurrenceDayOfWeek.monday
WeekDay.TU -> RecurrenceDayOfWeek.tuesday
WeekDay.WE -> RecurrenceDayOfWeek.wednesday
WeekDay.TH -> RecurrenceDayOfWeek.thursday
WeekDay.FR -> RecurrenceDayOfWeek.friday
WeekDay.SA -> RecurrenceDayOfWeek.saturday
else -> null
}
},
month = when (frequency) {
RecurrenceType.absoluteYearly -> dueDateTime.monthOfYear
else -> 0
},
dayOfMonth = when (frequency) {
RecurrenceType.absoluteYearly,
RecurrenceType.absoluteMonthly -> dueDateTime.dayOfMonth
else -> 0
}
),
)
}
} else {
null
}
// subtasks to checklist
// isReminderOn =
// reminders
// files
)
}

@ -21,6 +21,7 @@ data class Tasks(
val completedDateTime: DateTime? = null,
val dueDateTime: DateTime? = null,
val linkedResources: List<LinkedResource>? = null,
val recurrence: Recurrence? = null,
@field:Json(name = "@removed") val removed: Removed? = null,
) {
data class Body(
@ -44,6 +45,47 @@ data class Tasks(
val timeZone: String,
)
data class Recurrence(
val pattern: Pattern,
)
data class Pattern(
val type: RecurrenceType,
val interval: Int,
val month: Int = 0,
val dayOfMonth: Int = 0,
val daysOfWeek: List<RecurrenceDayOfWeek>,
val firstDayOfWeek: RecurrenceDayOfWeek = RecurrenceDayOfWeek.sunday,
val index: RecurrenceIndex = RecurrenceIndex.first,
)
enum class RecurrenceType {
daily,
weekly,
absoluteMonthly,
relativeMonthly,
absoluteYearly,
relativeYearly,
}
enum class RecurrenceIndex {
first,
second,
third,
fourth,
last,
}
enum class RecurrenceDayOfWeek {
sunday,
monday,
tuesday,
wednesday,
thursday,
friday,
saturday,
}
enum class Importance {
low,
normal,

@ -96,4 +96,36 @@ class ConvertFromMicrosoftTests {
)
}
}
@Test
fun parseDailyRecurrence() {
withTZ("America/Chicago") {
val (local, _) = TestUtilities.mstodo("microsoft/repeat_daily.txt")
assertEquals("FREQ=DAILY", local.recurrence)
}
}
@Test
fun parseWeekdayRecurrence() {
withTZ("America/Chicago") {
val (local, _) = TestUtilities.mstodo("microsoft/repeat_weekdays.txt")
assertEquals("FREQ=WEEKLY;INTERVAL=2;BYDAY=MO,TU,WE,TH,FR", local.recurrence)
}
}
@Test
fun parseAbsoluteMonthlyRecurrence() {
withTZ("America/Chicago") {
val (local, _) = TestUtilities.mstodo("microsoft/repeat_monthly.txt")
assertEquals("FREQ=MONTHLY", local.recurrence)
}
}
@Test
fun parseAbsoluteYearlyRecurrence() {
withTZ("America/Chicago") {
val (local, _) = TestUtilities.mstodo("microsoft/repeat_yearly.txt")
assertEquals("FREQ=YEARLY", local.recurrence)
}
}
}

@ -4,8 +4,11 @@ import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.astrid.data.Task
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.tasks.Freeze.Companion.freezeAt
import org.tasks.TestUtilities.withTZ
import org.tasks.data.CaldavTask
import org.tasks.makers.CaldavTaskMaker.REMOTE_ID
import org.tasks.makers.CaldavTaskMaker.newCaldavTask
import org.tasks.makers.TagDataMaker.NAME
@ -14,92 +17,81 @@ import org.tasks.makers.TaskMaker.COMPLETION_TIME
import org.tasks.makers.TaskMaker.DESCRIPTION
import org.tasks.makers.TaskMaker.DUE_TIME
import org.tasks.makers.TaskMaker.PRIORITY
import org.tasks.makers.TaskMaker.RECUR
import org.tasks.makers.TaskMaker.TITLE
import org.tasks.makers.TaskMaker.newTask
import org.tasks.sync.microsoft.MicrosoftConverter.toRemote
import org.tasks.sync.microsoft.Tasks.Task.Importance
import org.tasks.sync.microsoft.Tasks.Task.RecurrenceDayOfWeek
import org.tasks.sync.microsoft.Tasks.Task.RecurrenceType
import org.tasks.time.DateTime
class ConvertToMicrosoftTests {
@Test
fun noIdForNewTask() {
val remote =
newTask().toRemote(newCaldavTask(with(REMOTE_ID, null as String?)), emptyList())
val remote = newTask().toRemote(newCaldavTask(with(REMOTE_ID, null as String?)))
assertNull(remote.id)
}
@Test
fun setTitle() {
val remote =
newTask(with(TITLE, "title")).toRemote(newCaldavTask(), emptyList())
val remote = newTask(with(TITLE, "title")).toRemote(newCaldavTask())
assertEquals("title", remote.title)
}
@Test
fun noBody() {
val remote =
newTask(with(DESCRIPTION, null as String?))
.toRemote(newCaldavTask(), emptyList())
val remote = newTask(with(DESCRIPTION, null as String?)).toRemote()
assertNull(remote.body)
}
@Test
fun setBody() {
val remote =
newTask(with(DESCRIPTION, "Description"))
.toRemote(newCaldavTask(), emptyList())
val remote = newTask(with(DESCRIPTION, "Description")).toRemote()
assertEquals("Description", remote.body?.content)
assertEquals("text", remote.body?.contentType)
}
@Test
fun setHighPriority() {
val remote =
newTask(with(PRIORITY, Task.Priority.HIGH))
.toRemote(newCaldavTask(), emptyList())
val remote = newTask(with(PRIORITY, Task.Priority.HIGH)).toRemote()
assertEquals(Importance.high, remote.importance)
}
@Test
fun setNormalPriority() {
val remote =
newTask(with(PRIORITY, Task.Priority.MEDIUM))
.toRemote(newCaldavTask(), emptyList())
val remote = newTask(with(PRIORITY, Task.Priority.MEDIUM)).toRemote()
assertEquals(Importance.normal, remote.importance)
}
@Test
fun setLowPriority() {
val remote =
newTask(with(PRIORITY, Task.Priority.LOW))
.toRemote(newCaldavTask(), emptyList())
val remote = newTask(with(PRIORITY, Task.Priority.LOW)).toRemote()
assertEquals(Importance.low, remote.importance)
}
@Test
fun setNoPriorityToLow() {
val remote =
newTask(with(PRIORITY, Task.Priority.NONE))
.toRemote(newCaldavTask(), emptyList())
val remote = newTask(with(PRIORITY, Task.Priority.NONE)).toRemote()
assertEquals(Importance.low, remote.importance)
}
@Test
fun statusForUncompletedTask() {
val remote = newTask().toRemote(newCaldavTask(), emptyList())
val remote = newTask().toRemote()
assertEquals(Tasks.Task.Status.notStarted, remote.status)
}
@Test
fun statusForCompletedTask() {
val remote =
newTask(with(COMPLETION_TIME, DateTime())).toRemote(newCaldavTask(), emptyList())
newTask(with(COMPLETION_TIME, DateTime())).toRemote()
assertEquals(Tasks.Task.Status.completed, remote.status)
}
@Test
fun noCategories() {
val remote = newTask().toRemote(newCaldavTask(), emptyList())
val remote = newTask().toRemote()
assertNull(remote.categories)
}
@ -120,7 +112,7 @@ class ConvertToMicrosoftTests {
withTZ("America/Chicago") {
val remote = Task(
creationDate = DateTime(2023, 7, 21, 0, 42, 13, 475).millis,
).toRemote(newCaldavTask(), emptyList())
).toRemote()
assertEquals(
"2023-07-21T05:42:13.4750000Z",
remote.createdDateTime
@ -133,7 +125,7 @@ class ConvertToMicrosoftTests {
withTZ("America/Chicago") {
val remote = Task(
modificationDate = DateTime(2023, 7, 21, 0, 49, 4, 3).millis,
).toRemote(newCaldavTask(), emptyList())
).toRemote()
assertEquals(
"2023-07-21T05:49:04.0030000Z",
remote.lastModifiedDateTime
@ -145,14 +137,103 @@ class ConvertToMicrosoftTests {
fun setDueDateTime() {
withTZ("America/Chicago") {
val remote = newTask(
with(
DUE_TIME,
DateTime(2023, 7, 21, 13, 30)
)
with(DUE_TIME, DateTime(2023, 7, 21, 13, 30))
)
.toRemote(newCaldavTask(), emptyList())
.toRemote()
assertEquals("2023-07-21T05:00:00.0000000", remote.dueDateTime?.dateTime)
assertEquals("UTC", remote.dueDateTime?.timeZone)
}
}
}
@Test
fun setDailyRecurrence() {
withTZ("America/Chicago") {
val remote = newTask(
with(DUE_TIME, DateTime(2023, 8, 2, 22, 42, 59)),
with(RECUR, "FREQ=DAILY")
)
.toRemote()
.recurrence
?: throw IllegalStateException()
assertEquals(RecurrenceType.daily, remote.pattern.type)
assertEquals(1, remote.pattern.interval)
assertTrue(remote.pattern.daysOfWeek.isEmpty())
}
}
@Test
fun setWeeklyRecurrence() {
withTZ("America/Chicago") {
val remote = newTask(
with(DUE_TIME, DateTime(2023, 7, 31, 0, 26, 48)),
with(RECUR, "FREQ=WEEKLY;INTERVAL=2;BYDAY=MO,TU,WE,TH,FR")
)
.toRemote()
.recurrence
?: throw IllegalStateException()
assertEquals(RecurrenceType.weekly, remote.pattern.type)
assertEquals(2, remote.pattern.interval)
assertEquals(
listOf(
RecurrenceDayOfWeek.monday,
RecurrenceDayOfWeek.tuesday,
RecurrenceDayOfWeek.wednesday,
RecurrenceDayOfWeek.thursday,
RecurrenceDayOfWeek.friday,
),
remote.pattern.daysOfWeek
)
}
}
@Test
fun setMonthlyRecurrence() {
withTZ("America/Chicago") {
val remote = newTask(
with(DUE_TIME, DateTime(2023, 7, 31, 0, 26, 48)),
with(RECUR, "FREQ=MONTHLY")
)
.toRemote()
.recurrence
?: throw IllegalStateException()
assertEquals(RecurrenceType.absoluteMonthly, remote.pattern.type)
assertEquals(31, remote.pattern.dayOfMonth)
assertEquals(1, remote.pattern.interval)
}
}
@Test
fun setAnnualRecurrence() {
withTZ("America/Chicago") {
val remote = newTask(
with(DUE_TIME, DateTime(2023, 8, 2, 23, 9, 7)),
with(RECUR, "FREQ=YEARLY")
)
.toRemote()
.recurrence
?: throw IllegalStateException()
assertEquals(RecurrenceType.absoluteYearly, remote.pattern.type)
assertEquals(8, remote.pattern.month)
assertEquals(2, remote.pattern.dayOfMonth)
assertEquals(1, remote.pattern.interval)
}
}
@Test
fun setAnnualRecurrenceWithoutDueDate() {
withTZ("America/Chicago") {
freezeAt(DateTime(2023, 8, 2, 23, 17, 22).millis) {
val remote = newTask(
with(RECUR, "FREQ=YEARLY")
)
.toRemote()
.recurrence
?: throw IllegalStateException()
assertEquals(8, remote.pattern.month)
assertEquals(2, remote.pattern.dayOfMonth)
}
}
}
private fun Task.toRemote(caldavTask: CaldavTask = newCaldavTask()) = toRemote(caldavTask, emptyList())
}

@ -0,0 +1,45 @@
{
"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#Collection(todoTask)",
"@odata.deltaLink": "https://graph.microsoft.com/v1.0/me/todo/lists/AQMkADAwATNiZmYAZC04OABiMC0xZDlkLTAwAi0wMAoALgAAA8dKmrSa60tBjeiKoPukmoQBAEkTxp6Wx2BEtV1vFJWK7bIAAAIBEgAAAA==/tasks/delta?$deltatoken=l7WI41swwioT5csv4k99noD3PiMxiCD5PIupo55YsiR-aZXAnrnNZiW0yD_OdiovDJMhDsVJJ_ti6QJ2sP4FKE-qI2-ZJCLm23O7AMamOLI.S4E74O9X-kqa291QVygbM6goWhVLeQfJ17LMdlbYFpE",
"value": [
{
"@odata.type": "#microsoft.graph.todoTask",
"@odata.etag": "W/\"SRPGnpbHYES1XW8UlYrtsgAGZvj3Hg==\"",
"importance": "normal",
"isReminderOn": false,
"status": "notStarted",
"title": "Test daily",
"createdDateTime": "2023-08-02T04:36:07.3165141Z",
"lastModifiedDateTime": "2023-08-02T04:36:07.3577757Z",
"hasAttachments": false,
"categories": [],
"id": "AQMkADAwATNiZmYAZC04OABiMC0xZDlkLTAwAi0wMAoARgAAA8dKmrSa60tBjeiKoPukmoQHAEkTxp6Wx2BEtV1vFJWK7bIAAAIBEgAAAEkTxp6Wx2BEtV1vFJWK7bIABmbYjnwAAAA=",
"body": {
"content": "",
"contentType": "text"
},
"dueDateTime": {
"dateTime": "2023-08-01T05:00:00.0000000",
"timeZone": "UTC"
},
"recurrence": {
"pattern": {
"type": "daily",
"interval": 1,
"month": 0,
"dayOfMonth": 0,
"daysOfWeek": [],
"firstDayOfWeek": "sunday",
"index": "first"
},
"range": {
"type": "noEnd",
"startDate": "2023-08-01",
"endDate": "0001-01-01",
"recurrenceTimeZone": "UTC",
"numberOfOccurrences": 0
}
}
}
]
}

@ -0,0 +1,45 @@
{
"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#Collection(todoTask)",
"@odata.deltaLink": "https://graph.microsoft.com/v1.0/me/todo/lists/AQMkADAwATNiZmYAZC04OABiMC0xZDlkLTAwAi0wMAoALgAAA8dKmrSa60tBjeiKoPukmoQBAEkTxp6Wx2BEtV1vFJWK7bIAAAIBEgAAAA==/tasks/delta?$deltatoken=l7WI41swwioT5csv4k99ngHvnDU_spnidrOYzb78--8L_CvTDIAkbXl6MuXT-elf9lo4mE3OVyH8kpKh-qhqlQvZ5QTayyCCCGunhVPj6Fo.mekQONzhwcEJiA_ZH8hSylfMUnFwnJ4iTjTdLny0tqw",
"value": [
{
"@odata.type": "#microsoft.graph.todoTask",
"@odata.etag": "W/\"SRPGnpbHYES1XW8UlYrtsgAGZZbnFw==\"",
"importance": "normal",
"isReminderOn": false,
"status": "notStarted",
"title": "Test monthly",
"createdDateTime": "2023-07-31T06:02:41.6818553Z",
"lastModifiedDateTime": "2023-07-31T06:02:41.7317004Z",
"hasAttachments": false,
"categories": [],
"id": "AQMkADAwATNiZmYAZC04OABiMC0xZDlkLTAwAi0wMAoARgAAA8dKmrSa60tBjeiKoPukmoQHAEkTxp6Wx2BEtV1vFJWK7bIAAAIBEgAAAEkTxp6Wx2BEtV1vFJWK7bIABmV2YREAAAA=",
"body": {
"content": "",
"contentType": "text"
},
"dueDateTime": {
"dateTime": "2023-08-07T05:00:00.0000000",
"timeZone": "UTC"
},
"recurrence": {
"pattern": {
"type": "absoluteMonthly",
"interval": 1,
"month": 0,
"dayOfMonth": 7,
"daysOfWeek": [],
"firstDayOfWeek": "sunday",
"index": "first"
},
"range": {
"type": "noEnd",
"startDate": "2023-08-07",
"endDate": "0001-01-01",
"recurrenceTimeZone": "UTC",
"numberOfOccurrences": 0
}
}
}
]
}

@ -0,0 +1,51 @@
{
"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#Collection(todoTask)",
"@odata.deltaLink": "https://graph.microsoft.com/v1.0/me/todo/lists/AQMkADAwATNiZmYAZC04OABiMC0xZDlkLTAwAi0wMAoALgAAA8dKmrSa60tBjeiKoPukmoQBAEkTxp6Wx2BEtV1vFJWK7bIAAAIBEgAAAA==/tasks/delta?$deltatoken=l7WI41swwioT5csv4k99ngHvnDU_spnidrOYzb78--8L_CvTDIAkbXl6MuXT-elfDvxAaStXSRqnXHi-F76OGkTL9CQOnOhkx-vCewPuUdY.XsfDMIxn_aFfN1pQ1QjiU4-UJBzkm7SbIdTgPm-Ie38",
"value": [
{
"@odata.type": "#microsoft.graph.todoTask",
"@odata.etag": "W/\"SRPGnpbHYES1XW8UlYrtsgAGZZbjOA==\"",
"importance": "normal",
"isReminderOn": false,
"status": "notStarted",
"title": "Test weekdays",
"createdDateTime": "2023-07-31T04:49:33.0062602Z",
"lastModifiedDateTime": "2023-07-31T04:49:33.0698644Z",
"hasAttachments": false,
"categories": [],
"id": "AQMkADAwATNiZmYAZC04OABiMC0xZDlkLTAwAi0wMAoARgAAA8dKmrSa60tBjeiKoPukmoQHAEkTxp6Wx2BEtV1vFJWK7bIAAAIBEgAAAEkTxp6Wx2BEtV1vFJWK7bIABmV2YRAAAAA=",
"body": {
"content": "",
"contentType": "text"
},
"dueDateTime": {
"dateTime": "2023-07-31T05:00:00.0000000",
"timeZone": "UTC"
},
"recurrence": {
"pattern": {
"type": "weekly",
"interval": 2,
"month": 0,
"dayOfMonth": 0,
"daysOfWeek": [
"monday",
"tuesday",
"wednesday",
"thursday",
"friday"
],
"firstDayOfWeek": "sunday",
"index": "first"
},
"range": {
"type": "noEnd",
"startDate": "2023-07-31",
"endDate": "0001-01-01",
"recurrenceTimeZone": "UTC",
"numberOfOccurrences": 0
}
}
}
]
}

@ -0,0 +1,45 @@
{
"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#Collection(todoTask)",
"@odata.deltaLink": "https://graph.microsoft.com/v1.0/me/todo/lists/AQMkADAwATNiZmYAZC04OABiMC0xZDlkLTAwAi0wMAoALgAAA8dKmrSa60tBjeiKoPukmoQBAEkTxp6Wx2BEtV1vFJWK7bIAAAIBEgAAAA==/tasks/delta?$deltatoken=l7WI41swwioT5csv4k99noD3PiMxiCD5PIupo55YsiR-aZXAnrnNZiW0yD_OdiovZPu9JSxeTq-fQqLN4UmEJp9Tr7du6pzeTXABpwX5LG4.6R96g2GwE6zj23ydupR0eYB92e6ta7VjtqgrYOn4d9k",
"value": [
{
"@odata.type": "#microsoft.graph.todoTask",
"@odata.etag": "W/\"SRPGnpbHYES1XW8UlYrtsgAGZvj3Eg==\"",
"importance": "normal",
"isReminderOn": false,
"status": "notStarted",
"title": "Test yearly",
"createdDateTime": "2023-08-02T04:34:29.5965057Z",
"lastModifiedDateTime": "2023-08-02T04:34:29.6508837Z",
"hasAttachments": false,
"categories": [],
"id": "AQMkADAwATNiZmYAZC04OABiMC0xZDlkLTAwAi0wMAoARgAAA8dKmrSa60tBjeiKoPukmoQHAEkTxp6Wx2BEtV1vFJWK7bIAAAIBEgAAAEkTxp6Wx2BEtV1vFJWK7bIABmbYjnsAAAA=",
"body": {
"content": "",
"contentType": "text"
},
"dueDateTime": {
"dateTime": "2023-08-01T05:00:00.0000000",
"timeZone": "UTC"
},
"recurrence": {
"pattern": {
"type": "absoluteYearly",
"interval": 1,
"month": 8,
"dayOfMonth": 1,
"daysOfWeek": [],
"firstDayOfWeek": "sunday",
"index": "first"
},
"range": {
"type": "noEnd",
"startDate": "2023-08-01",
"endDate": "0001-01-01",
"recurrenceTimeZone": "UTC",
"numberOfOccurrences": 0
}
}
}
]
}
Loading…
Cancel
Save