Use local time zone for microsoft tasks

pull/3212/head
Alex Baker 11 months ago
parent fc04f722fb
commit df153da8cf

@ -15,6 +15,10 @@ data class Error(
)
companion object {
suspend fun HttpResponse.toMicrosoftError() = body<Error>()
suspend fun HttpResponse.toMicrosoftError(): Error? = try {
body<Error>()
} catch (_: Exception) {
null
}
}
}

@ -4,8 +4,10 @@ import org.tasks.data.entity.Task
import net.fortuna.ical4j.model.Recur
import net.fortuna.ical4j.model.WeekDay
import net.fortuna.ical4j.model.WeekDayList
import org.tasks.data.createDueDate
import org.tasks.data.entity.CaldavTask
import org.tasks.data.entity.TagData
import org.tasks.date.DateTimeUtils
import org.tasks.sync.microsoft.Tasks.Task.RecurrenceDayOfWeek
import org.tasks.sync.microsoft.Tasks.Task.RecurrenceType
import org.tasks.time.DateTime
@ -34,7 +36,20 @@ object MicrosoftConverter {
else -> Task.Priority.NONE
}
completionDate = remote.completedDateTime.toLong(currentTimeMillis())
dueDate = remote.dueDateTime.toLong(0L)
remote.dueDateTime.toLong(0L).let {
if (it > 0 && hasDueTime()) {
val oldDate = DateTimeUtils.newDateTime(dueDate)
val newDate = DateTimeUtils.newDateTime(it)
.withHourOfDay(oldDate.hourOfDay)
.withMinuteOfHour(oldDate.minuteOfHour)
.withSecondOfMinute(oldDate.secondOfMinute)
setDueDateAdjustingHideUntil(
createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME, newDate.millis)
)
} else {
setDueDateAdjustingHideUntil(it)
}
}
creationDate = remote.createdDateTime.parseDateTime()
modificationDate = remote.lastModifiedDateTime.parseDateTime()
recurrence = remote.recurrence?.let { recurrence ->
@ -92,13 +107,13 @@ object MicrosoftConverter {
categories = tags.map { it.name!! }.takeIf { it.isNotEmpty() },
dueDateTime = if (hasDueDate()) {
Tasks.Task.DateTime(
dateTime = DateTime(dueDate).startOfDay().toUTC().toString(DATE_TIME_FORMAT),
timeZone = "UTC"
dateTime = DateTime(dueDate).startOfDay().toString(DATE_TIME_FORMAT),
timeZone = TimeZone.getDefault().id
)
} else if (isRecurring) {
} else if (isRecurring) { // fallback: recurring task must have due date
Tasks.Task.DateTime(
dateTime = DateTime().startOfDay().toUTC().toString(DATE_TIME_FORMAT),
timeZone = "UTC"
dateTime = DateTime().startOfDay().toString(DATE_TIME_FORMAT),
timeZone = TimeZone.getDefault().id
)
} else {
null
@ -107,8 +122,8 @@ object MicrosoftConverter {
createdDateTime = DateTime(creationDate).toUTC().toString(DATE_TIME_UTC_FORMAT),
completedDateTime = if (isCompleted) {
Tasks.Task.DateTime(
dateTime = DateTime(completionDate).toUTC().toString(DATE_TIME_FORMAT),
timeZone = "UTC",
dateTime = DateTime(completionDate).toString(DATE_TIME_FORMAT),
timeZone = TimeZone.getDefault().id,
)
} else {
null
@ -168,13 +183,13 @@ object MicrosoftConverter {
private fun Tasks.Task.DateTime?.toLong(default: Long): Long =
this
?.let {
val tz = TimeZone.getTimeZone(it.timeZone)
?.let { task ->
val tz = TimeZone.getTimeZone(task.timeZone)
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.ssssss", Locale.US)
.apply { timeZone = tz }
.parse(it.dateTime)
.parse(task.dateTime)
?.time
?.let { ts -> DateTime(ts, tz).toLocal().millis }
?.let { DateTime(it, tz).millis }
?: default
}
?: 0L

@ -37,10 +37,9 @@ class MicrosoftService(
suspend fun deleteList(listId: String) = client.delete("$baseUrl/lists/$listId")
suspend fun getTasks(listId: String): Tasks =
client.get("$baseUrl/lists/$listId/tasks/delta").body()
suspend fun getTasks(listId: String) = client.get("$baseUrl/lists/$listId/tasks/delta")
suspend fun paginateTasks(nextPage: String): Tasks = client.get(nextPage).body()
suspend fun paginateTasks(nextPage: String) = client.get(nextPage)
suspend fun createTask(listId: String, body: Tasks.Task): Tasks.Task =
client

@ -9,7 +9,7 @@ import org.tasks.data.entity.CaldavCalendar
data class TaskLists(
@SerialName("@odata.context") val context: String,
val value: List<TaskList>,
@SerialName("@odata.nextLink") val nextPage: String?,
@SerialName("@odata.nextLink") val nextPage: String? = null,
) {
@Serializable
data class TaskList(

@ -26,6 +26,8 @@ data class Tasks(
val dueDateTime: DateTime? = null,
val linkedResources: List<LinkedResource>? = null,
val recurrence: Recurrence? = null,
val reminderDateTime: DateTime? = null,
val checklistItems: List<ChecklistItem>? = null,
@SerialName("@removed") val removed: Removed? = null,
) {
@Serializable
@ -96,6 +98,14 @@ data class Tasks(
saturday,
}
@Serializable
data class ChecklistItem(
val id: String,
val displayName: String,
val createdDateTime: String,
val isChecked: Boolean,
)
enum class Importance {
low,
normal,

@ -61,24 +61,24 @@ class ConvertFromMicrosoftTests {
@Test
fun parseCompletionDate() {
val (local, _) = TestUtilities.mstodo("microsoft/completed_task.txt")
withTZ("America/Chicago") {
val (local, _) = TestUtilities.mstodo("microsoft/completed_task.txt")
assertEquals(DateTime(2022, 9, 18, 0, 0).millis, local.completionDate)
}
}
@Test
fun parseDueDate() {
val (local, _) = TestUtilities.mstodo("microsoft/basic_task_with_due_date.txt")
withTZ("America/Chicago") {
val (local, _) = TestUtilities.mstodo("microsoft/basic_task_with_due_date.txt")
assertEquals(DateTime(2023, 7, 19, 0, 0).millis, local.dueDate)
}
}
@Test
fun parseCreationDate() {
val (local, _) = TestUtilities.mstodo("microsoft/basic_task_with_due_date.txt")
withTZ("America/Chicago") {
val (local, _) = TestUtilities.mstodo("microsoft/basic_task_with_due_date.txt")
assertEquals(
DateTime(2023, 7, 19, 23, 20, 56, 9).millis,
local.creationDate
@ -88,8 +88,8 @@ class ConvertFromMicrosoftTests {
@Test
fun parseModificationDate() {
val (local, _) = TestUtilities.mstodo("microsoft/basic_task_with_due_date.txt")
withTZ("America/Chicago") {
val (local, _) = TestUtilities.mstodo("microsoft/basic_task_with_due_date.txt")
assertEquals(
DateTime(2023, 7, 19, 23, 21, 6, 269).millis,
local.modificationDate

@ -139,8 +139,8 @@ class ConvertToMicrosoftTests {
with(DUE_TIME, DateTime(2023, 7, 21, 13, 30))
)
.toRemote()
assertEquals("2023-07-21T05:00:00.0000000", remote.dueDateTime?.dateTime)
assertEquals("UTC", remote.dueDateTime?.timeZone)
assertEquals("2023-07-21T00:00:00.0000000", remote.dueDateTime?.dateTime)
assertEquals("America/Chicago", remote.dueDateTime?.timeZone)
}
}

@ -0,0 +1,27 @@
{
"@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-elfKFgB_iu9jwMdUnbCSooCAPAbUSRcpefYeIuTrt_20ZA.4hYl5mAJ-NfssRdizzms-3tZN2OLFUtndPb1x3Ca9Ak",
"value": [
{
"@odata.type": "#microsoft.graph.todoTask",
"@odata.etag": "W/\"SRPGnpbHYES1XW8UlYrtsgAGX5rVEA==\"",
"importance": "normal",
"isReminderOn": true,
"status": "notStarted",
"title": "Test",
"createdDateTime": "2023-07-22T04:58:52.9064041Z",
"lastModifiedDateTime": "2023-07-22T04:58:52.9358461Z",
"hasAttachments": false,
"categories": [],
"id": "AQMkADAwATNiZmYAZC04OABiMC0xZDlkLTAwAi0wMAoARgAAA8dKmrSa60tBjeiKoPukmoQHAEkTxp6Wx2BEtV1vFJWK7bIAAAIBEgAAAEkTxp6Wx2BEtV1vFJWK7bIABl95iCgAAAA=",
"body": {
"content": "",
"contentType": "text"
},
"reminderDateTime": {
"dateTime": "2023-07-22T14:00:00.0000000",
"timeZone": "UTC"
}
}
]
}

@ -0,0 +1,43 @@
{
"@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=l7WI41swwioT5csv4k99nvA6Vw_JoxmsCI684Nwl-4ciS2_UHNuV2sSv4_GUGLu08H56FKa7bq_dusmAUGzg0npiBEHMJiTtGq4Tydf9hKM.uyzmw12DAcVivLt2W2wnOpvqSqoOwLQK4lxe_EG5Tzo",
"value": [
{
"@odata.type": "#microsoft.graph.todoTask",
"@odata.etag": "W/\"SRPGnpbHYES1XW8UlYrtsgAGhj1xBA==\"",
"importance": "normal",
"isReminderOn": false,
"status": "notStarted",
"title": "Test with subtasks",
"createdDateTime": "2023-09-18T04:43:02.7761097Z",
"lastModifiedDateTime": "2023-09-18T04:43:12.7208067Z",
"hasAttachments": false,
"categories": [],
"id": "AQMkADAwATNiZmYAZC04OABiMC0xZDlkLTAwAi0wMAoARgAAA8dKmrSa60tBjeiKoPukmoQHAEkTxp6Wx2BEtV1vFJWK7bIAAAIBEgAAAEkTxp6Wx2BEtV1vFJWK7bIABoYcWyYAAAA=",
"body": {
"content": "",
"contentType": "text"
},
"checklistItems": [
{
"displayName": "A",
"createdDateTime": "2023-09-18T04:43:10.1170114Z",
"isChecked": false,
"id": "426d6b9f-6cb0-4642-b9ed-b12a17fa0730"
},
{
"displayName": "B",
"createdDateTime": "2023-09-18T04:43:10.6820549Z",
"isChecked": false,
"id": "774adbcb-5e89-4e1a-896d-6b18f898d6e0"
},
{
"displayName": "C",
"createdDateTime": "2023-09-18T04:43:12.6585325Z",
"isChecked": false,
"id": "ecde7824-1e4a-42b2-b3a8-aea7504d316e"
}
]
}
]
}
Loading…
Cancel
Save