Separate principal and principal_access tables

pull/1408/head
Alex Baker 5 years ago
parent 14cf7b39ec
commit 3d980ebc59

File diff suppressed because it is too large Load Diff

@ -65,10 +65,10 @@ class SharingMailboxDotOrgTest : CaldavTest() {
val principal = principalDao.getAll().first() val principal = principalDao.getAll().first()
assertEquals(calendar.id, principal.list) assertEquals(calendar.id, principal.list)
assertEquals("/principals/users/5", principal.principal) assertEquals("/principals/users/5", principal.href)
assertNull(principal.displayName) assertNull(principal.displayName)
assertEquals(CaldavCalendar.INVITE_ACCEPTED, principal.inviteStatus) assertEquals(CaldavCalendar.INVITE_ACCEPTED, principal.inviteStatus)
assertEquals(CaldavCalendar.ACCESS_UNKNOWN, principal.access) assertEquals(CaldavCalendar.ACCESS_UNKNOWN, principal.access.access)
} }
companion object { companion object {

@ -80,10 +80,10 @@ class SharingOwncloudTest : CaldavTest() {
.first() .first()
assertEquals(calendar.id, principal.list) assertEquals(calendar.id, principal.list)
assertEquals("principal:principals/users/user2", principal.principal) assertEquals("principal:principals/users/user2", principal.href)
assertEquals("user2", principal.displayName) assertEquals("user2", principal.name)
assertEquals(CaldavCalendar.INVITE_ACCEPTED, principal.inviteStatus) assertEquals(CaldavCalendar.INVITE_ACCEPTED, principal.inviteStatus)
assertEquals(CaldavCalendar.ACCESS_READ_ONLY, principal.access) assertEquals(ACCESS_READ_ONLY, principal.access.access)
} }
@Test @Test
@ -104,10 +104,10 @@ class SharingOwncloudTest : CaldavTest() {
.first() .first()
assertEquals(calendar.id, principal.list) assertEquals(calendar.id, principal.list)
assertEquals("principals/users/user1", principal.principal) assertEquals("principals/users/user1", principal.href)
assertEquals(null, principal.displayName) assertEquals(null, principal.displayName)
assertEquals(CaldavCalendar.INVITE_ACCEPTED, principal.inviteStatus) assertEquals(CaldavCalendar.INVITE_ACCEPTED, principal.inviteStatus)
assertEquals(CaldavCalendar.ACCESS_OWNER, principal.access) assertEquals(ACCESS_OWNER, principal.access.access)
} }
companion object { companion object {

@ -102,10 +102,10 @@ class SharingSabredavTest : CaldavTest() {
val principal = principalDao.getAll().first() val principal = principalDao.getAll().first()
assertEquals(calendar.id, principal.list) assertEquals(calendar.id, principal.list)
assertEquals("mailto:user@example.com", principal.principal) assertEquals("mailto:user@example.com", principal.href)
assertEquals("Example User", principal.displayName) assertEquals("Example User", principal.displayName)
assertEquals(INVITE_ACCEPTED, principal.inviteStatus) assertEquals(INVITE_ACCEPTED, principal.inviteStatus)
assertEquals(ACCESS_READ_WRITE, principal.access) assertEquals(ACCESS_READ_WRITE, principal.access.access)
} }
@Test @Test
@ -126,10 +126,10 @@ class SharingSabredavTest : CaldavTest() {
.first() .first()
assertEquals(calendar.id, principal.list) assertEquals(calendar.id, principal.list)
assertEquals("/principals/user1", principal.principal) assertEquals("/principals/user1", principal.href)
assertEquals(null, principal.displayName) assertEquals(null, principal.displayName)
assertEquals(INVITE_ACCEPTED, principal.inviteStatus) assertEquals(INVITE_ACCEPTED, principal.inviteStatus)
assertEquals(ACCESS_OWNER, principal.access) assertEquals(ACCESS_OWNER, principal.access.access)
} }
companion object { companion object {

@ -3,8 +3,39 @@ package com.todoroo.astrid.dao
import androidx.room.Database import androidx.room.Database
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import com.todoroo.astrid.data.Task import com.todoroo.astrid.data.Task
import org.tasks.data.* import org.tasks.data.Alarm
import org.tasks.data.AlarmDao
import org.tasks.data.CaldavAccount
import org.tasks.data.CaldavCalendar
import org.tasks.data.CaldavDao
import org.tasks.data.CaldavTask
import org.tasks.data.ContentProviderDao
import org.tasks.data.DeletionDao
import org.tasks.data.Filter
import org.tasks.data.FilterDao
import org.tasks.data.Geofence
import org.tasks.data.GoogleTask
import org.tasks.data.GoogleTaskAccount
import org.tasks.data.GoogleTaskDao
import org.tasks.data.GoogleTaskList
import org.tasks.data.GoogleTaskListDao
import org.tasks.data.LocationDao
import org.tasks.data.Place
import org.tasks.data.Principal
import org.tasks.data.PrincipalAccess
import org.tasks.data.PrincipalDao
import org.tasks.data.Tag
import org.tasks.data.TagDao
import org.tasks.data.TagData
import org.tasks.data.TagDataDao
import org.tasks.data.TaskAttachment
import org.tasks.data.TaskAttachmentDao
import org.tasks.data.TaskDao import org.tasks.data.TaskDao
import org.tasks.data.TaskListMetadata
import org.tasks.data.TaskListMetadataDao
import org.tasks.data.UpgraderDao
import org.tasks.data.UserActivity
import org.tasks.data.UserActivityDao
import org.tasks.notifications.Notification import org.tasks.notifications.Notification
import org.tasks.notifications.NotificationDao import org.tasks.notifications.NotificationDao
@ -28,8 +59,9 @@ import org.tasks.notifications.NotificationDao
CaldavAccount::class, CaldavAccount::class,
GoogleTaskAccount::class, GoogleTaskAccount::class,
Principal::class, Principal::class,
PrincipalAccess::class
], ],
version = 79) version = 80)
abstract class Database : RoomDatabase() { abstract class Database : RoomDatabase() {
abstract fun notificationDao(): NotificationDao abstract fun notificationDao(): NotificationDao
abstract val tagDataDao: TagDataDao abstract val tagDataDao: TagDataDao

@ -24,8 +24,8 @@ import org.tasks.data.CaldavAccount.Companion.SERVER_TASKS
import org.tasks.data.CaldavCalendar import org.tasks.data.CaldavCalendar
import org.tasks.data.CaldavCalendar.Companion.ACCESS_OWNER import org.tasks.data.CaldavCalendar.Companion.ACCESS_OWNER
import org.tasks.data.Principal import org.tasks.data.Principal
import org.tasks.data.Principal.Companion.name
import org.tasks.data.PrincipalDao import org.tasks.data.PrincipalDao
import org.tasks.data.PrincipalWithAccess
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -94,7 +94,7 @@ class CaldavCalendarSettingsActivity : BaseCaldavCalendarSettingsActivity() {
private val canRemovePrincipals: Boolean private val canRemovePrincipals: Boolean
get() = caldavCalendar?.access == ACCESS_OWNER && caldavAccount.canRemovePrincipal get() = caldavCalendar?.access == ACCESS_OWNER && caldavAccount.canRemovePrincipal
private fun onRemove(principal: Principal) { private fun onRemove(principal: PrincipalWithAccess) {
if (requestInProgress()) { if (requestInProgress()) {
return return
} }
@ -106,7 +106,7 @@ class CaldavCalendarSettingsActivity : BaseCaldavCalendarSettingsActivity() {
.show() .show()
} }
private fun removePrincipal(principal: Principal) = lifecycleScope.launch { private fun removePrincipal(principal: PrincipalWithAccess) = lifecycleScope.launch {
try { try {
viewModel.removeUser(caldavAccount, caldavCalendar!!, principal) viewModel.removeUser(caldavAccount, caldavCalendar!!, principal)
} catch (e: Exception) { } catch (e: Exception) {

@ -17,8 +17,8 @@ import org.tasks.data.CaldavCalendar
import org.tasks.data.CaldavCalendar.Companion.ACCESS_READ_WRITE import org.tasks.data.CaldavCalendar.Companion.ACCESS_READ_WRITE
import org.tasks.data.CaldavCalendar.Companion.INVITE_UNKNOWN import org.tasks.data.CaldavCalendar.Companion.INVITE_UNKNOWN
import org.tasks.data.CaldavDao import org.tasks.data.CaldavDao
import org.tasks.data.Principal
import org.tasks.data.PrincipalDao import org.tasks.data.PrincipalDao
import org.tasks.data.PrincipalWithAccess
import org.tasks.sync.SyncAdapters import org.tasks.sync.SyncAdapters
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -103,21 +103,17 @@ class CaldavCalendarViewModel @Inject constructor(
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
provider.forAccount(account, list.url!!).share(account, href) provider.forAccount(account, list.url!!).share(account, href)
} }
principalDao.insert(Principal().apply { val principal = principalDao.getOrCreatePrincipal(account, href)
this.list = list.id principalDao.getOrCreateAccess(list, principal, INVITE_UNKNOWN, ACCESS_READ_WRITE)
principal = href
inviteStatus = INVITE_UNKNOWN
access = ACCESS_READ_WRITE
})
syncAdapters.sync(true) syncAdapters.sync(true)
} }
suspend fun removeUser(account: CaldavAccount, list: CaldavCalendar, principal: Principal) = suspend fun removeUser(account: CaldavAccount, list: CaldavCalendar, principal: PrincipalWithAccess) =
doRequest { doRequest {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
provider.forAccount(account).removePrincipal(account, list, principal) provider.forAccount(account).removePrincipal(account, list, principal.href)
} }
principalDao.delete(principal) principalDao.delete(principal.access)
} }
private suspend fun <T> doRequest(action: suspend () -> T): T? = private suspend fun <T> doRequest(action: suspend () -> T): T? =

@ -43,7 +43,6 @@ import org.tasks.data.CaldavAccount.Companion.SERVER_OWNCLOUD
import org.tasks.data.CaldavAccount.Companion.SERVER_SABREDAV import org.tasks.data.CaldavAccount.Companion.SERVER_SABREDAV
import org.tasks.data.CaldavAccount.Companion.SERVER_TASKS import org.tasks.data.CaldavAccount.Companion.SERVER_TASKS
import org.tasks.data.CaldavCalendar import org.tasks.data.CaldavCalendar
import org.tasks.data.Principal
import org.tasks.ui.DisplayableException import org.tasks.ui.DisplayableException
import org.xmlpull.v1.XmlPullParserException import org.xmlpull.v1.XmlPullParserException
import org.xmlpull.v1.XmlPullParserFactory import org.xmlpull.v1.XmlPullParserFactory
@ -277,37 +276,37 @@ open class CaldavClient(
suspend fun removePrincipal( suspend fun removePrincipal(
account: CaldavAccount, account: CaldavAccount,
calendar: CaldavCalendar, calendar: CaldavCalendar,
principal: Principal, href: String,
) { ) {
when (account.serverType) { when (account.serverType) {
SERVER_TASKS, SERVER_SABREDAV -> removeSabrePrincipal(calendar, principal) SERVER_TASKS, SERVER_SABREDAV -> removeSabrePrincipal(calendar, href)
SERVER_OWNCLOUD -> removeOwncloudPrincipal(calendar, principal) SERVER_OWNCLOUD -> removeOwncloudPrincipal(calendar, href)
else -> throw IllegalArgumentException() else -> throw IllegalArgumentException()
} }
} }
private suspend fun removeOwncloudPrincipal(calendar: CaldavCalendar, principal: Principal) = private suspend fun removeOwncloudPrincipal(calendar: CaldavCalendar, href: String) =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
DavCollection(httpClient, calendar.url!!.toHttpUrl()) DavCollection(httpClient, calendar.url!!.toHttpUrl())
.post( .post(
""" """
<x4:share xmlns:x4="$NS_OWNCLOUD"> <x4:share xmlns:x4="$NS_OWNCLOUD">
<x4:remove> <x4:remove>
<x0:href xmlns:x0="$NS_WEBDAV">${principal.principal}</x0:href> <x0:href xmlns:x0="$NS_WEBDAV">$href</x0:href>
</x4:remove> </x4:remove>
</x4:share> </x4:share>
""".trimIndent().toRequestBody(MIME_XML) """.trimIndent().toRequestBody(MIME_XML)
) {} ) {}
} }
private suspend fun removeSabrePrincipal(calendar: CaldavCalendar, principal: Principal) = private suspend fun removeSabrePrincipal(calendar: CaldavCalendar, href: String) =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
DavCollection(httpClient, calendar.url!!.toHttpUrl()) DavCollection(httpClient, calendar.url!!.toHttpUrl())
.post( .post(
""" """
<D:share-resource xmlns:D="$NS_WEBDAV"> <D:share-resource xmlns:D="$NS_WEBDAV">
<D:sharee> <D:sharee>
<D:href>${principal.principal}</D:href> <D:href>$href</D:href>
<D:share-access> <D:share-access>
<D:no-access /> <D:no-access />
</D:share-access> </D:share-access>

@ -180,12 +180,8 @@ class CaldavSynchronizer @Inject constructor(
localBroadcastManager.broadcastRefreshList() localBroadcastManager.broadcastRefreshList()
} }
resource resource
.principals .principals(account, calendar)
.onEach { it.list = calendar.id } .let { principalDao.deleteRemoved(calendar.id, it.map(PrincipalAccess::id)) }
.let {
principalDao.deleteRemoved(calendar.id, it.mapNotNull { p -> p.principal } )
principalDao.insert(it)
}
sync(calendar, resource, caldavClient.httpClient) sync(calendar, resource, caldavClient.httpClient)
} }
setError(account, "") setError(account, "")
@ -345,6 +341,57 @@ class CaldavSynchronizer @Inject constructor(
Timber.d("SENT %s", caldavTask) Timber.d("SENT %s", caldavTask)
} }
fun Response.principals(
account: CaldavAccount,
list: CaldavCalendar
): List<PrincipalAccess> {
val access = ArrayList<PrincipalAccess>()
this[Invite::class.java]
?.sharees
?.filter { it.href?.let { href -> !isCurrentUser(href) } ?: false }
?.map {
val principal = principalDao.getOrCreatePrincipal(
account,
it.href!!,
it.properties
.find { p -> p is DisplayName }
?.let { name -> (name as DisplayName).displayName }
)
principalDao.getOrCreateAccess(
list,
principal,
invite = it.response?.toStatus ?: INVITE_UNKNOWN,
access = it.access?.access?.toAccess ?: ACCESS_UNKNOWN
)
}
?.let { access.addAll(it) }
this[OCInvite::class.java]?.users
?.map {
val principal = principalDao.getOrCreatePrincipal(account, it.href)
principalDao.getOrCreateAccess(
list,
principal,
it.response.toStatus,
it.access.access.toAccess
)
}
?.let {
if (!isOwncloudOwner) {
val principal = principalDao.getOrCreatePrincipal(
account, this@principals[OCOwnerPrincipal::class.java]?.owner!!
)
access.add(principalDao.getOrCreateAccess(
list,
principal,
INVITE_ACCEPTED,
ACCESS_OWNER
))
}
access.addAll(it)
}
return access
}
companion object { companion object {
init { init {
prodId = ProdId("+//IDN tasks.org//android-" + BuildConfig.VERSION_CODE + "//EN") prodId = ProdId("+//IDN tasks.org//android-" + BuildConfig.VERSION_CODE + "//EN")
@ -387,45 +434,6 @@ class CaldavSynchronizer @Inject constructor(
private fun Response.isCurrentUser(href: String) = private fun Response.isCurrentUser(href: String) =
this[CurrentUserPrincipal::class.java]?.href?.endsWith("$href/") == true this[CurrentUserPrincipal::class.java]?.href?.endsWith("$href/") == true
val Response.principals: List<Principal>
get() {
val principals = ArrayList<Principal>()
this[Invite::class.java]
?.sharees
?.filter { it.href?.let { href -> !isCurrentUser(href) } ?: false }
?.map {
Principal().apply {
principal = it.href
it.properties.find { it is DisplayName }?.let { name ->
displayName = (name as DisplayName).displayName
}
inviteStatus = it.response?.toStatus ?: INVITE_UNKNOWN
access = it.access?.access?.toAccess ?: ACCESS_UNKNOWN
}
}
?.let { principals.addAll(it) }
this[OCInvite::class.java]?.users
?.map {
Principal().apply {
principal = it.href
displayName = it.commonName
inviteStatus = it.response.toStatus
access = it.access.access.toAccess
}
}
?.let {
if (!isOwncloudOwner) {
principals.add(Principal().apply {
principal = this@principals[OCOwnerPrincipal::class.java]?.owner
inviteStatus = INVITE_ACCEPTED
access = ACCESS_OWNER
})
}
principals.addAll(it)
}
return principals
}
private val Property.Name.toAccess: Int private val Property.Name.toAccess: Int
get() = when (this) { get() = when (this) {
SHARED_OWNER, OCAccess.SHARED_OWNER -> ACCESS_OWNER SHARED_OWNER, OCAccess.SHARED_OWNER -> ACCESS_OWNER

@ -29,20 +29,24 @@ import org.tasks.R
import org.tasks.compose.Constants.HALF_KEYLINE import org.tasks.compose.Constants.HALF_KEYLINE
import org.tasks.compose.Constants.ICON_ALPHA import org.tasks.compose.Constants.ICON_ALPHA
import org.tasks.compose.Constants.KEYLINE_FIRST import org.tasks.compose.Constants.KEYLINE_FIRST
import org.tasks.data.CaldavCalendar import org.tasks.data.CaldavCalendar.Companion.INVITE_ACCEPTED
import org.tasks.data.CaldavCalendar.Companion.INVITE_DECLINED import org.tasks.data.CaldavCalendar.Companion.INVITE_DECLINED
import org.tasks.data.CaldavCalendar.Companion.INVITE_INVALID import org.tasks.data.CaldavCalendar.Companion.INVITE_INVALID
import org.tasks.data.CaldavCalendar.Companion.INVITE_NO_RESPONSE import org.tasks.data.CaldavCalendar.Companion.INVITE_NO_RESPONSE
import org.tasks.data.CaldavCalendar.Companion.INVITE_UNKNOWN import org.tasks.data.CaldavCalendar.Companion.INVITE_UNKNOWN
import org.tasks.data.Principal import org.tasks.data.Principal
import org.tasks.data.Principal.Companion.name import org.tasks.data.PrincipalAccess
import org.tasks.data.PrincipalWithAccess
private val principals = listOf( private val principals = listOf(
Principal().apply { PrincipalWithAccess(
displayName = "user1" PrincipalAccess(list = 0, invite = INVITE_INVALID),
inviteStatus = INVITE_INVALID Principal(account = 0, href = "", displayName = "user1")
}, ),
Principal().apply { displayName = "a really really really really really long display name" }, PrincipalWithAccess(
PrincipalAccess(list = 0, invite = INVITE_ACCEPTED),
Principal(account = 0, href = "", displayName = "a really really really really really long display name")
)
) )
@Preview(showBackground = true, backgroundColor = 0xFFFFFF) @Preview(showBackground = true, backgroundColor = 0xFFFFFF)
@ -66,8 +70,8 @@ private fun NotOwner() = MaterialTheme {
object ListSettingsComposables { object ListSettingsComposables {
@Composable @Composable
fun PrincipalList( fun PrincipalList(
principals: List<Principal>, principals: List<PrincipalWithAccess>,
onRemove: ((Principal) -> Unit)?, onRemove: ((PrincipalWithAccess) -> Unit)?,
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
@ -88,8 +92,8 @@ object ListSettingsComposables {
@Composable @Composable
fun PrincipalRow( fun PrincipalRow(
principal: Principal, principal: PrincipalWithAccess,
onRemove: ((Principal) -> Unit)?, onRemove: ((PrincipalWithAccess) -> Unit)?,
) { ) {
Row( Row(
Modifier Modifier
@ -121,7 +125,7 @@ object ListSettingsComposables {
style = MaterialTheme.typography.body1, style = MaterialTheme.typography.body1,
color = colors.onBackground, color = colors.onBackground,
) )
if (principal.inviteStatus != CaldavCalendar.INVITE_ACCEPTED) { if (principal.inviteStatus != INVITE_ACCEPTED) {
Text( Text(
stringResource(when (principal.inviteStatus) { stringResource(when (principal.inviteStatus) {
INVITE_UNKNOWN, INVITE_NO_RESPONSE -> INVITE_UNKNOWN, INVITE_NO_RESPONSE ->

@ -243,13 +243,13 @@ SELECT EXISTS(SELECT 1
abstract suspend fun getCalendars(tasks: List<Long>): List<String> abstract suspend fun getCalendars(tasks: List<Long>): List<String>
@Query(""" @Query("""
SELECT caldav_lists.*, COUNT(DISTINCT(tasks._id)) AS count, COUNT(DISTINCT(principals.principal_id)) AS principals SELECT caldav_lists.*, COUNT(DISTINCT(tasks._id)) AS count, COUNT(DISTINCT(principal_access.id)) AS principals
FROM caldav_lists FROM caldav_lists
LEFT JOIN caldav_tasks LEFT JOIN caldav_tasks
ON caldav_tasks.cd_calendar = caldav_lists.cdl_uuid ON caldav_tasks.cd_calendar = caldav_lists.cdl_uuid
LEFT JOIN tasks ON caldav_tasks.cd_task = tasks._id AND tasks.deleted = 0 AND tasks.completed = 0 AND LEFT JOIN tasks ON caldav_tasks.cd_task = tasks._id AND tasks.deleted = 0 AND tasks.completed = 0 AND
tasks.hideUntil < :now AND cd_deleted = 0 tasks.hideUntil < :now AND cd_deleted = 0
LEFT JOIN principals ON caldav_lists.cdl_id = principals.principal_list LEFT JOIN principal_access ON caldav_lists.cdl_id = principal_access.list
WHERE caldav_lists.cdl_account = :uuid WHERE caldav_lists.cdl_account = :uuid
GROUP BY caldav_lists.cdl_uuid GROUP BY caldav_lists.cdl_uuid
""") """)

@ -1,76 +1,38 @@
package org.tasks.data package org.tasks.data
import androidx.room.* import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
@Entity( @Entity(
tableName = "principals", tableName = "principals",
foreignKeys = [ForeignKey( foreignKeys = [
entity = CaldavCalendar::class, ForeignKey(
parentColumns = arrayOf("cdl_id"), entity = CaldavAccount::class,
childColumns = arrayOf("principal_list"), parentColumns = arrayOf("cda_id"),
childColumns = arrayOf("account"),
onDelete = ForeignKey.CASCADE onDelete = ForeignKey.CASCADE
)],
indices = [Index(value = ["principal_list", "principal"], unique = true)]
) )
class Principal { ],
@PrimaryKey(autoGenerate = true) indices = [Index(value = ["account", "href"], unique = true)]
@ColumnInfo(name = "principal_id") )
@Transient data class Principal(
var id: Long = 0 @PrimaryKey(autoGenerate = true) var id: Long = 0,
val account: Long,
@ColumnInfo(name = "principal_list") val href: String,
var list: Long = 0 var email: String? = null,
@ColumnInfo(name = "display_name") var displayName: String? = null
@ColumnInfo(name = "principal") ) {
var principal: String? = null val name: String
get() = displayName
@ColumnInfo(name = "display_name") ?: href
var displayName: String? = null .replace(MAILTO, "")
.replaceFirst(LAST_SEGMENT, "$1")
@ColumnInfo(name = "invite")
var inviteStatus: Int = CaldavCalendar.INVITE_UNKNOWN
@ColumnInfo(name = "access")
var access: Int = CaldavCalendar.ACCESS_UNKNOWN
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Principal
if (id != other.id) return false
if (list != other.list) return false
if (principal != other.principal) return false
if (displayName != other.displayName) return false
if (inviteStatus != other.inviteStatus) return false
if (access != other.access) return false
return true
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + list.hashCode()
result = 31 * result + (principal?.hashCode() ?: 0)
result = 31 * result + (displayName?.hashCode() ?: 0)
result = 31 * result + inviteStatus
result = 31 * result + access
return result
}
override fun toString(): String {
return "Principal(id=$id, list=$list, principal=$principal, displayName=$displayName, inviteStatus=$inviteStatus, access=$access)"
}
companion object { companion object {
private val MAILTO = "^mailto:".toRegex() private val MAILTO = "^mailto:".toRegex()
private val LAST_SEGMENT = ".*/([^/]+).*".toRegex() private val LAST_SEGMENT = ".*/([^/]+).*".toRegex()
val Principal.name: String?
get() = displayName
?: principal
?.replace(MAILTO, "")
?.replaceFirst(LAST_SEGMENT, "$1")
} }
} }

@ -0,0 +1,36 @@
package org.tasks.data
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
import org.tasks.data.CaldavCalendar.Companion.ACCESS_UNKNOWN
import org.tasks.data.CaldavCalendar.Companion.INVITE_UNKNOWN
@Entity(
tableName = "principal_access",
foreignKeys = [
ForeignKey(
entity = Principal::class,
parentColumns = arrayOf("id"),
childColumns = arrayOf("principal"),
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = CaldavCalendar::class,
parentColumns = arrayOf("cdl_id"),
childColumns = arrayOf("list"),
onDelete = ForeignKey.CASCADE
)],
indices = [
Index(value = ["list", "principal"], unique = true),
Index(value = ["principal"])
]
)
data class PrincipalAccess(
@PrimaryKey(autoGenerate = true) var id: Long = 0,
val principal: Long = 0,
val list: Long,
var invite: Int = INVITE_UNKNOWN,
var access: Int = ACCESS_UNKNOWN
)

@ -4,33 +4,68 @@ import androidx.lifecycle.LiveData
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Delete import androidx.room.Delete
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Update
@Dao @Dao
interface PrincipalDao { interface PrincipalDao {
@Insert @Insert
fun insert(principal: Principal) fun insert(principal: Principal): Long
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert
fun insert(principals: List<Principal>) fun insert(access: PrincipalAccess): Long
@Update
fun update(access: PrincipalAccess)
@Query(""" @Query("""
DELETE DELETE
FROM principals FROM principal_access
WHERE principal_list = :list WHERE list = :list
AND principal NOT IN (:principals)""") AND id NOT IN (:access)""")
fun deleteRemoved(list: Long, principals: List<String>) fun deleteRemoved(list: Long, access: List<Long>)
@Delete @Delete
fun delete(principal: Principal) fun delete(access: PrincipalAccess)
@Delete @Transaction
fun delete(principals: List<Principal>) @Query("SELECT * FROM principal_access")
fun getAll(): List<PrincipalWithAccess>
fun getOrCreatePrincipal(account: CaldavAccount, href: String, displayName: String? = null) =
findPrincipal(account.id, href)
?: Principal(account = account.id, href = href, displayName = displayName)
.apply { id = insert(this) }
fun getOrCreateAccess(
calendar: CaldavCalendar,
principal: Principal,
invite: Int,
access: Int,
): PrincipalAccess =
findAccess(calendar.id, principal.id)
?.apply {
if (this.access != access || this.invite != invite) {
this.access = access
this.invite = invite
update(this)
}
}
?: PrincipalAccess(
principal = principal.id,
list = calendar.id,
invite = invite,
access = access
).apply { id = insert(this) }
@Query("SELECT * FROM principals WHERE account = :account AND href = :href")
fun findPrincipal(account: Long, href: String): Principal?
@Query("SELECT * FROM principals") @Query("SELECT * FROM principal_access WHERE list = :list and principal = :principal")
fun getAll(): List<Principal> fun findAccess(list: Long, principal: Long): PrincipalAccess?
@Query("SELECT * FROM principals WHERE principal_list = :id") @Transaction
fun getPrincipals(id: Long): LiveData<List<Principal>> @Query("SELECT * FROM principal_access WHERE list = :id")
fun getPrincipals(id: Long): LiveData<List<PrincipalWithAccess>>
} }

@ -0,0 +1,19 @@
package org.tasks.data
import androidx.room.Embedded
import androidx.room.Relation
data class PrincipalWithAccess(
@Embedded val access: PrincipalAccess,
@Relation(
parentColumn = "principal",
entityColumn = "id"
)
val principal: Principal
) {
val displayName get() = principal.displayName
val list get() = access.list
val href get() = principal.href
val inviteStatus get() = access.invite
val name get() = principal.name
}

@ -383,6 +383,27 @@ object Migrations {
} }
} }
private val MIGRATION_79_80 = object : Migration(79, 80) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("DROP TABLE `principals`")
database.execSQL(
"CREATE TABLE IF NOT EXISTS `principals` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `account` INTEGER NOT NULL, `href` TEXT NOT NULL, `email` TEXT, `display_name` TEXT, FOREIGN KEY(`account`) REFERENCES `caldav_accounts`(`cda_id`) ON UPDATE NO ACTION ON DELETE CASCADE )"
)
database.execSQL(
"CREATE UNIQUE INDEX IF NOT EXISTS `index_principals_account_href` ON `principals` (`account`, `href`)"
)
database.execSQL(
"CREATE TABLE IF NOT EXISTS `principal_access` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `principal` INTEGER NOT NULL, `list` INTEGER NOT NULL, `invite` INTEGER NOT NULL, `access` INTEGER NOT NULL, FOREIGN KEY(`principal`) REFERENCES `principals`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`list`) REFERENCES `caldav_lists`(`cdl_id`) ON UPDATE NO ACTION ON DELETE CASCADE )"
)
database.execSQL(
"CREATE UNIQUE INDEX IF NOT EXISTS `index_principal_access_list_principal` ON `principal_access` (`list`, `principal`)"
)
database.execSQL(
"CREATE INDEX IF NOT EXISTS `index_principal_access_principal` ON `principal_access` (`principal`)"
)
}
}
val MIGRATIONS = arrayOf( val MIGRATIONS = arrayOf(
MIGRATION_35_36, MIGRATION_35_36,
MIGRATION_36_37, MIGRATION_36_37,
@ -419,6 +440,7 @@ object Migrations {
MIGRATION_76_77, MIGRATION_76_77,
MIGRATION_77_78, MIGRATION_77_78,
MIGRATION_78_79, MIGRATION_78_79,
MIGRATION_79_80,
) )
private fun noop(from: Int, to: Int): Migration = object : Migration(from, to) { private fun noop(from: Int, to: Int): Migration = object : Migration(from, to) {

@ -2,32 +2,25 @@ package org.tasks.data
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
import org.tasks.data.Principal.Companion.name
class PrincipalTest { class PrincipalTest {
@Test @Test
fun lastSegmentTrailingSlash() { fun lastSegmentTrailingSlash() {
val principal = Principal().apply { val principal = Principal(account = 0, href = "/principals/users/user1")
principal = "principals/users/user1/"
}
assertEquals("user1", principal.name) assertEquals("user1", principal.name)
} }
@Test @Test
fun lastSegmentNoTrailingSlash() { fun lastSegmentNoTrailingSlash() {
val principal = Principal().apply { val principal = Principal(account = 0, href = "principals/users/user1")
principal = "principals/users/user1"
}
assertEquals("user1", principal.name) assertEquals("user1", principal.name)
} }
@Test @Test
fun stripMailto() { fun stripMailto() {
val principal = Principal().apply { val principal = Principal(account = 0, href = "mailto:user@example.com")
principal = "mailto:user@example.com"
}
assertEquals("user@example.com", principal.name) assertEquals("user@example.com", principal.name)
} }

Loading…
Cancel
Save