Displaying Category image and Description (#4531)

* API call done

* API call

* Image implementation done

* Gradle

* Java docs and code convention

* Description added

* Refactoring Category

* Refactoring Category

* Refactoring Category

* Description and thumbnail issue fixed

* Description and thumbnail issue fixed

* Minor issue fixed

* Minor issue fixed

* Server changed

* Logo changed

* Change in structure

* Fixed failed tests

* Fixed Test failed

* Optimized imports

* Dialog can't be dismissed

* Dialog can't be dismissed

* Resolved Conflicts

* UI fixed

* Added description and thumbnail in local DB

* Added description and thumbnail in local DB

* Test fixed

* Added

* Updated with latest master

* Test Updated with latest master

* Issue fixed

* Revert gradle changes

* Revert gradle changes

* Update gradle-wrapper.properties

* Require Api removed
This commit is contained in:
Ayan Sarkar 2022-01-20 11:49:57 +05:30 committed by GitHub
parent 0914eeea53
commit 92957f4204
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 373 additions and 151 deletions

View file

@ -5,6 +5,7 @@ import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.RemoteException;
import fr.free.nrw.commons.category.CategoryItem;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import java.util.ArrayList;
import java.util.Arrays;
@ -141,9 +142,17 @@ public class BookmarkItemsDao {
final String instanceListString
= cursor.getString(cursor.getColumnIndex(Table.COLUMN_INSTANCE_LIST));
final List<String> instanceList = StringToArray(instanceListString);
final String categoryListString = cursor.getString(cursor
.getColumnIndex(Table.COLUMN_CATEGORIES_LIST));
final List<String> categoryList = StringToArray(categoryListString);
final String categoryNameListString = cursor.getString(cursor
.getColumnIndex(Table.COLUMN_CATEGORIES_NAME_LIST));
final List<String> categoryNameList = StringToArray(categoryNameListString);
final String categoryDescriptionListString = cursor.getString(cursor
.getColumnIndex(Table.COLUMN_CATEGORIES_DESCRIPTION_LIST));
final List<String> categoryDescriptionList = StringToArray(categoryDescriptionListString);
final String categoryThumbnailListString = cursor.getString(cursor
.getColumnIndex(Table.COLUMN_CATEGORIES_THUMBNAIL_LIST));
final List<String> categoryThumbnailList = StringToArray(categoryThumbnailListString);
final List<CategoryItem> categoryList = convertToCategoryItems(categoryNameList,
categoryDescriptionList, categoryThumbnailList);
final boolean isSelected
= Boolean.parseBoolean(cursor.getString(cursor
.getColumnIndex(Table.COLUMN_IS_SELECTED)));
@ -160,6 +169,17 @@ public class BookmarkItemsDao {
);
}
private List<CategoryItem> convertToCategoryItems(List<String> categoryNameList,
List<String> categoryDescriptionList, List<String> categoryThumbnailList) {
List<CategoryItem> categoryItems = new ArrayList<>();
for(int i=0; i<categoryNameList.size(); i++){
categoryItems.add(new CategoryItem(categoryNameList.get(i),
categoryDescriptionList.get(i),
categoryThumbnailList.get(i), false));
}
return categoryItems;
}
/**
* Converts string to List
* @param listString comma separated single string from of list items
@ -188,12 +208,35 @@ public class BookmarkItemsDao {
* @return ContentValues
*/
private ContentValues toContentValues(final DepictedItem depictedItem) {
final List<String> namesOfCommonsCategories = new ArrayList<>();
for (final CategoryItem category :
depictedItem.getCommonsCategories()) {
namesOfCommonsCategories.add(category.getName());
}
final List<String> descriptionsOfCommonsCategories = new ArrayList<>();
for (final CategoryItem category :
depictedItem.getCommonsCategories()) {
descriptionsOfCommonsCategories.add(category.getDescription());
}
final List<String> thumbnailsOfCommonsCategories = new ArrayList<>();
for (final CategoryItem category :
depictedItem.getCommonsCategories()) {
thumbnailsOfCommonsCategories.add(category.getThumbnail());
}
final ContentValues cv = new ContentValues();
cv.put(Table.COLUMN_NAME, depictedItem.getName());
cv.put(Table.COLUMN_DESCRIPTION, depictedItem.getDescription());
cv.put(Table.COLUMN_IMAGE, depictedItem.getImageUrl());
cv.put(Table.COLUMN_INSTANCE_LIST, ArrayToString(depictedItem.getInstanceOfs()));
cv.put(Table.COLUMN_CATEGORIES_LIST, ArrayToString(depictedItem.getCommonsCategories()));
cv.put(Table.COLUMN_CATEGORIES_NAME_LIST, ArrayToString(namesOfCommonsCategories));
cv.put(Table.COLUMN_CATEGORIES_DESCRIPTION_LIST,
ArrayToString(descriptionsOfCommonsCategories));
cv.put(Table.COLUMN_CATEGORIES_THUMBNAIL_LIST,
ArrayToString(thumbnailsOfCommonsCategories));
cv.put(Table.COLUMN_IS_SELECTED, depictedItem.isSelected());
cv.put(Table.COLUMN_ID, depictedItem.getId());
return cv;
@ -208,7 +251,9 @@ public class BookmarkItemsDao {
public static final String COLUMN_DESCRIPTION = "item_description";
public static final String COLUMN_IMAGE = "item_image_url";
public static final String COLUMN_INSTANCE_LIST = "item_instance_of";
public static final String COLUMN_CATEGORIES_LIST = "item_categories";
public static final String COLUMN_CATEGORIES_NAME_LIST = "item_name_categories";
public static final String COLUMN_CATEGORIES_DESCRIPTION_LIST = "item_description_categories";
public static final String COLUMN_CATEGORIES_THUMBNAIL_LIST = "item_thumbnail_categories";
public static final String COLUMN_IS_SELECTED = "item_is_selected";
public static final String COLUMN_ID = "item_id";
@ -217,7 +262,9 @@ public class BookmarkItemsDao {
COLUMN_DESCRIPTION,
COLUMN_IMAGE,
COLUMN_INSTANCE_LIST,
COLUMN_CATEGORIES_LIST,
COLUMN_CATEGORIES_NAME_LIST,
COLUMN_CATEGORIES_DESCRIPTION_LIST,
COLUMN_CATEGORIES_THUMBNAIL_LIST,
COLUMN_IS_SELECTED,
COLUMN_ID
};
@ -228,7 +275,9 @@ public class BookmarkItemsDao {
+ COLUMN_DESCRIPTION + " STRING,"
+ COLUMN_IMAGE + " STRING,"
+ COLUMN_INSTANCE_LIST + " STRING,"
+ COLUMN_CATEGORIES_LIST + " STRING,"
+ COLUMN_CATEGORIES_NAME_LIST + " STRING,"
+ COLUMN_CATEGORIES_DESCRIPTION_LIST + " STRING,"
+ COLUMN_CATEGORIES_THUMBNAIL_LIST + " STRING,"
+ COLUMN_IS_SELECTED + " STRING,"
+ COLUMN_ID + " STRING PRIMARY KEY"
+ ");";

View file

@ -56,7 +56,7 @@ class CategoriesModel @Inject constructor(
// Newly used category...
if (category == null) {
category = Category(null, item.name, Date(), 0)
category = Category(null, item.name, item.description, item.thumbnail, Date(), 0)
}
category.incTimesUsed()
categoryDao.save(category)
@ -74,14 +74,14 @@ class CategoriesModel @Inject constructor(
selectedDepictions: List<DepictedItem>
): Observable<List<CategoryItem>> {
return suggestionsOrSearch(term, imageTitleList, selectedDepictions)
.map { it.map { CategoryItem(it, false) } }
.map { it.map { CategoryItem(it.name, it.description, it.thumbnail, false) } }
}
private fun suggestionsOrSearch(
term: String,
imageTitleList: List<String>,
selectedDepictions: List<DepictedItem>
): Observable<List<String>> {
): Observable<List<CategoryItem>> {
return if (TextUtils.isEmpty(term))
Observable.combineLatest(
categoriesFromDepiction(selectedDepictions),
@ -100,10 +100,10 @@ class CategoriesModel @Inject constructor(
Observable.just(selectedDepictions.map { it.commonsCategories }.flatten())
private fun combine(
depictionCategories: List<String>,
locationCategories: List<String>,
titles: List<String>,
recents: List<String>
depictionCategories: List<CategoryItem>,
locationCategories: List<CategoryItem>,
titles: List<CategoryItem>,
recents: List<CategoryItem>
) = depictionCategories + locationCategories + titles + recents
@ -115,7 +115,7 @@ class CategoriesModel @Inject constructor(
private fun titleCategories(titleList: List<String>) =
if (titleList.isNotEmpty())
Observable.combineLatest(titleList.map { getTitleCategories(it) }) { searchResults ->
searchResults.map { it as List<String> }.flatten()
searchResults.map { it as List<CategoryItem> }.flatten()
}
else
Observable.just(emptyList())
@ -125,7 +125,7 @@ class CategoriesModel @Inject constructor(
* @param title
* @return
*/
private fun getTitleCategories(title: String): Observable<List<String>> {
private fun getTitleCategories(title: String): Observable<List<CategoryItem>> {
return categoryClient.searchCategories(title, SEARCH_CATS_LIMIT).toObservable()
}

View file

@ -10,15 +10,19 @@ import java.util.Date;
public class Category {
private Uri contentUri;
private String name;
private String description;
private String thumbnail;
private Date lastUsed;
private int timesUsed;
public Category() {
}
public Category(Uri contentUri, String name, Date lastUsed, int timesUsed) {
public Category(Uri contentUri, String name, String description, String thumbnail, Date lastUsed, int timesUsed) {
this.contentUri = contentUri;
this.name = name;
this.description = description;
this.thumbnail = thumbnail;
this.lastUsed = lastUsed;
this.timesUsed = timesUsed;
}
@ -93,4 +97,19 @@ public class Category {
this.contentUri = contentUri;
}
public String getDescription() {
return description;
}
public String getThumbnail() {
return thumbnail;
}
public void setDescription(final String description) {
this.description = description;
}
public void setThumbnail(final String thumbnail) {
this.thumbnail = thumbnail;
}
}

View file

@ -16,7 +16,7 @@ const val CATEGORY_NEEDING_CATEGORIES = "needing categories"
*/
@Singleton
class CategoryClient @Inject constructor(private val categoryInterface: CategoryInterface) :
ContinuationClient<MwQueryResponse, String>() {
ContinuationClient<MwQueryResponse, CategoryItem>() {
/**
* Searches for categories containing the specified string.
@ -28,7 +28,7 @@ class CategoryClient @Inject constructor(private val categoryInterface: Category
*/
@JvmOverloads
fun searchCategories(filter: String?, itemLimit: Int, offset: Int = 0):
Single<List<String>> {
Single<List<CategoryItem>> {
return responseMapper(categoryInterface.searchCategories(filter, itemLimit, offset))
}
@ -42,7 +42,7 @@ class CategoryClient @Inject constructor(private val categoryInterface: Category
*/
@JvmOverloads
fun searchCategoriesForPrefix(prefix: String?, itemLimit: Int, offset: Int = 0):
Single<List<String>> {
Single<List<CategoryItem>> {
return responseMapper(
categoryInterface.searchCategoriesForPrefix(prefix, itemLimit, offset)
)
@ -55,7 +55,7 @@ class CategoryClient @Inject constructor(private val categoryInterface: Category
* @param categoryName Category name as defined on commons
* @return Observable emitting the categories returned. If our search yielded "Category:Test", "Test" is emitted.
*/
fun getSubCategoryList(categoryName: String): Single<List<String>> {
fun getSubCategoryList(categoryName: String): Single<List<CategoryItem>> {
return continuationRequest(SUB_CATEGORY_CONTINUATION_PREFIX, categoryName) {
categoryInterface.getSubCategoryList(
categoryName, it
@ -70,7 +70,7 @@ class CategoryClient @Inject constructor(private val categoryInterface: Category
* @param categoryName Category name as defined on commons
* @return
*/
fun getParentCategoryList(categoryName: String): Single<List<String>> {
fun getParentCategoryList(categoryName: String): Single<List<CategoryItem>> {
return continuationRequest(PARENT_CATEGORY_CONTINUATION_PREFIX, categoryName) {
categoryInterface.getParentCategoryList(categoryName, it)
}
@ -87,7 +87,7 @@ class CategoryClient @Inject constructor(private val categoryInterface: Category
override fun responseMapper(
networkResult: Single<MwQueryResponse>,
key: String?
): Single<List<String>> {
): Single<List<CategoryItem>> {
return networkResult
.map {
handleContinuationResponse(it.continuation(), key)
@ -96,7 +96,10 @@ class CategoryClient @Inject constructor(private val categoryInterface: Category
.map {
it.filter {
page -> page.categoryInfo() == null || !page.categoryInfo().isHidden
}.map { page -> page.title().replace(CATEGORY_PREFIX, "") }
}.map {
CategoryItem(it.title().replace(CATEGORY_PREFIX, ""),
it.description().toString(), it.thumbUrl().toString(), false)
}
}
}
}

View file

@ -79,8 +79,8 @@ public class CategoryDao {
* @return a list containing recent categories
*/
@NonNull
List<String> recentCategories(int limit) {
List<String> items = new ArrayList<>();
List<CategoryItem> recentCategories(int limit) {
List<CategoryItem> items = new ArrayList<>();
Cursor cursor = null;
ContentProviderClient db = clientProvider.get();
try {
@ -93,7 +93,9 @@ public class CategoryDao {
// fixme add a limit on the original query instead of falling out of the loop?
while (cursor != null && cursor.moveToNext()
&& cursor.getPosition() < limit) {
items.add(fromCursor(cursor).getName());
items.add(new CategoryItem(fromCursor(cursor).getName(),
fromCursor(cursor).getDescription(), fromCursor(cursor).getThumbnail(),
false));
}
} catch (RemoteException e) {
throw new RuntimeException(e);
@ -112,6 +114,8 @@ public class CategoryDao {
return new Category(
CategoryContentProvider.uriForId(cursor.getInt(cursor.getColumnIndex(Table.COLUMN_ID))),
cursor.getString(cursor.getColumnIndex(Table.COLUMN_NAME)),
cursor.getString(cursor.getColumnIndex(Table.COLUMN_DESCRIPTION)),
cursor.getString(cursor.getColumnIndex(Table.COLUMN_THUMBNAIL)),
new Date(cursor.getLong(cursor.getColumnIndex(Table.COLUMN_LAST_USED))),
cursor.getInt(cursor.getColumnIndex(Table.COLUMN_TIMES_USED))
);
@ -120,6 +124,8 @@ public class CategoryDao {
private ContentValues toContentValues(Category category) {
ContentValues cv = new ContentValues();
cv.put(CategoryDao.Table.COLUMN_NAME, category.getName());
cv.put(Table.COLUMN_DESCRIPTION, category.getDescription());
cv.put(Table.COLUMN_THUMBNAIL, category.getThumbnail());
cv.put(CategoryDao.Table.COLUMN_LAST_USED, category.getLastUsed().getTime());
cv.put(CategoryDao.Table.COLUMN_TIMES_USED, category.getTimesUsed());
return cv;
@ -130,6 +136,8 @@ public class CategoryDao {
public static final String COLUMN_ID = "_id";
static final String COLUMN_NAME = "name";
static final String COLUMN_DESCRIPTION = "description";
static final String COLUMN_THUMBNAIL = "thumbnail";
static final String COLUMN_LAST_USED = "last_used";
static final String COLUMN_TIMES_USED = "times_used";
@ -137,6 +145,8 @@ public class CategoryDao {
public static final String[] ALL_FIELDS = {
COLUMN_ID,
COLUMN_NAME,
COLUMN_DESCRIPTION,
COLUMN_THUMBNAIL,
COLUMN_LAST_USED,
COLUMN_TIMES_USED
};
@ -146,6 +156,8 @@ public class CategoryDao {
static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
+ COLUMN_ID + " INTEGER PRIMARY KEY,"
+ COLUMN_NAME + " STRING,"
+ COLUMN_DESCRIPTION + " STRING,"
+ COLUMN_THUMBNAIL + " STRING,"
+ COLUMN_LAST_USED + " INTEGER,"
+ COLUMN_TIMES_USED + " INTEGER"
+ ");";

View file

@ -94,8 +94,14 @@ public class CategoryEditSearchRecyclerViewAdapter
@Override
protected FilterResults performFiltering(CharSequence constraint) {
FilterResults results = new FilterResults();
List<String> resultCategories = categoryClient.searchCategories(constraint.toString(), 10).blockingGet();
results.values = resultCategories;
List<CategoryItem> resultCategories = categoryClient
.searchCategories(constraint.toString(), 10).blockingGet();
final List<String> namesOfCommonsCategories = new ArrayList<>();
for (final CategoryItem category :
resultCategories) {
namesOfCommonsCategories.add(category.getName());
}
results.values = namesOfCommonsCategories;
results.count = resultCategories.size();
return results;
}

View file

@ -20,9 +20,11 @@ public interface CategoryInterface {
* @return
*/
@GET("w/api.php?action=query&format=json&formatversion=2"
+ "&generator=search&gsrnamespace=14")
+ "&generator=search&prop=description|pageimages&piprop=thumbnail&pithumbsize=70"
+ "&gsrnamespace=14")
Single<MwQueryResponse> searchCategories(@Query("gsrsearch") String filter,
@Query("gsrlimit") int itemLimit, @Query("gsroffset") int offset);
@Query("gsrlimit") int itemLimit,
@Query("gsroffset") int offset);
/**
* Searches for categories starting with the specified prefix.
@ -32,9 +34,11 @@ public interface CategoryInterface {
* @return
*/
@GET("w/api.php?action=query&format=json&formatversion=2"
+ "&generator=allcategories&prop=categoryinfo")
+ "&generator=allcategories&prop=categoryinfo|description|pageimages&piprop=thumbnail"
+ "&pithumbsize=70")
Single<MwQueryResponse> searchCategoriesForPrefix(@Query("gacprefix") String prefix,
@Query("gaclimit") int itemLimit, @Query("gacoffset") int offset);
@Query("gaclimit") int itemLimit,
@Query("gacoffset") int offset);
@GET("w/api.php?action=query&format=json&formatversion=2"
+ "&generator=categorymembers&gcmtype=subcat"

View file

@ -4,7 +4,8 @@ import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
@Parcelize
data class CategoryItem(val name: String, var isSelected: Boolean) : Parcelable {
data class CategoryItem(val name: String, val description: String,
val thumbnail: String, var isSelected: Boolean) : Parcelable {
override fun toString(): String {
return "CategoryItem: '$name'"

View file

@ -250,8 +250,10 @@ public class NetworkingModule {
@Provides
@Singleton
public CategoryInterface provideCategoryInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {
return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, CategoryInterface.class);
public CategoryInterface provideCategoryInterface(
@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {
return ServiceFactory
.get(commonsWikiSite, BuildConfig.COMMONS_URL, CategoryInterface.class);
}
@Provides

View file

@ -14,6 +14,6 @@ class PageableParentCategoriesDataSource @Inject constructor(
if (startPosition == 0) {
categoryClient.resetParentCategoryContinuation(query)
}
categoryClient.getParentCategoryList(query).blockingGet()
categoryClient.getParentCategoryList(query).blockingGet().map { it.name }
}
}

View file

@ -12,5 +12,6 @@ class PageableSearchCategoriesDataSource @Inject constructor(
override val loadFunction = { loadSize: Int, startPosition: Int ->
categoryClient.searchCategories(query, loadSize, startPosition).blockingGet()
.map { it.name }
}
}

View file

@ -14,6 +14,6 @@ class PageableSubCategoriesDataSource @Inject constructor(
if (startPosition == 0) {
categoryClient.resetSubCategoryContinuation(query)
}
categoryClient.getSubCategoryList(query).blockingGet()
categoryClient.getSubCategoryList(query).blockingGet().map { it.name }
}
}

View file

@ -3,6 +3,7 @@ package fr.free.nrw.commons.mwapi;
import static fr.free.nrw.commons.category.CategoryClientKt.CATEGORY_PREFIX;
import com.google.gson.Gson;
import fr.free.nrw.commons.category.CategoryItem;
import io.reactivex.Single;
import java.util.ArrayList;
import java.util.Collections;
@ -40,7 +41,7 @@ public class CategoryApi {
this.gson = gson;
}
public Single<List<String>> request(String coords) {
public Single<List<CategoryItem>> request(String coords) {
return Single.fromCallable(() -> {
HttpUrl apiUrl = buildUrl(coords);
Timber.d("URL: %s", apiUrl.toString());
@ -53,12 +54,12 @@ public class CategoryApi {
}
MwQueryResponse apiResponse = gson.fromJson(body.charStream(), MwQueryResponse.class);
Set<String> categories = new LinkedHashSet<>();
Set<CategoryItem> categories = new LinkedHashSet<>();
if (apiResponse != null && apiResponse.query() != null && apiResponse.query().pages() != null) {
for (MwQueryPage page : apiResponse.query().pages()) {
if (page.categories() != null) {
for (MwQueryPage.Category category : page.categories()) {
categories.add(category.title().replace(CATEGORY_PREFIX, ""));
categories.add(new CategoryItem(category.title().replace(CATEGORY_PREFIX, ""), "", "", false));
}
}
}

View file

@ -1,18 +1,19 @@
package fr.free.nrw.commons.upload
import fr.free.nrw.commons.category.CategoryItem
import io.reactivex.subjects.BehaviorSubject
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class GpsCategoryModel @Inject constructor() {
val categoriesFromLocation = BehaviorSubject.createDefault(emptyList<String>())
val categoriesFromLocation = BehaviorSubject.createDefault(emptyList<CategoryItem>())
fun clear() {
categoriesFromLocation.onNext(emptyList())
}
fun setCategoriesFromLocation(categoryList: List<String>) {
fun setCategoriesFromLocation(categoryList: List<CategoryItem>) {
categoriesFromLocation.onNext(categoryList)
}
}

View file

@ -1,8 +1,10 @@
package fr.free.nrw.commons.upload.categories
import fr.free.nrw.commons.category.CategoryItem
import org.jetbrains.annotations.NotNull
class UploadCategoryAdapter(onCategoryClicked: (CategoryItem) -> Unit) :
class UploadCategoryAdapter(
onCategoryClicked: @NotNull() (CategoryItem) -> Unit) :
BaseDelegateAdapter<CategoryItem>(
uploadCategoryDelegate(onCategoryClicked),
areItemsTheSame = { oldItem, newItem -> oldItem.name == newItem.name },

View file

@ -1,20 +1,38 @@
package fr.free.nrw.commons.upload.categories
import android.view.View
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import fr.free.nrw.commons.R
import fr.free.nrw.commons.category.CategoryItem
import fr.free.nrw.commons.databinding.LayoutUploadCategoriesItemBinding
fun uploadCategoryDelegate(onCategoryClicked: (CategoryItem) -> Unit) =
adapterDelegateViewBinding<CategoryItem, CategoryItem, LayoutUploadCategoriesItemBinding>({ layoutInflater, root ->
adapterDelegateViewBinding<CategoryItem, CategoryItem,
LayoutUploadCategoriesItemBinding>({ layoutInflater, root ->
LayoutUploadCategoriesItemBinding.inflate(layoutInflater, root, false)
}) {
binding.root.setOnClickListener {
val onClickListener = { _: View? ->
item.isSelected = !item.isSelected
binding.uploadCategoryCheckbox.isChecked = item.isSelected
onCategoryClicked(item)
}
binding.root.setOnClickListener(onClickListener)
binding.uploadCategoryCheckbox.setOnClickListener(onClickListener)
bind {
binding.uploadCategoryCheckbox.isChecked = item.isSelected
binding.uploadCategoryCheckbox.text = item.name
binding.categoryLabel.text = item.name
if(item.thumbnail != "null") {
binding.categoryImage.setImageURI(item.thumbnail)
} else {
binding.categoryImage.setActualImageResource(R.drawable.commons)
}
if(item.description != "null") {
binding.categoryDescription.text = item.description
} else {
binding.categoryDescription.text = ""
}
}
}

View file

@ -3,6 +3,7 @@ package fr.free.nrw.commons.upload.structure.depictions
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.PrimaryKey
import fr.free.nrw.commons.category.CategoryItem
import fr.free.nrw.commons.nearby.Place
import fr.free.nrw.commons.upload.WikidataItem
import fr.free.nrw.commons.wikidata.WikidataProperties
@ -28,7 +29,7 @@ data class DepictedItem constructor(
val description: String?,
val imageUrl: String?,
val instanceOfs: List<String>,
val commonsCategories: List<String>,
val commonsCategories: List<CategoryItem>,
var isSelected: Boolean,
@PrimaryKey override val id: String
) : WikidataItem, Parcelable {
@ -52,7 +53,8 @@ data class DepictedItem constructor(
getImageUrl(it.value, THUMB_IMAGE_SIZE)
},
entity[INSTANCE_OF].toIds(),
entity[COMMONS_CATEGORY]?.map { (it.mainSnak.dataValue as DataValue.ValueString).value }
entity[COMMONS_CATEGORY]?.map { CategoryItem((it.mainSnak.dataValue as DataValue.ValueString).value,
"", "", false) }
?: emptyList(),
false,
entity.id()

View file

@ -1,5 +1,6 @@
package fr.free.nrw.commons.utils;
import fr.free.nrw.commons.category.CategoryItem;
import java.util.Comparator;
public class StringSortingUtils {
@ -16,10 +17,10 @@ public class StringSortingUtils {
* @param filter String to compare similarity with
* @return Comparator with string similarity
*/
public static Comparator<String> sortBySimilarity(final String filter) {
public static Comparator<CategoryItem> sortBySimilarity(final String filter) {
return (firstItem, secondItem) -> {
double firstItemSimilarity = calculateSimilarity(firstItem, filter);
double secondItemSimilarity = calculateSimilarity(secondItem, filter);
double firstItemSimilarity = calculateSimilarity(firstItem.getName(), filter);
double secondItemSimilarity = calculateSimilarity(secondItem.getName(), filter);
return (int) Math.signum(secondItemSimilarity - firstItemSimilarity);
};
}

View file

@ -1,9 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<CheckBox xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/uploadCategoryCheckbox"
android:layout_width="match_parent"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/category_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<CheckBox
android:id="@+id/upload_category_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checkMark="?android:attr/textCheckMark"
android:checked="false"
android:gravity="center_vertical"
android:padding="@dimen/tiny_gap"/>
android:padding="@dimen/tiny_gap"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/category_image"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/category_image"
android:layout_width="50dp"
android:layout_height="50dp"
android:paddingEnd="@dimen/tiny_gap"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@+id/upload_category_checkbox"
app:layout_constraintTop_toTopOf="parent"
app:placeholderImage="@drawable/commons" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/category_image"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/category_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="@string/label"
android:textStyle="bold" />
<TextView
android:id="@+id/category_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="@string/description" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -652,6 +652,8 @@ Upload your first media by tapping on the add button.</string>
The shadow of the image view of the location picker</string>
<string name="image_location">Image Location</string>
<string name="check_whether_location_is_correct">Check whether location is correct</string>
<string name="label">Label</string>
<string name="description">Description</string>
<string name="title_page_bookmarks_items">Items</string>
<string name="custom_selector_title">Custom Selector</string>
<string name="custom_selector_empty_text">No Images</string>

View file

@ -16,7 +16,7 @@ fun depictedItem(
description: String = "desc",
imageUrl: String = "",
instanceOfs: List<String> = listOf(),
commonsCategories: List<String> = listOf(),
commonsCategories: List<CategoryItem> = listOf(),
isSelected: Boolean = false,
id: String = "entityId"
) = DepictedItem(
@ -29,8 +29,9 @@ fun depictedItem(
id = id
)
fun categoryItem(name: String = "name", selected: Boolean = false) =
CategoryItem(name, selected)
fun categoryItem(name: String = "name", description: String = "desc",
thumbUrl: String = "thumbUrl", selected: Boolean = false) =
CategoryItem(name, description, thumbUrl, selected)
fun media(
thumbUrl: String? = "thumbUrl",

View file

@ -1,6 +1,7 @@
package fr.free.nrw.commons.bookmarks.items
import com.nhaarman.mockitokotlin2.whenever
import fr.free.nrw.commons.category.CategoryItem
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import org.junit.Assert
import org.junit.Before
@ -33,7 +34,8 @@ class BookmarkItemsControllerTest {
list.add(
DepictedItem(
"name", "description", "image url", listOf("instance"),
listOf("categories"), true, "id")
listOf(CategoryItem("category name", "category description",
"category thumbnail", false)), true, "id")
)
return list
}

View file

@ -10,6 +10,7 @@ import android.os.RemoteException
import com.nhaarman.mockitokotlin2.*
import fr.free.nrw.commons.TestCommonsApplication
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table.*
import fr.free.nrw.commons.category.CategoryItem
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import org.junit.Assert
import org.junit.Before
@ -26,7 +27,9 @@ class BookmarkItemsDaoTest {
COLUMN_DESCRIPTION,
COLUMN_IMAGE,
COLUMN_INSTANCE_LIST,
COLUMN_CATEGORIES_LIST,
COLUMN_CATEGORIES_NAME_LIST,
COLUMN_CATEGORIES_DESCRIPTION_LIST,
COLUMN_CATEGORIES_THUMBNAIL_LIST,
COLUMN_IS_SELECTED,
COLUMN_ID,
)
@ -43,7 +46,10 @@ class BookmarkItemsDaoTest {
@Before
fun setUp() {
exampleItemBookmark = DepictedItem("itemName", "itemDescription",
"itemImageUrl", listOf("instance"), listOf("categories"), false,
"itemImageUrl", listOf("instance"), listOf(
CategoryItem("category name", "category description",
"category thumbnail", false)
), false,
"itemID")
testObject = BookmarkItemsDao { client }
}
@ -72,7 +78,9 @@ class BookmarkItemsDaoTest {
Assert.assertEquals("itemDescription", it.description)
Assert.assertEquals("itemImageUrl", it.imageUrl)
Assert.assertEquals(listOf("instance"), it.instanceOfs)
Assert.assertEquals(listOf("categories"), it.commonsCategories)
Assert.assertEquals(listOf(CategoryItem("category name",
"category description",
"category thumbnail", false)), it.commonsCategories)
Assert.assertEquals(false, it.isSelected)
Assert.assertEquals("itemID", it.id)
}
@ -131,7 +139,7 @@ class BookmarkItemsDaoTest {
Assert.assertTrue(testObject.updateBookmarkItem(exampleItemBookmark))
verify(client).insert(eq(BookmarkItemsContentProvider.BASE_URI), captor.capture())
captor.firstValue.let { cv ->
Assert.assertEquals(7, cv.size())
Assert.assertEquals(9, cv.size())
Assert.assertEquals(
exampleItemBookmark.name,
cv.getAsString(COLUMN_NAME)
@ -149,8 +157,16 @@ class BookmarkItemsDaoTest {
cv.getAsString(COLUMN_INSTANCE_LIST)
)
Assert.assertEquals(
exampleItemBookmark.commonsCategories[0],
cv.getAsString(COLUMN_CATEGORIES_LIST)
exampleItemBookmark.commonsCategories[0].name,
cv.getAsString(COLUMN_CATEGORIES_NAME_LIST)
)
Assert.assertEquals(
exampleItemBookmark.commonsCategories[0].description,
cv.getAsString(COLUMN_CATEGORIES_DESCRIPTION_LIST)
)
Assert.assertEquals(
exampleItemBookmark.commonsCategories[0].thumbnail,
cv.getAsString(COLUMN_CATEGORIES_THUMBNAIL_LIST)
)
Assert.assertEquals(
exampleItemBookmark.isSelected,
@ -263,8 +279,8 @@ class BookmarkItemsDaoTest {
for (i in 0 until rowCount) {
addRow(listOf("itemName", "itemDescription",
"itemImageUrl", "instance", "categories", false,
"itemID"))
"itemImageUrl", "instance", "category name", "category description",
"category thumbnail", false, "itemID"))
}
}
}

View file

@ -14,6 +14,7 @@ import com.nhaarman.mockitokotlin2.whenever
import fr.free.nrw.commons.R
import fr.free.nrw.commons.TestAppAdapter
import fr.free.nrw.commons.TestCommonsApplication
import fr.free.nrw.commons.category.CategoryItem
import fr.free.nrw.commons.profile.ProfileActivity
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import org.junit.Assert
@ -62,7 +63,10 @@ class BookmarkItemsFragmentUnitTest {
list.add(
DepictedItem(
"name", "description", "image url", listOf("instance"),
listOf("categories"), true, "id")
listOf(
CategoryItem("category name", "category description",
"category thumbnail", false)
), true, "id")
)
return list
}

View file

@ -34,7 +34,8 @@ class CategoriesModelTest {
fun searchAllFoundCaseTest() {
val categoriesModel = CategoriesModel(categoryClient, mock(), mock())
val expectedList = listOf("Test")
val expectedList = listOf(CategoryItem(
"Test", "", "", false))
whenever(
categoryClient.searchCategoriesForPrefix(
ArgumentMatchers.anyString(),
@ -45,7 +46,8 @@ class CategoriesModelTest {
.thenReturn(Single.just(expectedList))
// Checking if both return "Test"
val expectedItems = expectedList.map { CategoryItem(it, false) }
val expectedItems = expectedList.map { CategoryItem(
it.name, it.description, it.thumbnail, false) }
var categoryTerm = "Test"
categoriesModel.searchAll(categoryTerm, emptyList(), emptyList())
.test()
@ -65,10 +67,12 @@ 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"))
val depictedItem = depictedItem(commonsCategories = listOf(CategoryItem(
"depictionCategory", "", "", false)))
whenever(gpsCategoryModel.categoriesFromLocation)
.thenReturn(BehaviorSubject.createDefault(listOf("gpsCategory")))
.thenReturn(BehaviorSubject.createDefault(listOf(CategoryItem(
"gpsCategory", "", "", false))))
whenever(
categoryClient.searchCategories(
ArgumentMatchers.anyString(),
@ -76,8 +80,10 @@ class CategoriesModelTest {
ArgumentMatchers.anyInt()
)
)
.thenReturn(Single.just(listOf("titleSearch")))
whenever(categoryDao.recentCategories(25)).thenReturn(listOf("recentCategories"))
.thenReturn(Single.just(listOf(CategoryItem(
"titleSearch", "", "", false))))
whenever(categoryDao.recentCategories(25)).thenReturn(listOf(CategoryItem(
"recentCategories","","", false)))
val imageTitleList = listOf("Test")
CategoriesModel(categoryClient, categoryDao, gpsCategoryModel)
.searchAll("", imageTitleList, listOf(depictedItem))

View file

@ -34,10 +34,10 @@ class CategoryClientTest {
.thenReturn(Single.just(mockResponse))
categoryClient.searchCategories("tes", 10)
.test()
.assertValues(listOf("Test"))
.assertValues(listOf(CategoryItem("Test", "", "", false)))
categoryClient.searchCategories("tes", 10, 10)
.test()
.assertValues(listOf("Test"))
.assertValues(listOf(CategoryItem("Test", "", "", false)))
}
@Test
@ -59,10 +59,10 @@ class CategoryClientTest {
.thenReturn(Single.just(mockResponse))
categoryClient.searchCategoriesForPrefix("tes", 10)
.test()
.assertValues(listOf("Test"))
.assertValues(listOf(CategoryItem("Test", "", "", false)))
categoryClient.searchCategoriesForPrefix("tes", 10, 10)
.test()
.assertValues(listOf("Test"))
.assertValues(listOf(CategoryItem("Test", "", "", false)))
}
@Test
@ -84,7 +84,7 @@ class CategoryClientTest {
.thenReturn(Single.just(mockResponse))
categoryClient.getParentCategoryList("tes")
.test()
.assertValues(listOf("Test"))
.assertValues(listOf(CategoryItem("Test", "", "", false)))
}
@Test
@ -104,7 +104,7 @@ class CategoryClientTest {
.thenReturn(Single.just(mockResponse))
categoryClient.getSubCategoryList("tes")
.test()
.assertValues(listOf("Test"))
.assertValues(listOf(CategoryItem("Test", "", "", false)))
}
@Test

View file

@ -24,7 +24,8 @@ import java.util.*
@Config(sdk = [21], application = TestCommonsApplication::class)
class CategoryDaoTest {
private val columns = arrayOf(COLUMN_ID, COLUMN_NAME, COLUMN_LAST_USED, COLUMN_TIMES_USED)
private val columns = arrayOf(COLUMN_ID, COLUMN_NAME, COLUMN_DESCRIPTION,
COLUMN_THUMBNAIL, COLUMN_LAST_USED, COLUMN_TIMES_USED)
private val client: ContentProviderClient = mock()
private val database: SQLiteDatabase = mock()
private val captor = argumentCaptor<ContentValues>()
@ -122,8 +123,10 @@ class CategoryDaoTest {
verify(client).update(eq(category.contentUri), captor.capture(), isNull(), isNull())
captor.firstValue.let { cv ->
assertEquals(3, cv.size())
assertEquals(5, cv.size())
assertEquals(category.name, cv.getAsString(COLUMN_NAME))
assertEquals(category.description, cv.getAsString(COLUMN_DESCRIPTION))
assertEquals(category.thumbnail, cv.getAsString(COLUMN_THUMBNAIL))
assertEquals(category.lastUsed.time, cv.getAsLong(COLUMN_LAST_USED))
assertEquals(category.timesUsed, cv.getAsInteger(COLUMN_TIMES_USED))
}
@ -134,14 +137,17 @@ class CategoryDaoTest {
fun saveNewCategory() {
val contentUri = CategoryContentProvider.uriForId(111)
whenever(client.insert(isA(), isA())).thenReturn(contentUri)
val category = Category(null, "showImageWithItem", Date(234L), 1)
val category = Category(null, "showImageWithItem", "description",
"image", Date(234L), 1)
testObject.save(category)
verify(client).insert(eq(BASE_URI), captor.capture())
captor.firstValue.let { cv ->
assertEquals(3, cv.size())
assertEquals(5, cv.size())
assertEquals(category.name, cv.getAsString(COLUMN_NAME))
assertEquals(category.description, cv.getAsString(COLUMN_DESCRIPTION))
assertEquals(category.thumbnail, cv.getAsString(COLUMN_THUMBNAIL))
assertEquals(category.lastUsed.time, cv.getAsLong(COLUMN_LAST_USED))
assertEquals(category.timesUsed, cv.getAsInteger(COLUMN_TIMES_USED))
assertEquals(contentUri, category.contentUri)
@ -186,6 +192,8 @@ class CategoryDaoTest {
assertEquals(uriForId(1), category?.contentUri)
assertEquals("showImageWithItem", category?.name)
assertEquals("description", category?.description)
assertEquals("image", category?.thumbnail)
assertEquals(123L, category?.lastUsed?.time)
assertEquals(2, category?.timesUsed)
@ -241,7 +249,7 @@ class CategoryDaoTest {
val result = testObject.recentCategories(10)
assertEquals(1, result.size)
assertEquals("showImageWithItem", result[0])
assertEquals("showImageWithItem", result[0].name)
verify(client).query(
eq(BASE_URI),
@ -264,7 +272,7 @@ class CategoryDaoTest {
private fun createCursor(rowCount: Int) = MatrixCursor(columns, rowCount).apply {
for (i in 0 until rowCount) {
addRow(listOf("1", "showImageWithItem", "123", "2"))
addRow(listOf("1", "showImageWithItem", "description", "image", "123", "2"))
}
}

View file

@ -67,13 +67,15 @@ class CategoriesPresenterTest {
)
whenever(repository.containsYear("selected")).thenReturn(false)
whenever(repository.containsYear("doesContainYear")).thenReturn(true)
whenever(repository.selectedCategories).thenReturn(listOf(categoryItem("selected", true)))
whenever(repository.selectedCategories).thenReturn(listOf(
categoryItem("selected", "", "",true)))
categoriesPresenter.searchForCategories("test")
testScheduler.triggerActions()
verify(view).showProgress(true)
verify(view).showError(null)
verify(view).setCategories(null)
verify(view).setCategories(listOf(categoryItem("selected", true)))
verify(view).setCategories(listOf(
categoryItem("selected", "", "", true)))
verify(view).showProgress(false)
verifyNoMoreInteractions(view)
}

View file

@ -1,5 +1,6 @@
package fr.free.nrw.commons.upload
import fr.free.nrw.commons.category.CategoryItem
import org.junit.Before
import org.junit.Test
@ -18,7 +19,8 @@ class GpsCategoryModelTest {
@Test
fun `setCategoriesFromLocation emits the new value`() {
val expectedList = listOf("category")
val expectedList = listOf(
CategoryItem("category", "", "", false))
gpsCategoryModel.categoriesFromLocation.test()
.also { gpsCategoryModel.setCategoriesFromLocation(expectedList) }
.assertValues(emptyList(), expectedList)

View file

@ -110,7 +110,7 @@ class DepictedItemTest {
)
)
)
).commonsCategories,
).commonsCategories.map { it.name },
listOf("1", "2"))
}

View file

@ -1,5 +1,6 @@
package fr.free.nrw.commons.utils
import fr.free.nrw.commons.category.CategoryItem
import fr.free.nrw.commons.utils.StringSortingUtils.sortBySimilarity
import org.junit.Assert.assertEquals
import org.junit.Test
@ -9,8 +10,18 @@ class StringSortingUtilsTest {
@Test
fun testSortingNumbersBySimilarity() {
val actualList = listOf("1234567", "4567", "12345", "123", "1234")
val expectedList = listOf("1234", "12345", "123", "1234567", "4567")
val actualList = listOf(
CategoryItem("1234567", "", "", false),
CategoryItem("4567", "", "", false),
CategoryItem("12345", "", "", false),
CategoryItem("123", "", "", false),
CategoryItem("1234", "", "", false))
val expectedList = listOf(
CategoryItem("1234", "", "", false),
CategoryItem("12345", "", "", false),
CategoryItem("123", "", "", false),
CategoryItem("1234567", "", "", false),
CategoryItem("4567", "", "", false))
sort(actualList, sortBySimilarity("1234"))
@ -20,22 +31,22 @@ class StringSortingUtilsTest {
@Test
fun testSortingTextBySimilarity() {
val actualList = listOf(
"The quick brown fox",
"quick brown fox",
"The",
"The quick ",
"The fox",
"brown fox",
"fox"
CategoryItem("The quick brown fox", "", "", false),
CategoryItem("quick brown fox", "", "", false),
CategoryItem("The", "", "", false),
CategoryItem("The quick ", "", "", false),
CategoryItem("The fox", "", "", false),
CategoryItem("brown fox", "", "", false),
CategoryItem("fox", "", "", false)
)
val expectedList = listOf(
"The",
"The fox",
"The quick ",
"The quick brown fox",
"quick brown fox",
"brown fox",
"fox"
CategoryItem("The", "", "", false),
CategoryItem("The fox", "", "", false),
CategoryItem("The quick ", "", "", false),
CategoryItem("The quick brown fox", "", "", false),
CategoryItem("quick brown fox", "", "", false),
CategoryItem("brown fox", "", "", false),
CategoryItem("fox", "", "", false)
)
sort(actualList, sortBySimilarity("The"))
@ -46,18 +57,18 @@ class StringSortingUtilsTest {
@Test
fun testSortingSymbolsBySimilarity() {
val actualList = listOf(
"$$$$$",
"****",
"**$*",
"*$*$",
".*$"
CategoryItem("$$$$$", "", "", false),
CategoryItem("****", "", "", false),
CategoryItem("**$*", "", "", false),
CategoryItem("*$*$", "", "", false),
CategoryItem(".*$", "", "", false)
)
val expectedList = listOf(
"**$*",
"*$*$",
".*$",
"****",
"$$$$$"
CategoryItem("**$*", "", "", false),
CategoryItem("*$*$", "", "", false),
CategoryItem(".*$", "", "", false),
CategoryItem("****", "", "", false),
CategoryItem("$$$$$", "", "", false)
)
sort(actualList, sortBySimilarity("**$"))
@ -69,25 +80,25 @@ class StringSortingUtilsTest {
fun testSortingMixedStringsBySimilarity() {
// Sample from Category:2018 Android phones
val actualList = listOf(
"ASUS ZenFone 5 (2018)",
"Google Pixel 3",
"HTC U12",
"Huawei P20",
"LG G7 ThinQ",
"Samsung Galaxy A8 (2018)",
"Samsung Galaxy S9",
CategoryItem("ASUS ZenFone 5 (2018)", "", "", false),
CategoryItem("Google Pixel 3", "", "", false),
CategoryItem("HTC U12", "", "", false),
CategoryItem("Huawei P20", "", "", false),
CategoryItem("LG G7 ThinQ", "", "", false),
CategoryItem("Samsung Galaxy A8 (2018)", "", "", false),
CategoryItem("Samsung Galaxy S9", "", "", false),
// One with more complicated symbols
"MadeUpPhone 2018.$£#你好"
CategoryItem("MadeUpPhone 2018.$£#你好", "", "", false)
)
val expectedList = listOf(
"Samsung Galaxy S9",
"ASUS ZenFone 5 (2018)",
"Samsung Galaxy A8 (2018)",
"Google Pixel 3",
"HTC U12",
"Huawei P20",
"LG G7 ThinQ",
"MadeUpPhone 2018.$£#你好"
CategoryItem("Samsung Galaxy S9", "", "", false),
CategoryItem("ASUS ZenFone 5 (2018)", "", "", false),
CategoryItem("Samsung Galaxy A8 (2018)", "", "", false),
CategoryItem("Google Pixel 3", "", "", false),
CategoryItem("HTC U12", "", "", false),
CategoryItem("Huawei P20", "", "", false),
CategoryItem("LG G7 ThinQ", "", "", false),
CategoryItem("MadeUpPhone 2018.$£#你好", "", "", false)
)
sort(actualList, sortBySimilarity("S9"))
@ -98,26 +109,26 @@ class StringSortingUtilsTest {
@Test
fun testSortingWithEmptyStrings() {
val actualList = listOf(
"brown fox",
"",
"quick brown fox",
"the",
"",
"the fox",
"fox",
"",
""
CategoryItem("brown fox", "", "", false),
CategoryItem("", "", "", false),
CategoryItem("quick brown fox", "", "", false),
CategoryItem("the", "", "", false),
CategoryItem("", "", "", false),
CategoryItem("the fox", "", "", false),
CategoryItem("fox", "", "", false),
CategoryItem("", "", "", false),
CategoryItem("", "", "", false)
)
val expectedList = listOf(
"the fox",
"brown fox",
"the",
"fox",
"quick brown fox",
"",
"",
"",
""
CategoryItem("the fox", "", "", false),
CategoryItem("brown fox", "", "", false),
CategoryItem("the", "", "", false),
CategoryItem("fox", "", "", false),
CategoryItem("quick brown fox", "", "", false),
CategoryItem("", "", "", false),
CategoryItem("", "", "", false),
CategoryItem("", "", "", false),
CategoryItem("", "", "", false)
)
sort(actualList, sortBySimilarity("the fox"))