Skip to content

Commit

Permalink
Merge pull request #442 from amberin/174-id-links-for-notebooks-dont-…
Browse files Browse the repository at this point in the history
…work

Parse notebook properties and allow linking to notebooks
  • Loading branch information
amberin authored Jan 10, 2025
2 parents 38c2e5a + 13794d4 commit 69306b1
Show file tree
Hide file tree
Showing 21 changed files with 1,842 additions and 66 deletions.
1,534 changes: 1,534 additions & 0 deletions app/schemas/com.orgzly.android.db.OrgzlyDatabase/157.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.instanceOf
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Test


Expand Down Expand Up @@ -61,7 +60,7 @@ class InternalLinksTest : OrgzlyTest() {
"book-b",
"""
:PROPERTIES:
dd791937-3fb6-4018-8d5d-b278e0e52c80
:ID: dd791937-3fb6-4018-8d5d-b278e0e52c80
:END:
* Note [b-1]
Expand Down Expand Up @@ -138,18 +137,14 @@ class InternalLinksTest : OrgzlyTest() {
.perform(clickClickableSpan("id:note-with-this-id-does-not-exist"))
SystemClock.sleep(500)
onSnackbar()
.check(matches(withText("Note with “ID” property set to “note-with-this-id-does-not-exist” not found")))
.check(matches(withText("No note or book found with the “ID” property set to “note-with-this-id-does-not-exist”")))
}

@Test
@Ignore("Parsing PROPERTIES drawer from book preface is not supported yet")
fun testLinkToBookById() {
onNoteInBook(7, R.id.item_head_content_view)
.perform(clickClickableSpan("Link to book-b by id"))

// onSnackbar()
// .check(matches(withText("Note with “ID” property set to “dd791937-3fb6-4018-8d5d-b278e0e52c80” not found")))

// In book
onView(withId(R.id.fragment_book_view_flipper)).check(matches(isDisplayed()))

Expand Down
103 changes: 98 additions & 5 deletions app/src/androidTest/java/com/orgzly/android/misc/BookParsingTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
import java.io.InputStream;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;

Expand Down Expand Up @@ -79,14 +81,14 @@ public void testIndented2() {
}

@Test
public void testEmptyProperties() {
public void testEmptyNoteProperties() {
onBook("* Note 1\n :PROPERTIES:\n :END:").onLoad()
.isWhenSaved("* Note 1\n");

}

@Test
public void testProperties() {
public void testNoteProperties() {
onBook("* Note 1\n" +
" :PROPERTIES:\n" +
" :name: value\n" +
Expand All @@ -98,7 +100,7 @@ public void testProperties() {
}

@Test
public void testPropertiesMultiple() {
public void testNotePropertiesMultiple() {
onBook("* Note 1\n" +
" :PROPERTIES:\n" +
" :name2: value2\n" +
Expand All @@ -112,7 +114,7 @@ public void testPropertiesMultiple() {
}

@Test
public void testPropertiesOrder() {
public void testNotePropertiesOrder() {
onBook("* Note 1\n" +
" :PROPERTIES:\n" +
" :LAST_REPEAT: [2017-04-03 Mon 10:26]\n" +
Expand Down Expand Up @@ -142,7 +144,7 @@ public void testPropertiesOrder() {
}

@Test
public void testPropertiesEmpty() {
public void testNotePropertiesEmpty() {
onBook("* Note 1\n" +
" :PROPERTIES:\n" +
" :END:").onLoad()
Expand All @@ -162,6 +164,97 @@ public void testParsingClockTimesOutsideLogbook() {
" CLOCK: [2016-10-27 Thu 17:50]--[2016-10-27 Thu 18:05] => 0:15\n\n");
}

@Test
public void testBookSinglePropertyIsParsed() {
String content = """
:PROPERTIES:
:FOO: bar
:END:
content
""";
TestedBook testedBook = onBook(content);
testedBook.onLoad().isWhenSaved(content);
assertEquals(testedBook.book, dataRepository.findNoteOrBookHavingProperty("foo", "bar"));
}

/**
* So far, we never write properties into the preface -- the preface is only edited as
* regular text. So there is no risk of Orgzly changing the order of the properties, and
* therefore we don't need to keep track of their order.
*/
@Test
public void testBookMultiplePropertiesAreParsed() {
String content = """
:PROPERTIES:
:FOO: bar
:BAR: foo
:END:
content
""";
TestedBook testedBook = onBook(content);
testedBook.onLoad().isWhenSaved(content);
assertEquals(testedBook.book, dataRepository.findNoteOrBookHavingProperty("foo", "bar"));
assertEquals(testedBook.book, dataRepository.findNoteOrBookHavingProperty("bar", "foo"));
}

/**
* The preface is left untouched, but if a property is duplicated, we only parse the last
* defined value.
*/
@Test
public void testBookDuplicateProperties() {
String content = """
:PROPERTIES:
:FOO: firstvalue
:BAR: firstvalue
:FOO: secondvalue
:BAR: secondvalue
:END:
content
""";
TestedBook testedBook = onBook(content);
testedBook.onLoad().isWhenSaved(content);
assertEquals(testedBook.book, dataRepository.findNoteOrBookHavingProperty("foo",
"secondvalue"));
assertEquals(testedBook.book, dataRepository.findNoteOrBookHavingProperty("bar",
"secondvalue"));
assertNull(dataRepository.findNoteOrBookHavingProperty("foo", "firstvalue"));
assertNull(dataRepository.findNoteOrBookHavingProperty("bar", "firstvalue"));
}

@Test
public void testBookDuplicatePropertiesDifferentCase() {
String content = """
:PROPERTIES:
:foo: firstvalue
:bar: firstvalue
:FOO: secondvalue
:BAR: secondvalue
:END:
content
""";
TestedBook testedBook = onBook(content);
testedBook.onLoad().isWhenSaved(content);
assertEquals(testedBook.book, dataRepository.findNoteOrBookHavingProperty("foo",
"secondvalue"));
assertEquals(testedBook.book, dataRepository.findNoteOrBookHavingProperty("bar",
"secondvalue"));
assertEquals(testedBook.book, dataRepository.findNoteOrBookHavingProperty("FOO",
"secondvalue"));
assertEquals(testedBook.book, dataRepository.findNoteOrBookHavingProperty("BAR",
"secondvalue"));
assertNull(dataRepository.findNoteOrBookHavingProperty("foo", "firstvalue"));
assertNull(dataRepository.findNoteOrBookHavingProperty("bar", "firstvalue"));
}

/*
* Books in different languages, different sizes and formats ...
*/
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/java/com/orgzly/android/AppIntent.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public class AppIntent {
public static final String ACTION_UPDATE_TIMESTAMPS = "com.orgzly.intent.action.UPDATE_TIMESTAMPS";

public static final String ACTION_OPEN_NOTE = "com.orgzly.intent.action.OPEN_NOTE";
public static final String ACTION_FOLLOW_LINK_TO_NOTE_WITH_PROPERTY = "com.orgzly.intent.action.FOLLOW_LINK_TO_NOTE_WITH_PROPERTY";
public static final String ACTION_FOLLOW_LINK_TO_NOTE_OR_BOOK_WITH_PROPERTY = "com.orgzly.intent.action.FOLLOW_LINK_TO_NOTE_OR_BOOK_WITH_PROPERTY";
public static final String ACTION_FOLLOW_LINK_TO_FILE = "com.orgzly.intent.action.FOLLOW_LINK_TO_FILE";
public static final String ACTION_OPEN_SAVED_SEARCHES = "com.orgzly.intent.action.OPEN_SAVED_SEARCHES";
public static final String ACTION_OPEN_QUERY = "com.orgzly.intent.action.OPEN_QUERY";
Expand Down
18 changes: 18 additions & 0 deletions app/src/main/java/com/orgzly/android/data/DataRepository.kt
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,7 @@ class DataRepository @Inject constructor(
val settings = OrgFileSettings.fromPreface(preface)

db.book().updatePreface(bookId, preface, settings.title)
setBookPropertiesFromPreface(bookId, preface)

updateBookIsModified(bookId, true)
}
Expand Down Expand Up @@ -1859,6 +1860,10 @@ class DataRepository @Inject constructor(
)

db.book().update(book)

// Parse and store any properties in the book's preface
if (file.preface.isNotEmpty())
setBookPropertiesFromPreface(bookId, file.preface)
}

})
Expand All @@ -1879,6 +1884,12 @@ class DataRepository @Inject constructor(
return bookId
}

private fun setBookPropertiesFromPreface(bookId: Long, preface: String) {
for (property: OrgProperty in OrgProperties.fromString(preface).all) {
db.bookProperty().upsert(bookId, property.name, property.value)
}
}

private fun getOrgRangeId(range: String?): Long? {
return getOrgRangeId(OrgRange.parseOrNull(range))
}
Expand Down Expand Up @@ -1980,6 +1991,13 @@ class DataRepository @Inject constructor(
return db.note().firstNoteHavingPropertyLowerCase(name.lowercase(), value.lowercase())
}

fun findNoteOrBookHavingProperty(name: String, value: String): Any? {
val foundNote = findNoteHavingProperty(name, value)
if (foundNote != null)
return foundNote
return db.book().firstBookHavingPropertyLowerCase(name.lowercase(), value.lowercase())
}

/*
* Saved search
*/
Expand Down
57 changes: 52 additions & 5 deletions app/src/main/java/com/orgzly/android/db/OrgzlyDatabase.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,54 @@ import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.sqlite.db.SupportSQLiteQueryBuilder
import com.orgzly.BuildConfig
import com.orgzly.android.db.dao.*
import com.orgzly.android.db.entity.*
import com.orgzly.android.db.dao.AppLogDao
import com.orgzly.android.db.dao.BookDao
import com.orgzly.android.db.dao.BookLinkDao
import com.orgzly.android.db.dao.BookPropertyDao
import com.orgzly.android.db.dao.BookSyncDao
import com.orgzly.android.db.dao.BookViewDao
import com.orgzly.android.db.dao.DbRepoBookDao
import com.orgzly.android.db.dao.NoteAncestorDao
import com.orgzly.android.db.dao.NoteDao
import com.orgzly.android.db.dao.NoteEventDao
import com.orgzly.android.db.dao.NotePropertyDao
import com.orgzly.android.db.dao.NoteViewDao
import com.orgzly.android.db.dao.OrgRangeDao
import com.orgzly.android.db.dao.OrgTimestampDao
import com.orgzly.android.db.dao.ReminderTimeDao
import com.orgzly.android.db.dao.RepoDao
import com.orgzly.android.db.dao.RookDao
import com.orgzly.android.db.dao.RookUrlDao
import com.orgzly.android.db.dao.SavedSearchDao
import com.orgzly.android.db.dao.VersionedRookDao
import com.orgzly.android.db.entity.AppLog
import com.orgzly.android.db.entity.Book
import com.orgzly.android.db.entity.BookLink
import com.orgzly.android.db.entity.BookProperty
import com.orgzly.android.db.entity.BookSync
import com.orgzly.android.db.entity.DbRepoBook
import com.orgzly.android.db.entity.Note
import com.orgzly.android.db.entity.NoteAncestor
import com.orgzly.android.db.entity.NoteEvent
import com.orgzly.android.db.entity.NoteProperty
import com.orgzly.android.db.entity.OrgRange
import com.orgzly.android.db.entity.OrgTimestamp
import com.orgzly.android.db.entity.Repo
import com.orgzly.android.db.entity.Rook
import com.orgzly.android.db.entity.RookUrl
import com.orgzly.android.db.entity.SavedSearch
import com.orgzly.android.db.entity.VersionedRook
import com.orgzly.android.db.mappers.OrgTimestampMapper
import com.orgzly.android.util.LogUtils
import com.orgzly.org.OrgActiveTimestamps
import com.orgzly.org.datetime.OrgDateTime
import java.util.*
import java.util.Calendar

@Database(
entities = [
Book::class,
BookLink::class,
BookProperty::class,
BookSync::class,
DbRepoBook::class,
Note::class,
Expand All @@ -39,13 +75,14 @@ import java.util.*
AppLog::class
],

version = 156
version = 157
)
@TypeConverters(com.orgzly.android.db.TypeConverters::class)
abstract class OrgzlyDatabase : RoomDatabase() {

abstract fun book(): BookDao
abstract fun bookLink(): BookLinkDao
abstract fun bookProperty(): BookPropertyDao
abstract fun bookView(): BookViewDao
abstract fun bookSync(): BookSyncDao
abstract fun noteAncestor(): NoteAncestorDao
Expand Down Expand Up @@ -113,7 +150,8 @@ abstract class OrgzlyDatabase : RoomDatabase() {
MIGRATION_152_153,
MIGRATION_153_154,
MIGRATION_154_155,
MIGRATION_155_156
MIGRATION_155_156,
MIGRATION_156_157
)
.addCallback(object : Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
Expand Down Expand Up @@ -564,5 +602,14 @@ abstract class OrgzlyDatabase : RoomDatabase() {
db.execSQL("CREATE INDEX IF NOT EXISTS `index_app_logs_name` ON `app_logs` (`name`)")
}
}

private val MIGRATION_156_157 = object : Migration(156, 157) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("CREATE TABLE IF NOT EXISTS `book_properties` (`book_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`book_id`, `name`), FOREIGN KEY(`book_id`) REFERENCES `books`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE)")
db.execSQL("CREATE INDEX IF NOT EXISTS `index_book_properties_book_id` ON `book_properties` (`book_id`)")
db.execSQL("CREATE INDEX IF NOT EXISTS `index_book_properties_name` ON `book_properties` (`name`)")
db.execSQL("CREATE INDEX IF NOT EXISTS `index_book_properties_value` ON `book_properties` (`value`)")
}
}
}
}
9 changes: 8 additions & 1 deletion app/src/main/java/com/orgzly/android/db/dao/BookDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,14 @@ abstract class BookDao : BaseDao<Book> {
@Query("UPDATE books SET is_modified = 0 WHERE id IN (:ids)")
abstract fun setIsNotModified(ids: Set<Long>): Int


@Query("""
SELECT books.*
FROM book_properties
LEFT JOIN books ON (books.id = book_properties.book_id)
WHERE LOWER(book_properties.name) = :name AND LOWER(book_properties.value) = :value AND books.id IS NOT NULL
LIMIT 1
""")
abstract fun firstBookHavingPropertyLowerCase(name: String, value: String): Book?

fun getOrInsert(name: String): Long =
get(name).let {
Expand Down
41 changes: 41 additions & 0 deletions app/src/main/java/com/orgzly/android/db/dao/BookPropertyDao.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.orgzly.android.db.dao

import androidx.room.Dao
import androidx.room.Query
import androidx.room.Transaction
import com.orgzly.android.db.entity.BookProperty

@Dao
abstract class BookPropertyDao : BaseDao<BookProperty> {

@Query("SELECT * FROM book_properties WHERE book_id = :bookId")
abstract fun get(bookId: Long): List<BookProperty>

@Query("SELECT * FROM book_properties WHERE book_id = :bookId AND LOWER(name) = LOWER(:name)")
abstract fun get(bookId: Long, name: String): List<BookProperty>

@Query("SELECT * FROM book_properties")
abstract fun getAll(): List<BookProperty>

@Transaction
open fun upsert(bookId: Long, name: String, value: String) {
val properties = get(bookId, name)

if (properties.isEmpty()) {
// Insert new
insert(BookProperty(bookId, name, value))

} else {
// Update first
update(properties.first().copy(value = value))

// Delete others
for (i in 1 until properties.size) {
delete(properties[i])
}
}
}

@Query("DELETE FROM book_properties WHERE book_id = :bookId")
abstract fun delete(bookId: Long)
}
Loading

0 comments on commit 69306b1

Please sign in to comment.