mirror of https://github.com/iSoron/uhabits.git
parent
340bde9f69
commit
73387e5e63
@ -0,0 +1,216 @@
|
|||||||
|
package org.isoron.uhabits.core.models.memory
|
||||||
|
|
||||||
|
import org.isoron.uhabits.core.models.HabitGroup
|
||||||
|
import org.isoron.uhabits.core.models.HabitGroupList
|
||||||
|
import org.isoron.uhabits.core.models.HabitList.Order
|
||||||
|
import org.isoron.uhabits.core.models.HabitMatcher
|
||||||
|
import org.isoron.uhabits.core.utils.DateUtils.Companion.getTodayWithOffset
|
||||||
|
import java.util.LinkedList
|
||||||
|
import java.util.Objects
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In-memory implementation of [HabitGroupList].
|
||||||
|
*/
|
||||||
|
class MemoryHabitGroupList : HabitGroupList {
|
||||||
|
private val list = LinkedList<HabitGroup>()
|
||||||
|
|
||||||
|
@get:Synchronized
|
||||||
|
override var primaryOrder = Order.BY_POSITION
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
comparator = getComposedComparatorByOrder(primaryOrder, secondaryOrder)
|
||||||
|
resort()
|
||||||
|
}
|
||||||
|
|
||||||
|
@get:Synchronized
|
||||||
|
override var secondaryOrder = Order.BY_NAME_ASC
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
comparator = getComposedComparatorByOrder(primaryOrder, secondaryOrder)
|
||||||
|
resort()
|
||||||
|
}
|
||||||
|
|
||||||
|
private var comparator: Comparator<HabitGroup>? =
|
||||||
|
getComposedComparatorByOrder(primaryOrder, secondaryOrder)
|
||||||
|
private var parent: MemoryHabitGroupList? = null
|
||||||
|
|
||||||
|
constructor() : super()
|
||||||
|
constructor(
|
||||||
|
matcher: HabitMatcher,
|
||||||
|
comparator: Comparator<HabitGroup>?,
|
||||||
|
parent: MemoryHabitGroupList
|
||||||
|
) : super(matcher) {
|
||||||
|
this.parent = parent
|
||||||
|
this.comparator = comparator
|
||||||
|
primaryOrder = parent.primaryOrder
|
||||||
|
secondaryOrder = parent.secondaryOrder
|
||||||
|
parent.observable.addListener { loadFromParent() }
|
||||||
|
loadFromParent()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
@Throws(IllegalArgumentException::class)
|
||||||
|
override fun add(habitGroup: HabitGroup) {
|
||||||
|
throwIfHasParent()
|
||||||
|
require(!list.contains(habitGroup)) { "habit already added" }
|
||||||
|
val id = habitGroup.id
|
||||||
|
if (id != null && getById(id) != null) throw RuntimeException("duplicate id")
|
||||||
|
if (id == null) habitGroup.id = list.size.toLong()
|
||||||
|
list.addLast(habitGroup)
|
||||||
|
resort()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun getById(id: Long): HabitGroup? {
|
||||||
|
for (h in list) {
|
||||||
|
checkNotNull(h.id)
|
||||||
|
if (h.id == id) return h
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun getByUUID(uuid: String?): HabitGroup? {
|
||||||
|
for (h in list) if (Objects.requireNonNull(h.uuid) == uuid) return h
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun getByPosition(position: Int): HabitGroup {
|
||||||
|
return list[position]
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun getFiltered(matcher: HabitMatcher?): HabitGroupList {
|
||||||
|
return MemoryHabitGroupList(matcher!!, comparator, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getComposedComparatorByOrder(
|
||||||
|
firstOrder: Order,
|
||||||
|
secondOrder: Order?
|
||||||
|
): Comparator<HabitGroup> {
|
||||||
|
return Comparator { h1: HabitGroup, h2: HabitGroup ->
|
||||||
|
val firstResult = getComparatorByOrder(firstOrder).compare(h1, h2)
|
||||||
|
if (firstResult != 0 || secondOrder == null) {
|
||||||
|
return@Comparator firstResult
|
||||||
|
}
|
||||||
|
getComparatorByOrder(secondOrder).compare(h1, h2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getComparatorByOrder(order: Order): Comparator<HabitGroup> {
|
||||||
|
val nameComparatorAsc = Comparator<HabitGroup> { habit1, habit2 ->
|
||||||
|
habit1.name.compareTo(habit2.name)
|
||||||
|
}
|
||||||
|
val nameComparatorDesc =
|
||||||
|
Comparator { h1: HabitGroup, h2: HabitGroup -> nameComparatorAsc.compare(h2, h1) }
|
||||||
|
val colorComparatorAsc = Comparator<HabitGroup> { (color1), (color2) ->
|
||||||
|
color1.compareTo(color2)
|
||||||
|
}
|
||||||
|
val colorComparatorDesc =
|
||||||
|
Comparator { h1: HabitGroup, h2: HabitGroup -> colorComparatorAsc.compare(h2, h1) }
|
||||||
|
val scoreComparatorDesc =
|
||||||
|
Comparator<HabitGroup> { habit1, habit2 ->
|
||||||
|
val today = getTodayWithOffset()
|
||||||
|
habit1.scores[today].value.compareTo(habit2.scores[today].value)
|
||||||
|
}
|
||||||
|
val scoreComparatorAsc =
|
||||||
|
Comparator { h1: HabitGroup, h2: HabitGroup -> scoreComparatorDesc.compare(h2, h1) }
|
||||||
|
val positionComparator =
|
||||||
|
Comparator<HabitGroup> { habit1, habit2 -> habit1.position.compareTo(habit2.position) }
|
||||||
|
val statusComparatorDesc = Comparator { h1: HabitGroup, h2: HabitGroup ->
|
||||||
|
if (h1.isCompletedToday() != h2.isCompletedToday()) {
|
||||||
|
return@Comparator if (h1.isCompletedToday()) -1 else 1
|
||||||
|
}
|
||||||
|
val today = getTodayWithOffset()
|
||||||
|
val v1 = h1.scores[today].value
|
||||||
|
val v2 = h2.scores[today].value
|
||||||
|
v2.compareTo(v1)
|
||||||
|
}
|
||||||
|
val statusComparatorAsc =
|
||||||
|
Comparator { h1: HabitGroup, h2: HabitGroup -> statusComparatorDesc.compare(h2, h1) }
|
||||||
|
return when {
|
||||||
|
order === Order.BY_POSITION -> positionComparator
|
||||||
|
order === Order.BY_NAME_ASC -> nameComparatorAsc
|
||||||
|
order === Order.BY_NAME_DESC -> nameComparatorDesc
|
||||||
|
order === Order.BY_COLOR_ASC -> colorComparatorAsc
|
||||||
|
order === Order.BY_COLOR_DESC -> colorComparatorDesc
|
||||||
|
order === Order.BY_SCORE_DESC -> scoreComparatorDesc
|
||||||
|
order === Order.BY_SCORE_ASC -> scoreComparatorAsc
|
||||||
|
order === Order.BY_STATUS_DESC -> statusComparatorDesc
|
||||||
|
order === Order.BY_STATUS_ASC -> statusComparatorAsc
|
||||||
|
else -> throw IllegalStateException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun indexOf(h: HabitGroup): Int {
|
||||||
|
return list.indexOf(h)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun iterator(): Iterator<HabitGroup> {
|
||||||
|
return ArrayList(list).iterator()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun remove(h: HabitGroup) {
|
||||||
|
throwIfHasParent()
|
||||||
|
list.remove(h)
|
||||||
|
observable.notifyListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun reorder(from: HabitGroup, to: HabitGroup) {
|
||||||
|
throwIfHasParent()
|
||||||
|
check(!(primaryOrder !== Order.BY_POSITION)) { "cannot reorder automatically sorted list" }
|
||||||
|
require(indexOf(from) >= 0) { "list does not contain (from) habit" }
|
||||||
|
val toPos = indexOf(to)
|
||||||
|
require(toPos >= 0) { "list does not contain (to) habit" }
|
||||||
|
list.remove(from)
|
||||||
|
list.add(toPos, from)
|
||||||
|
var position = 0
|
||||||
|
for (h in list) h.position = position++
|
||||||
|
observable.notifyListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun size(): Int {
|
||||||
|
return list.size
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun update(habitGroups: List<HabitGroup>) {
|
||||||
|
resort()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun throwIfHasParent() {
|
||||||
|
check(parent == null) {
|
||||||
|
"Filtered lists cannot be modified directly. " +
|
||||||
|
"You should modify the parent list instead."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun loadFromParent() {
|
||||||
|
checkNotNull(parent)
|
||||||
|
list.clear()
|
||||||
|
for (h in parent!!) if (filter.matches(h)) list.add(h)
|
||||||
|
resort()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun resort() {
|
||||||
|
for (hgr in list) {
|
||||||
|
hgr.habitList.primaryOrder = primaryOrder
|
||||||
|
hgr.habitList.secondaryOrder = secondaryOrder
|
||||||
|
hgr.habitList.resort()
|
||||||
|
|
||||||
|
hgr.habitGroupList.primaryOrder = primaryOrder
|
||||||
|
hgr.habitGroupList.secondaryOrder = secondaryOrder
|
||||||
|
hgr.habitGroupList.resort()
|
||||||
|
}
|
||||||
|
if (comparator != null) list.sortWith(comparator!!)
|
||||||
|
observable.notifyListeners()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,200 @@
|
|||||||
|
package org.isoron.uhabits.core.models.sqlite
|
||||||
|
|
||||||
|
import org.isoron.uhabits.core.database.Repository
|
||||||
|
import org.isoron.uhabits.core.models.HabitGroup
|
||||||
|
import org.isoron.uhabits.core.models.HabitGroupList
|
||||||
|
import org.isoron.uhabits.core.models.HabitList.Order
|
||||||
|
import org.isoron.uhabits.core.models.HabitMatcher
|
||||||
|
import org.isoron.uhabits.core.models.ModelFactory
|
||||||
|
import org.isoron.uhabits.core.models.memory.MemoryHabitGroupList
|
||||||
|
import org.isoron.uhabits.core.models.sqlite.records.HabitGroupRecord
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of a [HabitGroupList] that is backed by SQLite.
|
||||||
|
*/
|
||||||
|
class SQLiteHabitGroupList @Inject constructor(private val modelFactory: ModelFactory) : HabitGroupList() {
|
||||||
|
private val repository: Repository<HabitGroupRecord> = modelFactory.buildHabitGroupListRepository()
|
||||||
|
private val list: MemoryHabitGroupList = MemoryHabitGroupList()
|
||||||
|
private var loaded = false
|
||||||
|
private fun loadRecords() {
|
||||||
|
if (loaded) return
|
||||||
|
loaded = true
|
||||||
|
list.removeAll()
|
||||||
|
val records = repository.findAll("order by position")
|
||||||
|
var shouldRebuildOrder = false
|
||||||
|
for ((expectedPosition, rec) in records.withIndex()) {
|
||||||
|
if (rec.position != expectedPosition) shouldRebuildOrder = true
|
||||||
|
val h = modelFactory.buildHabitGroup()
|
||||||
|
rec.copyTo(h)
|
||||||
|
list.add(h)
|
||||||
|
}
|
||||||
|
if (shouldRebuildOrder) rebuildOrder()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun add(habitGroup: HabitGroup) {
|
||||||
|
loadRecords()
|
||||||
|
habitGroup.position = size()
|
||||||
|
val record = HabitGroupRecord()
|
||||||
|
record.copyFrom(habitGroup)
|
||||||
|
repository.save(record)
|
||||||
|
habitGroup.id = record.id
|
||||||
|
list.add(habitGroup)
|
||||||
|
observable.notifyListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun getById(id: Long): HabitGroup? {
|
||||||
|
loadRecords()
|
||||||
|
return list.getById(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun getByUUID(uuid: String?): HabitGroup? {
|
||||||
|
loadRecords()
|
||||||
|
return list.getByUUID(uuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun getByPosition(position: Int): HabitGroup {
|
||||||
|
loadRecords()
|
||||||
|
return list.getByPosition(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun getFiltered(matcher: HabitMatcher?): HabitGroupList {
|
||||||
|
loadRecords()
|
||||||
|
return list.getFiltered(matcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
@set:Synchronized
|
||||||
|
override var primaryOrder: Order
|
||||||
|
get() = list.primaryOrder
|
||||||
|
set(order) {
|
||||||
|
list.primaryOrder = order
|
||||||
|
observable.notifyListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
@set:Synchronized
|
||||||
|
override var secondaryOrder: Order
|
||||||
|
get() = list.secondaryOrder
|
||||||
|
set(order) {
|
||||||
|
list.secondaryOrder = order
|
||||||
|
observable.notifyListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun indexOf(h: HabitGroup): Int {
|
||||||
|
loadRecords()
|
||||||
|
return list.indexOf(h)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun iterator(): Iterator<HabitGroup> {
|
||||||
|
loadRecords()
|
||||||
|
return list.iterator()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun rebuildOrder() {
|
||||||
|
val records = repository.findAll("order by position")
|
||||||
|
repository.executeAsTransaction {
|
||||||
|
for ((pos, r) in records.withIndex()) {
|
||||||
|
if (r.position != pos) {
|
||||||
|
r.position = pos
|
||||||
|
repository.save(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun remove(h: HabitGroup) {
|
||||||
|
loadRecords()
|
||||||
|
list.remove(h)
|
||||||
|
val record = repository.find(
|
||||||
|
h.id!!
|
||||||
|
) ?: throw RuntimeException("habit not in database")
|
||||||
|
repository.executeAsTransaction {
|
||||||
|
repository.remove(record)
|
||||||
|
}
|
||||||
|
rebuildOrder()
|
||||||
|
observable.notifyListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun removeAll() {
|
||||||
|
list.removeAll()
|
||||||
|
repository.execSQL("delete from habits")
|
||||||
|
repository.execSQL("delete from repetitions")
|
||||||
|
observable.notifyListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun reorder(from: HabitGroup, to: HabitGroup) {
|
||||||
|
loadRecords()
|
||||||
|
list.reorder(from, to)
|
||||||
|
val fromRecord = repository.find(
|
||||||
|
from.id!!
|
||||||
|
)
|
||||||
|
val toRecord = repository.find(
|
||||||
|
to.id!!
|
||||||
|
)
|
||||||
|
if (fromRecord == null) throw RuntimeException("habit not in database")
|
||||||
|
if (toRecord == null) throw RuntimeException("habit not in database")
|
||||||
|
if (toRecord.position!! < fromRecord.position!!) {
|
||||||
|
repository.execSQL(
|
||||||
|
"update habits set position = position + 1 " +
|
||||||
|
"where position >= ? and position < ?",
|
||||||
|
toRecord.position!!,
|
||||||
|
fromRecord.position!!
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
repository.execSQL(
|
||||||
|
"update habits set position = position - 1 " +
|
||||||
|
"where position > ? and position <= ?",
|
||||||
|
fromRecord.position!!,
|
||||||
|
toRecord.position!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fromRecord.position = toRecord.position
|
||||||
|
repository.save(fromRecord)
|
||||||
|
observable.notifyListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun repair() {
|
||||||
|
loadRecords()
|
||||||
|
rebuildOrder()
|
||||||
|
observable.notifyListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun size(): Int {
|
||||||
|
loadRecords()
|
||||||
|
return list.size()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun update(habitGroups: List<HabitGroup>) {
|
||||||
|
loadRecords()
|
||||||
|
list.update(habitGroups)
|
||||||
|
for (h in habitGroups) {
|
||||||
|
val record = repository.find(h.id!!) ?: continue
|
||||||
|
record.copyFrom(h)
|
||||||
|
repository.save(record)
|
||||||
|
}
|
||||||
|
observable.notifyListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resort() {
|
||||||
|
list.resort()
|
||||||
|
observable.notifyListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun reload() {
|
||||||
|
loaded = false
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,100 @@
|
|||||||
|
package org.isoron.uhabits.core.models.sqlite.records
|
||||||
|
|
||||||
|
import org.isoron.uhabits.core.database.Column
|
||||||
|
import org.isoron.uhabits.core.database.Table
|
||||||
|
import org.isoron.uhabits.core.models.HabitGroup
|
||||||
|
import org.isoron.uhabits.core.models.PaletteColor
|
||||||
|
import org.isoron.uhabits.core.models.Reminder
|
||||||
|
import org.isoron.uhabits.core.models.WeekdayList
|
||||||
|
import java.util.Objects.requireNonNull
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The SQLite database record corresponding to a [HabitGroup].
|
||||||
|
*/
|
||||||
|
@Table(name = "habitgroups")
|
||||||
|
class HabitGroupRecord {
|
||||||
|
@field:Column
|
||||||
|
var description: String? = null
|
||||||
|
|
||||||
|
@field:Column
|
||||||
|
var question: String? = null
|
||||||
|
|
||||||
|
@field:Column
|
||||||
|
var name: String? = null
|
||||||
|
|
||||||
|
@field:Column
|
||||||
|
var color: Int? = null
|
||||||
|
|
||||||
|
@field:Column
|
||||||
|
var position: Int? = null
|
||||||
|
|
||||||
|
@field:Column(name = "reminder_hour")
|
||||||
|
var reminderHour: Int? = null
|
||||||
|
|
||||||
|
@field:Column(name = "reminder_min")
|
||||||
|
var reminderMin: Int? = null
|
||||||
|
|
||||||
|
@field:Column(name = "reminder_days")
|
||||||
|
var reminderDays: Int? = null
|
||||||
|
|
||||||
|
@field:Column
|
||||||
|
var highlight: Int? = null
|
||||||
|
|
||||||
|
@field:Column
|
||||||
|
var archived: Int? = null
|
||||||
|
|
||||||
|
@field:Column
|
||||||
|
var id: Long? = null
|
||||||
|
|
||||||
|
@field:Column
|
||||||
|
var uuid: String? = null
|
||||||
|
|
||||||
|
@field:Column(name = "parent_id")
|
||||||
|
var parentID: Long? = null
|
||||||
|
|
||||||
|
@field:Column(name = "parent_uuid")
|
||||||
|
var parentUUID: String? = null
|
||||||
|
|
||||||
|
fun copyFrom(model: HabitGroup) {
|
||||||
|
id = model.id
|
||||||
|
name = model.name
|
||||||
|
description = model.description
|
||||||
|
highlight = 0
|
||||||
|
color = model.color.paletteIndex
|
||||||
|
archived = if (model.isArchived) 1 else 0
|
||||||
|
position = model.position
|
||||||
|
question = model.question
|
||||||
|
uuid = model.uuid
|
||||||
|
reminderDays = 0
|
||||||
|
reminderMin = null
|
||||||
|
reminderHour = null
|
||||||
|
parentID = model.parentID
|
||||||
|
parentUUID = model.parentUUID
|
||||||
|
if (model.hasReminder()) {
|
||||||
|
val reminder = model.reminder
|
||||||
|
reminderHour = requireNonNull(reminder)!!.hour
|
||||||
|
reminderMin = reminder!!.minute
|
||||||
|
reminderDays = reminder.days.toInteger()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun copyTo(habitGroup: HabitGroup) {
|
||||||
|
habitGroup.id = id
|
||||||
|
habitGroup.name = name!!
|
||||||
|
habitGroup.description = description!!
|
||||||
|
habitGroup.question = question!!
|
||||||
|
habitGroup.color = PaletteColor(color!!)
|
||||||
|
habitGroup.isArchived = archived != 0
|
||||||
|
habitGroup.position = position!!
|
||||||
|
habitGroup.uuid = uuid
|
||||||
|
habitGroup.parentID = parentID
|
||||||
|
habitGroup.parentUUID = parentUUID
|
||||||
|
if (reminderHour != null && reminderMin != null) {
|
||||||
|
habitGroup.reminder = Reminder(
|
||||||
|
reminderHour!!,
|
||||||
|
reminderMin!!,
|
||||||
|
WeekdayList(reminderDays!!)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,2 +1,21 @@
|
|||||||
alter table Habits add column skip_days integer not null default 0;
|
alter table Habits add column skip_days integer not null default 0;
|
||||||
alter table Habits add column skip_days_list integer not null default 0;
|
alter table Habits add column skip_days_list integer not null default 0;
|
||||||
|
alter table Habits add column parent_id integer;
|
||||||
|
alter table Habits add column parent_uuid text;
|
||||||
|
|
||||||
|
create table HabitGroups (
|
||||||
|
id integer primary key autoincrement,
|
||||||
|
archived integer,
|
||||||
|
color integer,
|
||||||
|
description text not null default "",
|
||||||
|
highlight integer,
|
||||||
|
name text,
|
||||||
|
position integer,
|
||||||
|
reminder_days integer not null default 127,
|
||||||
|
reminder_hour integer,
|
||||||
|
reminder_min integer,
|
||||||
|
question text not null default "",
|
||||||
|
uuid text,
|
||||||
|
parent_id integer,
|
||||||
|
parent_uuid integer
|
||||||
|
);
|
Loading…
Reference in new issue