#3120 Suggest categories based on depictions (#3736)

This commit is contained in:
Seán Mac Gillicuddy 2020-05-20 15:26:16 +01:00 committed by GitHub
parent 01839dec6e
commit de3377c0fe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 99 additions and 84 deletions

View file

@ -1,11 +1,6 @@
package fr.free.nrw.commons;
import androidx.annotation.NonNull;
import okhttp3.logging.HttpLoggingInterceptor.Level;
import org.wikipedia.dataclient.SharedPreferenceCookieManager;
import org.wikipedia.dataclient.okhttp.HttpStatusException;
import java.io.File;
import java.io.IOException;
import okhttp3.Cache;

View file

@ -2,9 +2,10 @@ package fr.free.nrw.commons.category
import android.text.TextUtils
import fr.free.nrw.commons.upload.GpsCategoryModel
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import fr.free.nrw.commons.utils.StringSortingUtils
import io.reactivex.Observable
import io.reactivex.functions.Function3
import io.reactivex.functions.Function4
import timber.log.Timber
import java.util.*
import javax.inject.Inject
@ -67,30 +68,42 @@ class CategoriesModel @Inject constructor(
* @param imageTitleList
* @return
*/
fun searchAll(term: String, imageTitleList: List<String>): Observable<List<CategoryItem>> {
return suggestionsOrSearch(term, imageTitleList)
fun searchAll(
term: String,
imageTitleList: List<String>,
selectedDepictions: List<DepictedItem>
): Observable<List<CategoryItem>> {
return suggestionsOrSearch(term, imageTitleList, selectedDepictions)
.map { it.map { CategoryItem(it, false) } }
}
private fun suggestionsOrSearch(term: String, imageTitleList: List<String>):
Observable<List<String>> {
private fun suggestionsOrSearch(
term: String,
imageTitleList: List<String>,
selectedDepictions: List<DepictedItem>
): Observable<List<String>> {
return if (TextUtils.isEmpty(term))
Observable.combineLatest(
categoriesFromDepiction(selectedDepictions),
gpsCategoryModel.categoriesFromLocation,
titleCategories(imageTitleList),
Observable.just(categoryDao.recentCategories(SEARCH_CATS_LIMIT)),
Function3(::combine)
Function4(::combine)
)
else
categoryClient.searchCategoriesForPrefix(term.toLowerCase(), SEARCH_CATS_LIMIT)
.map { it.sortedWith(StringSortingUtils.sortBySimilarity(term)) }
}
private fun categoriesFromDepiction(selectedDepictions: List<DepictedItem>) =
Observable.just(selectedDepictions.map { it.commonsCategories }.flatten())
private fun combine(
depictionCategories: List<String>,
locationCategories: List<String>,
titles: List<String>,
recents: List<String>
) = locationCategories + titles + recents
) = depictionCategories + locationCategories + titles + recents
/**
@ -98,14 +111,13 @@ class CategoriesModel @Inject constructor(
* @param titleList
* @return
*/
private fun titleCategories(titleList: List<String>): Observable<List<String>> {
return if (titleList.isNotEmpty())
private fun titleCategories(titleList: List<String>) =
if (titleList.isNotEmpty())
Observable.combineLatest(titleList.map { getTitleCategories(it) }) { searchResults ->
searchResults.map { it as List<String> }.flatten()
}
else
Observable.just(emptyList())
}
/**
* Return category for single title

View file

@ -155,5 +155,4 @@ public class SearchDepictionsFragmentPresenter extends CommonsDaggerSupportFragm
offset=queryList.size();
}
}
}

View file

@ -171,7 +171,7 @@ public class MediaClient {
* @return caption for image using wikibaseIdentifier
*/
public Single<String> getCaptionByWikibaseIdentifier(String wikibaseIdentifier) {
return mediaDetailInterface.getCaptionForImage(Locale.getDefault().getLanguage(), wikibaseIdentifier)
return mediaDetailInterface.getEntityForImage(Locale.getDefault().getLanguage(), wikibaseIdentifier)
.map(mediaDetailResponse -> {
if (isSuccess(mediaDetailResponse)) {
for (Entity wikibaseItem : mediaDetailResponse.entities().values()) {

View file

@ -33,5 +33,5 @@ public interface MediaDetailInterface {
* @param wikibaseIdentifier pageId for the media
*/
@GET("/w/api.php?action=wbgetentities&props=labels&format=json&languagefallback=1&sites=commonswiki")
Observable<Entities> getCaptionForImage(@Query("languages") String language, @Query("ids") String wikibaseIdentifier);
Observable<Entities> getEntityForImage(@Query("languages") String language, @Query("ids") String wikibaseIdentifier);
}

View file

@ -1,5 +1,13 @@
package fr.free.nrw.commons.nearby.fragments;
import static fr.free.nrw.commons.contributions.MainActivity.CONTRIBUTIONS_TAB_POSITION;
import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED;
import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.LOCATION_SLIGHTLY_CHANGED;
import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.MAP_UPDATED;
import static fr.free.nrw.commons.nearby.Label.TEXT_TO_DESCRIPTION;
import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween;
import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT;
import android.Manifest;
import android.app.AlertDialog;
import android.content.BroadcastReceiver;
@ -26,14 +34,14 @@ import android.widget.RelativeLayout;
import android.widget.SearchView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
import butterknife.BindView;
import butterknife.ButterKnife;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.chip.Chip;
import com.google.android.material.chip.ChipGroup;
@ -58,17 +66,6 @@ import com.mapbox.mapboxsdk.maps.UiSettings;
import com.mapbox.pluginscalebar.ScaleBarOptions;
import com.mapbox.pluginscalebar.ScaleBarPlugin;
import com.pedrogomez.renderers.RVRendererAdapter;
import fr.free.nrw.commons.utils.DialogUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.inject.Named;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
@ -92,6 +89,7 @@ import fr.free.nrw.commons.nearby.NearbyMarker;
import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.nearby.contract.NearbyParentFragmentContract;
import fr.free.nrw.commons.nearby.presenter.NearbyParentFragmentPresenter;
import fr.free.nrw.commons.utils.DialogUtil;
import fr.free.nrw.commons.utils.ExecutorUtils;
import fr.free.nrw.commons.utils.LayoutUtils;
import fr.free.nrw.commons.utils.LocationUtils;
@ -105,16 +103,13 @@ import fr.free.nrw.commons.wikidata.WikidataEditListener;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.inject.Named;
import timber.log.Timber;
import static fr.free.nrw.commons.contributions.MainActivity.CONTRIBUTIONS_TAB_POSITION;
import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED;
import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.LOCATION_SLIGHTLY_CHANGED;
import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.MAP_UPDATED;
import static fr.free.nrw.commons.nearby.Label.TEXT_TO_DESCRIPTION;
import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween;
import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT;
public class NearbyParentFragment extends CommonsDaggerSupportFragment
implements NearbyParentFragmentContract.View,

View file

@ -108,10 +108,12 @@ public class UploadRepository {
*
* @param query
* @param imageTitleList
* @param selectedDepictions
* @return
*/
public Observable<List<CategoryItem>> searchAll(String query, List<String> imageTitleList) {
return categoriesModel.searchAll(query, imageTitleList);
public Observable<List<CategoryItem>> searchAll(String query, List<String> imageTitleList,
List<DepictedItem> selectedDepictions) {
return categoriesModel.searchAll(query, imageTitleList, selectedDepictions);
}
/**

View file

@ -60,7 +60,7 @@ class CategoriesPresenter @Inject constructor(
}
private fun searchResults(term: String) =
repository.searchAll(term, getImageTitleList())
repository.searchAll(term, getImageTitleList(), repository.selectedDepictions)
.subscribeOn(ioScheduler)
.map { it.filterNot { categoryItem -> repository.containsYear(categoryItem.name) } }

View file

@ -107,7 +107,6 @@ class DepictsPresenter @Inject constructor(
view.noDepictionSelected()
}
}
}
inline fun <reified T> proxy() = Proxy

View file

@ -5,6 +5,7 @@ import fr.free.nrw.commons.explore.depictions.THUMB_IMAGE_SIZE
import fr.free.nrw.commons.nearby.Place
import fr.free.nrw.commons.upload.WikidataItem
import fr.free.nrw.commons.wikidata.WikidataProperties
import fr.free.nrw.commons.wikidata.WikidataProperties.*
import org.wikipedia.wikidata.DataValue
import org.wikipedia.wikidata.Entities
import org.wikipedia.wikidata.Statement_partial
@ -17,6 +18,7 @@ data class DepictedItem constructor(
val description: String?,
val imageUrl: String?,
val instanceOfs: List<String>,
val commonsCategories: List<String>,
var isSelected: Boolean,
override val id: String
) : WikidataItem {
@ -36,10 +38,12 @@ data class DepictedItem constructor(
constructor(entity: Entities.Entity, name: String, description: String) : this(
name,
description,
entity[WikidataProperties.IMAGE].primaryImageValue?.let {
entity[IMAGE].primaryImageValue?.let {
getImageUrl(it.value, THUMB_IMAGE_SIZE)
},
entity[WikidataProperties.INSTANCE_OF].toIds(),
entity[INSTANCE_OF].toIds(),
entity[COMMONS_CATEGORY]?.map { (it.mainSnak.dataValue as DataValue.ValueString).value }
?: emptyList(),
false,
entity.id()
)
@ -57,7 +61,7 @@ data class DepictedItem constructor(
}
private fun List<Statement_partial>?.toIds(): List<String> {
return this?.map { it.mainSnak.dataValue }
return this?.map { it.mainSnak.dataValue }
?.filterIsInstance<DataValue.EntityId>()
?.map { it.value.id }
?: emptyList()

View file

@ -3,6 +3,8 @@ package fr.free.nrw.commons.wikidata
import fr.free.nrw.commons.BuildConfig
enum class WikidataProperties(val propertyName: String) {
IMAGE("P18"), DEPICTS(BuildConfig.DEPICTS_PROPERTY), INSTANCE_OF("P31");
IMAGE("P18"),
DEPICTS(BuildConfig.DEPICTS_PROPERTY),
COMMONS_CATEGORY("P373"),
INSTANCE_OF("P31");
}

View file

@ -0,0 +1,23 @@
import fr.free.nrw.commons.category.CategoryItem
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
fun depictedItem(
name: String = "label",
description: String = "desc",
imageUrl: String = "",
instanceOfs: List<String> = listOf(),
commonsCategories: List<String> = listOf(),
isSelected: Boolean = false,
id: String = "entityId"
) = DepictedItem(
name = name,
description = description,
imageUrl = imageUrl,
instanceOfs = instanceOfs,
commonsCategories = commonsCategories,
isSelected = isSelected,
id = id
)
fun categoryItem(name: String = "name", selected: Boolean = false) =
CategoryItem(name, selected)

View file

@ -1,7 +1,10 @@
package fr.free.nrw.commons.category
import categoryItem
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.whenever
import depictedItem
import fr.free.nrw.commons.explore.depictions.DepictsClient
import fr.free.nrw.commons.upload.GpsCategoryModel
import io.reactivex.Observable
import io.reactivex.subjects.BehaviorSubject
@ -36,11 +39,11 @@ class CategoriesModelTest {
// Checking if both return "Test"
val expectedItems = expectedList.map { CategoryItem(it, false) }
categoriesModel.searchAll("tes", emptyList())
categoriesModel.searchAll("tes", emptyList(), emptyList())
.test()
.assertValues(expectedItems)
categoriesModel.searchAll("Tes", emptyList())
categoriesModel.searchAll("Tes", emptyList(), emptyList())
.test()
.assertValues(expectedItems)
}
@ -48,6 +51,7 @@ class CategoriesModelTest {
@Test
fun `searchAll with empty search terms creates results from gps, title search & recents`() {
val gpsCategoryModel: GpsCategoryModel = mock()
val depictedItem = depictedItem(commonsCategories = listOf("depictionCategory"))
whenever(gpsCategoryModel.categoriesFromLocation)
.thenReturn(BehaviorSubject.createDefault(listOf("gpsCategory")))
@ -55,13 +59,14 @@ class CategoriesModelTest {
.thenReturn(Observable.just(listOf("titleSearch")))
whenever(categoryDao.recentCategories(25)).thenReturn(listOf("recentCategories"))
CategoriesModel(categoryClient, categoryDao, gpsCategoryModel)
.searchAll("", listOf("tes"))
.searchAll("", listOf("tes"), listOf(depictedItem))
.test()
.assertValue(
listOf(
CategoryItem("gpsCategory", false),
CategoryItem("titleSearch", false),
CategoryItem("recentCategories", false)
categoryItem("depictionCategory"),
categoryItem("gpsCategory"),
categoryItem("titleSearch"),
categoryItem("recentCategories")
)
)
}

View file

@ -1,10 +1,9 @@
package fr.free.nrw.commons.explore.depictions
import com.nhaarman.mockitokotlin2.whenever
import depictedItem
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao
import fr.free.nrw.commons.kvstore.JsonKvStore
import fr.free.nrw.commons.upload.depictedItem
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import io.reactivex.Single
import io.reactivex.schedulers.TestScheduler
import org.junit.Before
@ -36,7 +35,6 @@ class SearchDepictionsPresenterTest {
fun setUp() {
MockitoAnnotations.initMocks(this)
testScheduler = TestScheduler()
val depictedItem: DepictedItem = depictedItem(instanceOfs = listOf())
searchDepictionsFragmentPresenter = SearchDepictionsFragmentPresenter(
jsonKvStore,
recentSearchesDao,
@ -56,5 +54,4 @@ class SearchDepictionsPresenterTest {
testScheduler.triggerActions()
verify(view)?.onSuccess(expectedList)
}
}

View file

@ -1,8 +1,8 @@
package fr.free.nrw.commons.upload
import categoryItem
import com.nhaarman.mockitokotlin2.*
import fr.free.nrw.commons.R
import fr.free.nrw.commons.category.CategoryItem
import fr.free.nrw.commons.repository.UploadRepository
import fr.free.nrw.commons.upload.categories.CategoriesContract
import fr.free.nrw.commons.upload.categories.CategoriesPresenter
@ -27,11 +27,6 @@ class CategoriesPresenterTest {
private lateinit var testScheduler: TestScheduler
private val categoryItems: ArrayList<CategoryItem> = ArrayList()
@Mock
lateinit var categoryItem: CategoryItem
/**
* initial setup
*/
@ -40,7 +35,6 @@ class CategoriesPresenterTest {
fun setUp() {
MockitoAnnotations.initMocks(this)
testScheduler = TestScheduler()
categoryItems.add(categoryItem)
categoriesPresenter = CategoriesPresenter(repository, testScheduler, testScheduler)
categoriesPresenter.onAttachView(view)
}
@ -62,7 +56,7 @@ class CategoriesPresenterTest {
emptyCaptionUploadItem
)
)
whenever(repository.searchAll("test", listOf("nonEmpty")))
whenever(repository.searchAll("test", listOf("nonEmpty"), repository.selectedDepictions))
.thenReturn(
Observable.just(
listOf(
@ -87,7 +81,7 @@ class CategoriesPresenterTest {
@Test
fun `searchForCategoriesTest sets Error when list is empty`() {
whenever(repository.uploads).thenReturn(listOf())
whenever(repository.searchAll(any(), any())).thenReturn(Observable.just(listOf()))
whenever(repository.searchAll(any(), any(), any())).thenReturn(Observable.just(listOf()))
whenever(repository.selectedCategories).thenReturn(listOf())
categoriesPresenter.searchForCategories("test")
testScheduler.triggerActions()
@ -124,10 +118,8 @@ class CategoriesPresenterTest {
*/
@Test
fun onCategoryItemClickedTest() {
val categoryItem = categoryItem()
categoriesPresenter.onCategoryItemClicked(categoryItem)
verify(repository).onCategoryClicked(categoryItem)
}
private fun categoryItem(name: String = "name", selected: Boolean = false) =
CategoryItem(name, selected)
}

View file

@ -4,11 +4,11 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.jraska.livedata.test
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever
import depictedItem
import fr.free.nrw.commons.explore.depictions.DepictsClient
import fr.free.nrw.commons.repository.UploadRepository
import fr.free.nrw.commons.upload.depicts.DepictsContract
import fr.free.nrw.commons.upload.depicts.DepictsPresenter
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import fr.free.nrw.commons.wikidata.WikidataDisambiguationItems
import io.reactivex.Flowable
import io.reactivex.schedulers.TestScheduler
@ -62,8 +62,8 @@ class DepictsPresenterTest {
depictedItem(id="nonUnique"),
depictedItem(id="nonUnique"),
depictedItem(
id = "unique",
instanceOfs = listOf(WikidataDisambiguationItems.CATEGORY.id)
instanceOfs = listOf(WikidataDisambiguationItems.CATEGORY.id),
id = "unique"
)
)
whenever(repository.searchAllEntities("")).thenReturn(Flowable.just(searchResults))
@ -78,6 +78,7 @@ class DepictsPresenterTest {
.assertValue(listOf(selectedItem, depictedItem(id="nonUnique")))
}
@Test
fun `empty search results with empty term do not show error`() {
whenever(repository.searchAllEntities("")).thenReturn(Flowable.just(emptyList()))
@ -137,15 +138,4 @@ class DepictsPresenterTest {
depictsPresenter.verifyDepictions()
verify(view).noDepictionSelected()
}
}
fun depictedItem(
name: String = "label",
description: String = "desc",
imageUrl: String = "",
instanceOfs: List<String> = listOf(),
isSelected: Boolean = false,
id: String = "entityId"
) = DepictedItem(name, description, imageUrl, instanceOfs, isSelected, id)