mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 20:33:53 +01:00
If depicted Wikidata item has no associated Commons category property, then suggest categories from its P18 (#6130)
* Fix NPE with UploadMediaDetails.captionText * Store P18 instead of processed image url in DepictedItem * Add routes for fetching category info from titles * Consider depict's P18 when suggesting categories * Add tests * Corrected DepictedItem constructor arguments * Add test for DepictedItem::primaryImage
This commit is contained in:
parent
1f33926ed5
commit
1e64acdf1d
7 changed files with 212 additions and 28 deletions
|
|
@ -127,14 +127,41 @@ class CategoriesModel
|
||||||
/**
|
/**
|
||||||
* Fetches details of every category associated with selected depictions, converts them into
|
* Fetches details of every category associated with selected depictions, converts them into
|
||||||
* CategoryItem and returns them in a list.
|
* CategoryItem and returns them in a list.
|
||||||
|
* If a selected depiction has no categories, the categories in which its P18 belongs are
|
||||||
|
* returned in the list.
|
||||||
*
|
*
|
||||||
* @param selectedDepictions selected DepictItems
|
* @param selectedDepictions selected DepictItems
|
||||||
* @return List of CategoryItem associated with selected depictions
|
* @return List of CategoryItem associated with selected depictions
|
||||||
*/
|
*/
|
||||||
private fun categoriesFromDepiction(selectedDepictions: List<DepictedItem>): Observable<MutableList<CategoryItem>>? =
|
private fun categoriesFromDepiction(selectedDepictions: List<DepictedItem>): Observable<MutableList<CategoryItem>>? {
|
||||||
|
val observables = selectedDepictions.map { depictedItem ->
|
||||||
|
if (depictedItem.commonsCategories.isEmpty()) {
|
||||||
|
if (depictedItem.primaryImage == null) {
|
||||||
|
return@map Observable.just(emptyList<CategoryItem>())
|
||||||
|
}
|
||||||
|
Observable.just(
|
||||||
|
depictedItem.primaryImage
|
||||||
|
).map { image ->
|
||||||
|
categoryClient
|
||||||
|
.getCategoriesOfImage(
|
||||||
|
image,
|
||||||
|
SEARCH_CATS_LIMIT,
|
||||||
|
).map {
|
||||||
|
it.map { category ->
|
||||||
|
CategoryItem(
|
||||||
|
category.name,
|
||||||
|
category.description,
|
||||||
|
category.thumbnail,
|
||||||
|
category.isSelected,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.blockingGet()
|
||||||
|
}.flatMapIterable { it }.toList()
|
||||||
|
.toObservable()
|
||||||
|
} else {
|
||||||
Observable
|
Observable
|
||||||
.fromIterable(
|
.fromIterable(
|
||||||
selectedDepictions.map { it.commonsCategories }.flatten(),
|
depictedItem.commonsCategories,
|
||||||
).map { categoryItem ->
|
).map { categoryItem ->
|
||||||
categoryClient
|
categoryClient
|
||||||
.getCategoriesByName(
|
.getCategoriesByName(
|
||||||
|
|
@ -151,6 +178,13 @@ class CategoriesModel
|
||||||
}.blockingGet()
|
}.blockingGet()
|
||||||
}.toList()
|
}.toList()
|
||||||
.toObservable()
|
.toObservable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Observable.concat(observables)
|
||||||
|
.scan(mutableListOf<CategoryItem>()) { accumulator, currentList ->
|
||||||
|
accumulator.apply { addAll(currentList) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches details of every category by their name, converts them into
|
* Fetches details of every category by their name, converts them into
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,24 @@ class CategoryClient
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches categories belonging to an image (P18 of some wikidata entity).
|
||||||
|
*
|
||||||
|
* @param image P18 of some wikidata entity
|
||||||
|
* @param itemLimit How many categories to return
|
||||||
|
* @return Single Observable emitting the list of categories
|
||||||
|
*/
|
||||||
|
fun getCategoriesOfImage(
|
||||||
|
image: String,
|
||||||
|
itemLimit: Int,
|
||||||
|
): Single<List<CategoryItem>> =
|
||||||
|
responseMapper(
|
||||||
|
categoryInterface.getCategoriesByTitles(
|
||||||
|
"File:${image}",
|
||||||
|
itemLimit,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The method takes categoryName as input and returns a List of Subcategories
|
* The method takes categoryName as input and returns a List of Subcategories
|
||||||
* It uses the generator query API to get the subcategories in a category, 500 at a time.
|
* It uses the generator query API to get the subcategories in a category, 500 at a time.
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,21 @@ interface CategoryInterface {
|
||||||
@Query("gacoffset") offset: Int,
|
@Query("gacoffset") offset: Int,
|
||||||
): Single<MwQueryResponse>
|
): Single<MwQueryResponse>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches non-hidden categories by titles.
|
||||||
|
*
|
||||||
|
* @param titles titles to fetch categories for (e.g. File:<P18 of a wikidata entity>)
|
||||||
|
* @param itemLimit How many categories to return
|
||||||
|
* @return MwQueryResponse
|
||||||
|
*/
|
||||||
|
@GET(
|
||||||
|
"w/api.php?action=query&format=json&formatversion=2&generator=categories&prop=categoryinfo|description|pageimages&piprop=thumbnail&pithumbsize=70&gclshow=!hidden",
|
||||||
|
)
|
||||||
|
fun getCategoriesByTitles(
|
||||||
|
@Query("titles") titles: String?,
|
||||||
|
@Query("gcllimit") itemLimit: Int,
|
||||||
|
): Single<MwQueryResponse>
|
||||||
|
|
||||||
@GET("w/api.php?action=query&format=json&formatversion=2&generator=categorymembers&gcmtype=subcat&prop=info&gcmlimit=50")
|
@GET("w/api.php?action=query&format=json&formatversion=2&generator=categorymembers&gcmtype=subcat&prop=info&gcmlimit=50")
|
||||||
fun getSubCategoryList(
|
fun getSubCategoryList(
|
||||||
@Query("gcmtitle") categoryName: String,
|
@Query("gcmtitle") categoryName: String,
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,9 @@ data class DepictedItem constructor(
|
||||||
entity.id(),
|
entity.id(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val primaryImage: String?
|
||||||
|
get() = imageUrl?.split('-')?.lastOrNull()
|
||||||
|
|
||||||
override fun equals(other: Any?) =
|
override fun equals(other: Any?) =
|
||||||
when {
|
when {
|
||||||
this === other -> true
|
this === other -> true
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
package fr.free.nrw.commons.category
|
package fr.free.nrw.commons.category
|
||||||
|
|
||||||
import categoryItem
|
import categoryItem
|
||||||
|
import com.nhaarman.mockitokotlin2.any
|
||||||
import com.nhaarman.mockitokotlin2.mock
|
import com.nhaarman.mockitokotlin2.mock
|
||||||
|
import com.nhaarman.mockitokotlin2.times
|
||||||
import com.nhaarman.mockitokotlin2.verify
|
import com.nhaarman.mockitokotlin2.verify
|
||||||
import com.nhaarman.mockitokotlin2.whenever
|
import com.nhaarman.mockitokotlin2.whenever
|
||||||
import depictedItem
|
import depictedItem
|
||||||
|
|
@ -99,6 +101,10 @@ class CategoriesModelTest {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
val depictedItemWithoutCategories =
|
||||||
|
depictedItem(
|
||||||
|
imageUrl = "testUrl"
|
||||||
|
)
|
||||||
|
|
||||||
whenever(gpsCategoryModel.categoriesFromLocation)
|
whenever(gpsCategoryModel.categoriesFromLocation)
|
||||||
.thenReturn(
|
.thenReturn(
|
||||||
|
|
@ -159,6 +165,23 @@ class CategoriesModelTest {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
whenever(
|
||||||
|
categoryClient.getCategoriesOfImage(
|
||||||
|
"testUrl",
|
||||||
|
25,
|
||||||
|
),
|
||||||
|
).thenReturn(
|
||||||
|
Single.just(
|
||||||
|
listOf(
|
||||||
|
CategoryItem(
|
||||||
|
"categoriesOfP18",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
val imageTitleList = listOf("Test")
|
val imageTitleList = listOf("Test")
|
||||||
CategoriesModel(categoryClient, categoryDao, gpsCategoryModel)
|
CategoriesModel(categoryClient, categoryDao, gpsCategoryModel)
|
||||||
.searchAll("", imageTitleList, listOf(depictedItem))
|
.searchAll("", imageTitleList, listOf(depictedItem))
|
||||||
|
|
@ -171,8 +194,21 @@ class CategoriesModelTest {
|
||||||
categoryItem("recentCategories"),
|
categoryItem("recentCategories"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
CategoriesModel(categoryClient, categoryDao, gpsCategoryModel)
|
||||||
|
.searchAll("", imageTitleList, listOf(depictedItemWithoutCategories))
|
||||||
|
.test()
|
||||||
|
.assertValue(
|
||||||
|
listOf(
|
||||||
|
categoryItem("categoriesOfP18"),
|
||||||
|
categoryItem("gpsCategory"),
|
||||||
|
categoryItem("titleSearch"),
|
||||||
|
categoryItem("recentCategories"),
|
||||||
|
),
|
||||||
|
)
|
||||||
imageTitleList.forEach {
|
imageTitleList.forEach {
|
||||||
verify(categoryClient).searchCategories(it, CategoriesModel.SEARCH_CATS_LIMIT)
|
verify(categoryClient, times(2)).searchCategories(it, CategoriesModel.SEARCH_CATS_LIMIT)
|
||||||
|
verify(categoryClient).getCategoriesByName(any(), any(), any(), any())
|
||||||
|
verify(categoryClient).getCategoriesOfImage(any(), any())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,45 @@ class CategoryClientTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getCategoriesByTitlesFound() {
|
||||||
|
val mockResponse = withMockResponse("Category:Test")
|
||||||
|
whenever(
|
||||||
|
categoryInterface.getCategoriesByTitles(
|
||||||
|
anyString(),
|
||||||
|
anyInt(),
|
||||||
|
),
|
||||||
|
).thenReturn(Single.just(mockResponse))
|
||||||
|
categoryClient
|
||||||
|
.getCategoriesOfImage("tes", 10)
|
||||||
|
.test()
|
||||||
|
.assertValues(
|
||||||
|
listOf(
|
||||||
|
CategoryItem(
|
||||||
|
"Test",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
categoryClient
|
||||||
|
.getCategoriesOfImage(
|
||||||
|
"tes",
|
||||||
|
10,
|
||||||
|
).test()
|
||||||
|
.assertValues(
|
||||||
|
listOf(
|
||||||
|
CategoryItem(
|
||||||
|
"Test",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun getCategoriesByNameNull() {
|
fun getCategoriesByNameNull() {
|
||||||
val mockResponse = withNullPages()
|
val mockResponse = withNullPages()
|
||||||
|
|
@ -160,6 +199,29 @@ class CategoryClientTest {
|
||||||
.assertValues(emptyList())
|
.assertValues(emptyList())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getCategoriesByTitlesNull() {
|
||||||
|
val mockResponse = withNullPages()
|
||||||
|
whenever(
|
||||||
|
categoryInterface.getCategoriesByTitles(
|
||||||
|
anyString(),
|
||||||
|
anyInt(),
|
||||||
|
),
|
||||||
|
).thenReturn(Single.just(mockResponse))
|
||||||
|
categoryClient
|
||||||
|
.getCategoriesOfImage(
|
||||||
|
"tes",
|
||||||
|
10,
|
||||||
|
).test()
|
||||||
|
.assertValues(emptyList())
|
||||||
|
categoryClient
|
||||||
|
.getCategoriesOfImage(
|
||||||
|
"tes",
|
||||||
|
10,
|
||||||
|
).test()
|
||||||
|
.assertValues(emptyList())
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun getParentCategoryListFound() {
|
fun getParentCategoryListFound() {
|
||||||
val mockResponse = withMockResponse("Category:Test")
|
val mockResponse = withMockResponse("Category:Test")
|
||||||
|
|
|
||||||
|
|
@ -181,4 +181,20 @@ class DepictedItemTest {
|
||||||
fun `hashCode returns different values for objects with different name`() {
|
fun `hashCode returns different values for objects with different name`() {
|
||||||
Assert.assertNotEquals(depictedItem(name = "a").hashCode(), depictedItem(name = "b").hashCode())
|
Assert.assertNotEquals(depictedItem(name = "a").hashCode(), depictedItem(name = "b").hashCode())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `primaryImage is derived correctly from imageUrl`() {
|
||||||
|
Assert.assertEquals(
|
||||||
|
DepictedItem(
|
||||||
|
entity(
|
||||||
|
statements = mapOf(
|
||||||
|
WikidataProperties.IMAGE.propertyName to listOf(
|
||||||
|
statement(snak(dataValue = valueString("prefix: example_image name"))),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).primaryImage,
|
||||||
|
"_example_image_name",
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue