Merge remote-tracking branch 'origin/master' into macgills/3756-depictions-pagination

# Conflicts:
#	app/src/main/java/fr/free/nrw/commons/category/CategoryClient.java
#	app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java
#	app/src/main/java/fr/free/nrw/commons/upload/categories/UploadCategoriesFragment.java
#	app/src/main/res/values/strings.xml
#	gradle.properties
This commit is contained in:
Sean Mac Gillicuddy 2020-05-19 15:31:30 +01:00
commit 8d13122e0e
32 changed files with 651 additions and 907 deletions

View file

@ -127,6 +127,8 @@ dependencies {
implementation "androidx.preference:preference:$PREFERENCE_VERSION" implementation "androidx.preference:preference:$PREFERENCE_VERSION"
// Kotlin // Kotlin
implementation "androidx.preference:preference-ktx:$PREFERENCE_VERSION" implementation "androidx.preference:preference-ktx:$PREFERENCE_VERSION"
implementation "androidx.multidex:multidex:$MULTIDEX_VERSION"
} }
android { android {
@ -144,6 +146,8 @@ android {
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments clearPackageData: 'true' testInstrumentationRunnerArguments clearPackageData: 'true'
multiDexEnabled true
testOptions { testOptions {
execution 'ANDROIDX_TEST_ORCHESTRATOR' execution 'ANDROIDX_TEST_ORCHESTRATOR'
} }

View file

@ -1,242 +0,0 @@
package fr.free.nrw.commons.category;
import android.text.TextUtils;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.upload.GpsCategoryModel;
import fr.free.nrw.commons.utils.StringSortingUtils;
import io.reactivex.Observable;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import timber.log.Timber;
/**
* The model class for categories in upload
*/
public class CategoriesModel{
private static final int SEARCH_CATS_LIMIT = 25;
private final CategoryClient categoryClient;
private final CategoryDao categoryDao;
private final JsonKvStore directKvStore;
private final GpsCategoryModel gpsCategoryModel;
private List<CategoryItem> selectedCategories;
@Inject
public CategoriesModel(CategoryClient categoryClient,
CategoryDao categoryDao,
@Named("default_preferences") JsonKvStore directKvStore,
final GpsCategoryModel gpsCategoryModel) {
this.categoryClient = categoryClient;
this.categoryDao = categoryDao;
this.directKvStore = directKvStore;
this.gpsCategoryModel = gpsCategoryModel;
this.selectedCategories = new ArrayList<>();
}
/**
* Sorts CategoryItem by similarity
* @param filter
* @return
*/
public Comparator<CategoryItem> sortBySimilarity(final String filter) {
Comparator<String> stringSimilarityComparator = StringSortingUtils.sortBySimilarity(filter);
return (firstItem, secondItem) -> stringSimilarityComparator
.compare(firstItem.getName(), secondItem.getName());
}
/**
* Returns if the item contains an year
* @param item
* @return
*/
public boolean containsYear(String item) {
//Check for current and previous year to exclude these categories from removal
Calendar now = Calendar.getInstance();
int year = now.get(Calendar.YEAR);
String yearInString = String.valueOf(year);
int prevYear = year - 1;
String prevYearInString = String.valueOf(prevYear);
Timber.d("Previous year: %s", prevYearInString);
//Check if item contains a 4-digit word anywhere within the string (.* is wildcard)
//And that item does not equal the current year or previous year
//And if it is an irrelevant category such as Media_needing_categories_as_of_16_June_2017(Issue #750)
//Check if the year in the form of XX(X)0s is relevant, i.e. in the 2000s or 2010s as stated in Issue #1029
return ((item.matches(".*(19|20)\\d{2}.*") && !item.contains(yearInString) && !item.contains(prevYearInString))
|| item.matches("(.*)needing(.*)") || item.matches("(.*)taken on(.*)")
|| (item.matches(".*0s.*") && !item.matches(".*(200|201)0s.*")));
}
/**
* Updates category count in category dao
* @param item
*/
public void updateCategoryCount(CategoryItem item) {
Category category = categoryDao.find(item.getName());
// Newly used category...
if (category == null) {
category = new Category(null, item.getName(), new Date(), 0);
}
category.incTimesUsed();
categoryDao.save(category);
}
/**
* Regional category search
* @param term
* @param imageTitleList
* @return
*/
public Observable<CategoryItem> searchAll(String term, List<String> imageTitleList) {
//If query text is empty, show him category based on gps and title and recent searches
if (TextUtils.isEmpty(term)) {
Observable<CategoryItem> categoryItemObservable =
Observable.concat(gpsCategories(), titleCategories(imageTitleList));
if (hasDirectCategories()) {
return Observable.concat(
categoryItemObservable,
directCategories(),
recentCategories()
);
}
return categoryItemObservable;
}
//otherwise, search API for matching categories
//term passed as lower case to make search case-insensitive(taking only lower case for everything)
return categoryClient
.searchCategoriesForPrefix(term.toLowerCase(), SEARCH_CATS_LIMIT)
.map(name -> new CategoryItem(name, false));
}
/**
* Returns if we have a category in DirectKV Store
* @return
*/
private boolean hasDirectCategories() {
return !directKvStore.getString("Category", "").equals("");
}
/**
* Returns categories in DirectKVStore
* @return
*/
private Observable<CategoryItem> directCategories() {
String directCategory = directKvStore.getString("Category", "");
List<String> categoryList = new ArrayList<>();
Timber.d("Direct category found: " + directCategory);
if (!directCategory.equals("")) {
categoryList.add(directCategory);
Timber.d("DirectCat does not equal emptyString. Direct Cat list has " + categoryList);
}
return Observable.fromIterable(categoryList).map(name -> new CategoryItem(name, false));
}
/**
* Returns GPS categories
* @return
*/
Observable<CategoryItem> gpsCategories() {
return Observable.fromIterable(gpsCategoryModel.getCategoryList())
.map(name -> new CategoryItem(name, false));
}
/**
* Returns title based categories
* @param titleList
* @return
*/
private Observable<CategoryItem> titleCategories(List<String> titleList) {
return Observable.fromIterable(titleList)
.concatMap(this::getTitleCategories);
}
/**
* Return category for single title
* title is converted to lower case to make search case-insensitive
* @param title
* @return
*/
private Observable<CategoryItem> getTitleCategories(String title) {
return categoryClient.searchCategories(title.toLowerCase(), SEARCH_CATS_LIMIT)
.map(name -> new CategoryItem(name, false));
}
/**
* Returns recent categories
* @return
*/
private Observable<CategoryItem> recentCategories() {
return Observable.fromIterable(categoryDao.recentCategories(SEARCH_CATS_LIMIT))
.map(s -> new CategoryItem(s, false));
}
/**
* Handles category item selection
* @param item
*/
public void onCategoryItemClicked(CategoryItem item) {
if (item.isSelected()) {
selectCategory(item);
updateCategoryCount(item);
} else {
unselectCategory(item);
}
}
/**
* Select's category
* @param item
*/
public void selectCategory(CategoryItem item) {
selectedCategories.add(item);
}
/**
* Unselect Category
* @param item
*/
public void unselectCategory(CategoryItem item) {
selectedCategories.remove(item);
}
/**
* Get Selected Categories
* @return
*/
public List<CategoryItem> getSelectedCategories() {
return selectedCategories;
}
/**
* Get Categories String List
* @return
*/
public List<String> getCategoryStringList() {
List<String> output = new ArrayList<>();
for (CategoryItem item : selectedCategories) {
output.add(item.getName());
}
return output;
}
/**
* Cleanup the existing in memory cache's
*/
public void cleanUp() {
this.selectedCategories.clear();
}
}

View file

@ -0,0 +1,152 @@
package fr.free.nrw.commons.category
import android.text.TextUtils
import fr.free.nrw.commons.upload.GpsCategoryModel
import fr.free.nrw.commons.utils.StringSortingUtils
import io.reactivex.Observable
import io.reactivex.functions.Function3
import timber.log.Timber
import java.util.*
import javax.inject.Inject
/**
* The model class for categories in upload
*/
class CategoriesModel @Inject constructor(
private val categoryClient: CategoryClient,
private val categoryDao: CategoryDao,
private val gpsCategoryModel: GpsCategoryModel
) {
private val selectedCategories: MutableList<CategoryItem> = mutableListOf()
/**
* Returns if the item contains an year
* @param item
* @return
*/
fun containsYear(item: String): Boolean {
//Check for current and previous year to exclude these categories from removal
val now = Calendar.getInstance()
val year = now[Calendar.YEAR]
val yearInString = year.toString()
val prevYear = year - 1
val prevYearInString = prevYear.toString()
Timber.d("Previous year: %s", prevYearInString)
//Check if item contains a 4-digit word anywhere within the string (.* is wildcard)
//And that item does not equal the current year or previous year
//And if it is an irrelevant category such as Media_needing_categories_as_of_16_June_2017(Issue #750)
//Check if the year in the form of XX(X)0s is relevant, i.e. in the 2000s or 2010s as stated in Issue #1029
return item.matches(".*(19|20)\\d{2}.*".toRegex())
&& !item.contains(yearInString)
&& !item.contains(prevYearInString)
|| item.matches("(.*)needing(.*)".toRegex())
|| item.matches("(.*)taken on(.*)".toRegex())
|| item.matches(".*0s.*".toRegex())
&& !item.matches(".*(200|201)0s.*".toRegex())
}
/**
* Updates category count in category dao
* @param item
*/
fun updateCategoryCount(item: CategoryItem) {
var category = categoryDao.find(item.name)
// Newly used category...
if (category == null) {
category = Category(null, item.name, Date(), 0)
}
category.incTimesUsed()
categoryDao.save(category)
}
/**
* Regional category search
* @param term
* @param imageTitleList
* @return
*/
fun searchAll(term: String, imageTitleList: List<String>): Observable<List<CategoryItem>> {
return suggestionsOrSearch(term, imageTitleList)
.map { it.map { CategoryItem(it, false) } }
}
private fun suggestionsOrSearch(term: String, imageTitleList: List<String>):
Observable<List<String>> {
return if (TextUtils.isEmpty(term))
Observable.combineLatest(
gpsCategoryModel.categoriesFromLocation,
titleCategories(imageTitleList),
Observable.just(categoryDao.recentCategories(SEARCH_CATS_LIMIT)),
Function3(::combine)
)
else
categoryClient.searchCategoriesForPrefix(term.toLowerCase(), SEARCH_CATS_LIMIT)
.map { it.sortedWith(StringSortingUtils.sortBySimilarity(term)) }
}
private fun combine(
locationCategories: List<String>,
titles: List<String>,
recents: List<String>
) = locationCategories + titles + recents
/**
* Returns title based categories
* @param titleList
* @return
*/
private fun titleCategories(titleList: List<String>): Observable<List<String>> {
return 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
* title is converted to lower case to make search case-insensitive
* @param title
* @return
*/
private fun getTitleCategories(title: String): Observable<List<String>> {
return categoryClient.searchCategories(title.toLowerCase(), SEARCH_CATS_LIMIT)
}
/**
* Handles category item selection
* @param item
*/
fun onCategoryItemClicked(item: CategoryItem) {
if (item.isSelected) {
selectedCategories.add(item)
updateCategoryCount(item)
} else {
selectedCategories.remove(item)
}
}
/**
* Get Selected Categories
* @return
*/
fun getSelectedCategories(): List<CategoryItem> {
return selectedCategories
}
/**
* Cleanup the existing in memory cache's
*/
fun cleanUp() {
selectedCategories.clear()
}
companion object {
private const val SEARCH_CATS_LIMIT = 25
}
}

View file

@ -1,126 +0,0 @@
package fr.free.nrw.commons.category;
import androidx.annotation.NonNull;
import org.wikipedia.dataclient.mwapi.MwQueryPage;
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
import org.wikipedia.dataclient.mwapi.MwQueryResult;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
import io.reactivex.Observable;
import timber.log.Timber;
/**
* Category Client to handle custom calls to Commons MediaWiki APIs
*/
@Singleton
public class CategoryClient {
public static final String CATEGORY_PREFIX = "Category:";
private final CategoryInterface CategoryInterface;
@Inject
public CategoryClient(CategoryInterface CategoryInterface) {
this.CategoryInterface = CategoryInterface;
}
/**
* Searches for categories containing the specified string.
*
* @param filter The string to be searched
* @param itemLimit How many results are returned
* @param offset Starts returning items from the nth result. If offset is 9, the response starts with the 9th item of the search result
* @return
*/
public Observable<String> searchCategories(String filter, int itemLimit, int offset) {
return responseToCategoryName(CategoryInterface.searchCategories(filter, itemLimit, offset));
}
/**
* Searches for categories containing the specified string.
*
* @param filter The string to be searched
* @param itemLimit How many results are returned
* @return
*/
public Observable<String> searchCategories(String filter, int itemLimit) {
return searchCategories(filter, itemLimit, 0);
}
/**
* Searches for categories starting with the specified string.
*
* @param prefix The prefix to be searched
* @param itemLimit How many results are returned
* @param offset Starts returning items from the nth result. If offset is 9, the response starts with the 9th item of the search result
* @return
*/
public Observable<String> searchCategoriesForPrefix(String prefix, int itemLimit, int offset) {
return responseToCategoryName(CategoryInterface.searchCategoriesForPrefix(prefix, itemLimit, offset));
}
/**
* Searches for categories starting with the specified string.
*
* @param prefix The prefix to be searched
* @param itemLimit How many results are returned
* @return
*/
public Observable<String> searchCategoriesForPrefix(String prefix, int itemLimit) {
return searchCategoriesForPrefix(prefix, itemLimit, 0);
}
/**
* 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.
*
* @param categoryName Category name as defined on commons
* @return Observable emitting the categories returned. If our search yielded "Category:Test", "Test" is emitted.
*/
public Observable<String> getSubCategoryList(String categoryName) {
return responseToCategoryName(CategoryInterface.getSubCategoryList(categoryName));
}
/**
* The method takes categoryName as input and returns a List of parent categories
* It uses the generator query API to get the parent categories of a category, 500 at a time.
*
* @param categoryName Category name as defined on commons
* @return
*/
@NonNull
public Observable<String> getParentCategoryList(String categoryName) {
return responseToCategoryName(CategoryInterface.getParentCategoryList(categoryName));
}
/**
* Internal function to reduce code reuse. Extracts the categories returned from MwQueryResponse.
*
* @param responseObservable The query response observable
* @return Observable emitting the categories returned. If our search yielded "Category:Test", "Test" is emitted.
*/
private Observable<String> responseToCategoryName(Observable<MwQueryResponse> responseObservable) {
return responseObservable
.flatMap(mwQueryResponse -> {
MwQueryResult query;
List<MwQueryPage> pages;
if ((query = mwQueryResponse.query()) == null ||
(pages = query.pages()) == null) {
Timber.d("No categories returned.");
return Observable.empty();
} else
return Observable.fromIterable(pages);
})
.map(MwQueryPage::title)
.doOnEach(s -> Timber.d("Category returned: %s", s))
.map(cat -> cat.replace(CATEGORY_PREFIX, ""));
}
}

View file

@ -0,0 +1,80 @@
package fr.free.nrw.commons.category
import io.reactivex.Observable
import org.wikipedia.dataclient.mwapi.MwQueryResponse
import javax.inject.Inject
import javax.inject.Singleton
const val CATEGORY_PREFIX = "Category:"
/**
* Category Client to handle custom calls to Commons MediaWiki APIs
*/
@Singleton
class CategoryClient @Inject constructor(private val categoryInterface: CategoryInterface) {
/**
* Searches for categories containing the specified string.
*
* @param filter The string to be searched
* @param itemLimit How many results are returned
* @param offset Starts returning items from the nth result. If offset is 9, the response starts with the 9th item of the search result
* @return
*/
@JvmOverloads
fun searchCategories(filter: String?, itemLimit: Int, offset: Int = 0):
Observable<List<String>> {
return responseToCategoryName(categoryInterface.searchCategories(filter, itemLimit, offset))
}
/**
* Searches for categories starting with the specified string.
*
* @param prefix The prefix to be searched
* @param itemLimit How many results are returned
* @param offset Starts returning items from the nth result. If offset is 9, the response starts with the 9th item of the search result
* @return
*/
@JvmOverloads
fun searchCategoriesForPrefix(prefix: String?, itemLimit: Int, offset: Int = 0):
Observable<List<String>> {
return responseToCategoryName(
categoryInterface.searchCategoriesForPrefix(prefix, itemLimit, offset)
)
}
/**
* 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.
*
* @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?): Observable<List<String>> {
return responseToCategoryName(categoryInterface.getSubCategoryList(categoryName))
}
/**
* The method takes categoryName as input and returns a List of parent categories
* It uses the generator query API to get the parent categories of a category, 500 at a time.
*
* @param categoryName Category name as defined on commons
* @return
*/
fun getParentCategoryList(categoryName: String?): Observable<List<String>> {
return responseToCategoryName(categoryInterface.getParentCategoryList(categoryName))
}
/**
* Internal function to reduce code reuse. Extracts the categories returned from MwQueryResponse.
*
* @param responseObservable The query response observable
* @return Observable emitting the categories returned. If our search yielded "Category:Test", "Test" is emitted.
*/
private fun responseToCategoryName(responseObservable: Observable<MwQueryResponse>): Observable<List<String>> {
return responseObservable
.map { it.query()?.pages() ?: emptyList() }
.map {
it.map { page -> page.title().replace(CATEGORY_PREFIX, "") }
}
}
}

View file

@ -1,79 +0,0 @@
package fr.free.nrw.commons.category;
import android.os.Parcel;
import android.os.Parcelable;
public class CategoryItem implements Parcelable {
private final String name;
private boolean selected;
public static Creator<CategoryItem> CREATOR = new Creator<CategoryItem>() {
@Override
public CategoryItem createFromParcel(Parcel parcel) {
return new CategoryItem(parcel);
}
@Override
public CategoryItem[] newArray(int i) {
return new CategoryItem[0];
}
};
public CategoryItem(String name, boolean selected) {
this.name = name;
this.selected = selected;
}
private CategoryItem(Parcel in) {
name = in.readString();
selected = in.readInt() == 1;
}
public String getName() {
return name;
}
public boolean isSelected() {
return selected;
}
public void setSelected(boolean selected) {
this.selected = selected;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel parcel, int flags) {
parcel.writeString(name);
parcel.writeInt(selected ? 1 : 0);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
CategoryItem that = (CategoryItem) o;
return name.equals(that.name);
}
@Override
public int hashCode() {
return name.hashCode();
}
@Override
public String toString() {
return "CategoryItem: '" + name + '\'';
}
}

View file

@ -0,0 +1,27 @@
package fr.free.nrw.commons.category
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
@Parcelize
data class CategoryItem(val name: String, var isSelected: Boolean) : Parcelable {
override fun toString(): String {
return "CategoryItem: '$name'"
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as CategoryItem
if (name != other.name) return false
return true
}
override fun hashCode(): Int {
return name.hashCode()
}
}

View file

@ -89,14 +89,12 @@ public class SubCategoryListFragment extends CommonsDaggerSupportFragment {
CategoryClient.CATEGORY_PREFIX +categoryName) CategoryClient.CATEGORY_PREFIX +categoryName)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.collect(ArrayList<String>::new, ArrayList::add)
.subscribe(this::handleSuccess, this::handleError)); .subscribe(this::handleSuccess, this::handleError));
} else { } else {
compositeDisposable.add(categoryClient.getSubCategoryList( compositeDisposable.add(categoryClient.getSubCategoryList(
CategoryClient.CATEGORY_PREFIX +categoryName) CategoryClient.CATEGORY_PREFIX +categoryName)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.collect(ArrayList<String>::new, ArrayList::add)
.subscribe(this::handleSuccess, this::handleError)); .subscribe(this::handleSuccess, this::handleError));
} }
} }

View file

@ -129,7 +129,6 @@ public class SearchCategoryFragment extends CommonsDaggerSupportFragment {
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe(disposable -> saveQuery(query)) .doOnSubscribe(disposable -> saveQuery(query))
.collect(ArrayList<String>::new, ArrayList::add)
.subscribe(this::handleSuccess, this::handleError)); .subscribe(this::handleSuccess, this::handleError));
} }
@ -148,7 +147,6 @@ public class SearchCategoryFragment extends CommonsDaggerSupportFragment {
compositeDisposable.add(categoryClient.searchCategories(query,25, queryList.size()) compositeDisposable.add(categoryClient.searchCategories(query,25, queryList.size())
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.collect(ArrayList<String>::new, ArrayList::add)
.subscribe(this::handlePaginationSuccess, this::handleError)); .subscribe(this::handlePaginationSuccess, this::handleError));
} }

View file

@ -151,6 +151,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
MapView mapView; MapView mapView;
@BindView(R.id.rv_nearby_list) @BindView(R.id.rv_nearby_list)
RecyclerView rvNearbyList; RecyclerView rvNearbyList;
@BindView(R.id.no_results_message) TextView noResultsView;
@Inject LocationServiceManager locationManager; @Inject LocationServiceManager locationManager;
@Inject NearbyController nearbyController; @Inject NearbyController nearbyController;
@ -633,13 +634,13 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
@Override @Override
public void updateListFragment(final List<Place> placeList) { public void updateListFragment(final List<Place> placeList) {
adapter.setItems(placeList); adapter.setItems(placeList);
noResultsView.setVisibility(placeList.isEmpty() ? View.VISIBLE : View.GONE);
} }
public void clearNearbyList() { public void clearNearbyList() {
adapter.clear(); adapter.clear();
} }
public void addPlaceToNearbyList(final Place place) { public void addPlaceToNearbyList(final Place place) {
adapter.add(place); adapter.add(place);
} }

View file

@ -18,7 +18,6 @@ import io.reactivex.Flowable;
import io.reactivex.Observable; import io.reactivex.Observable;
import io.reactivex.Single; import io.reactivex.Single;
import java.io.IOException; import java.io.IOException;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import javax.inject.Inject; import javax.inject.Inject;
@ -111,20 +110,10 @@ public class UploadRepository {
* @param imageTitleList * @param imageTitleList
* @return * @return
*/ */
public Observable<CategoryItem> searchAll(String query, List<String> imageTitleList) { public Observable<List<CategoryItem>> searchAll(String query, List<String> imageTitleList) {
return categoriesModel.searchAll(query, imageTitleList); return categoriesModel.searchAll(query, imageTitleList);
} }
/**
* returns the string list of categories
*
* @return
*/
public List<String> getCategoryStringList() {
return categoriesModel.getCategoryStringList();
}
/** /**
* sets the list of selected categories for the current upload * sets the list of selected categories for the current upload
* *
@ -143,16 +132,6 @@ public class UploadRepository {
categoriesModel.onCategoryItemClicked(categoryItem); categoriesModel.onCategoryItemClicked(categoryItem);
} }
/**
* returns category sorted based on similarity with query
*
* @param query
* @return
*/
public Comparator<? super CategoryItem> sortBySimilarity(String query) {
return categoriesModel.sortBySimilarity(query);
}
/** /**
* prunes the category list for irrelevant categories see #750 * prunes the category list for irrelevant categories see #750
* *

View file

@ -180,7 +180,7 @@ class FileProcessor @Inject constructor(
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(Schedulers.io()) .observeOn(Schedulers.io())
.subscribe( .subscribe(
{ gpsCategoryModel.categoryList = it }, gpsCategoryModel::setCategoriesFromLocation,
{ {
Timber.e(it) Timber.e(it)
gpsCategoryModel.clear() gpsCategoryModel.clear()

View file

@ -1,36 +0,0 @@
package fr.free.nrw.commons.upload;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Singleton;
@Singleton
public class GpsCategoryModel {
private Set<String> categorySet;
@Inject
public GpsCategoryModel() {
clear();
}
public void clear() {
categorySet = new HashSet<>();
}
public boolean getGpsCatExists() {
return !categorySet.isEmpty();
}
public List<String> getCategoryList() {
return new ArrayList<>(categorySet);
}
public void setCategoryList(List<String> categoryList) {
clear();
categorySet.addAll(categoryList != null ? categoryList : new ArrayList<>());
}
}

View file

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

View file

@ -30,7 +30,6 @@ import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.LoginActivity; import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.category.CategoriesModel;
import fr.free.nrw.commons.contributions.ContributionController; import fr.free.nrw.commons.contributions.ContributionController;
import fr.free.nrw.commons.filepicker.UploadableFile; import fr.free.nrw.commons.filepicker.UploadableFile;
import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.kvstore.JsonKvStore;
@ -63,8 +62,6 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
@Inject @Inject
UploadContract.UserActionListener presenter; UploadContract.UserActionListener presenter;
@Inject @Inject
CategoriesModel categoriesModel;
@Inject
SessionManager sessionManager; SessionManager sessionManager;
@Inject @Inject
UserClient userClient; UserClient userClient;
@ -96,7 +93,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
private CompositeDisposable compositeDisposable; private CompositeDisposable compositeDisposable;
private ProgressDialog progressDialog; private ProgressDialog progressDialog;
private UploadImageAdapter uploadImagesAdapter; private UploadImageAdapter uploadImagesAdapter;
private List<Fragment> fragments; private List<UploadBaseFragment> fragments;
private UploadCategoriesFragment uploadCategoriesFragment; private UploadCategoriesFragment uploadCategoriesFragment;
private DepictsFragment depictsFragment; private DepictsFragment depictsFragment;
private MediaLicenseFragment mediaLicenseFragment; private MediaLicenseFragment mediaLicenseFragment;
@ -416,6 +413,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
public void onNextButtonClicked(int index) { public void onNextButtonClicked(int index) {
if (index < fragments.size() - 1) { if (index < fragments.size() - 1) {
vpUpload.setCurrentItem(index + 1, false); vpUpload.setCurrentItem(index + 1, false);
fragments.get(index + 1).onBecameVisible();
} else { } else {
presenter.handleSubmit(); presenter.handleSubmit();
} }
@ -425,6 +423,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
public void onPreviousButtonClicked(int index) { public void onPreviousButtonClicked(int index) {
if (index != 0) { if (index != 0) {
vpUpload.setCurrentItem(index - 1, true); vpUpload.setCurrentItem(index - 1, true);
fragments.get(index - 1).onBecameVisible();
} }
} }
@ -433,14 +432,14 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
*/ */
private class UploadImageAdapter extends FragmentStatePagerAdapter { private class UploadImageAdapter extends FragmentStatePagerAdapter {
List<Fragment> fragments; List<UploadBaseFragment> fragments;
public UploadImageAdapter(FragmentManager fragmentManager) { public UploadImageAdapter(FragmentManager fragmentManager) {
super(fragmentManager); super(fragmentManager);
this.fragments = new ArrayList<>(); this.fragments = new ArrayList<>();
} }
public void setFragments(List<Fragment> fragments) { public void setFragments(List<UploadBaseFragment> fragments) {
this.fragments = fragments; this.fragments = fragments;
notifyDataSetChanged(); notifyDataSetChanged();
} }

View file

@ -22,6 +22,9 @@ public class UploadBaseFragment extends CommonsDaggerSupportFragment {
this.callback = callback; this.callback = callback;
} }
protected void onBecameVisible() {
}
public interface Callback { public interface Callback {
void onNextButtonClicked(int index); void onNextButtonClicked(int index);

View file

@ -22,8 +22,8 @@ public abstract class UploadModule {
presenter); presenter);
@Binds @Binds
public abstract CategoriesContract.UserActionListener bindsCategoriesPresenter(CategoriesPresenter public abstract CategoriesContract.UserActionListener bindsCategoriesPresenter(
presenter); CategoriesPresenter presenter);
@Binds @Binds
public abstract MediaLicenseContract.UserActionListener bindsMediaLicensePresenter( public abstract MediaLicenseContract.UserActionListener bindsMediaLicensePresenter(

View file

@ -1,146 +0,0 @@
package fr.free.nrw.commons.upload.categories;
import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD;
import static fr.free.nrw.commons.di.CommonsApplicationModule.MAIN_THREAD;
import android.text.TextUtils;
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.UploadItem;
import io.reactivex.Observable;
import io.reactivex.Scheduler;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import timber.log.Timber;
/**
* The presenter class for UploadCategoriesFragment
*/
@Singleton
public class CategoriesPresenter implements CategoriesContract.UserActionListener {
private static final CategoriesContract.View DUMMY = (CategoriesContract.View) Proxy
.newProxyInstance(
CategoriesContract.View.class.getClassLoader(),
new Class[]{CategoriesContract.View.class},
(proxy, method, methodArgs) -> null);
private final Scheduler ioScheduler;
private final Scheduler mainThreadScheduler;
CategoriesContract.View view = DUMMY;
private UploadRepository repository;
private CompositeDisposable compositeDisposable;
@Inject
public CategoriesPresenter(UploadRepository repository, @Named(IO_THREAD) Scheduler ioScheduler,
@Named(MAIN_THREAD) Scheduler mainThreadScheduler) {
this.repository = repository;
this.ioScheduler = ioScheduler;
this.mainThreadScheduler = mainThreadScheduler;
compositeDisposable = new CompositeDisposable();
}
@Override
public void onAttachView(CategoriesContract.View view) {
this.view = view;
}
@Override
public void onDetachView() {
this.view = DUMMY;
compositeDisposable.clear();
}
/**
* asks the repository to fetch categories for the query
* @param query
*
*/
@Override
public void searchForCategories(String query) {
List<CategoryItem> categoryItems = new ArrayList<>();
List<String> imageTitleList = getImageTitleList();
Observable<CategoryItem> distinctCategoriesObservable = Observable
.fromIterable(repository.getSelectedCategories())
.subscribeOn(ioScheduler)
.observeOn(mainThreadScheduler)
.doOnSubscribe(disposable -> {
view.showProgress(true);
view.showError(null);
view.setCategories(null);
})
.observeOn(ioScheduler)
.concatWith(
repository.searchAll(query, imageTitleList)
)
.filter(categoryItem -> !repository.containsYear(categoryItem.getName()))
.distinct();
if(!TextUtils.isEmpty(query)) {
distinctCategoriesObservable=distinctCategoriesObservable.sorted(repository.sortBySimilarity(query));
}
Disposable searchCategoriesDisposable = distinctCategoriesObservable
.observeOn(mainThreadScheduler)
.subscribe(
s -> categoryItems.add(s),
Timber::e,
() -> {
view.setCategories(categoryItems);
view.showProgress(false);
if (categoryItems.isEmpty()) {
view.showError(R.string.no_categories_found);
}
}
);
compositeDisposable.add(searchCategoriesDisposable);
}
/**
* Returns image title list from UploadItem
* @return
*/
private List<String> getImageTitleList() {
List<String> titleList = new ArrayList<>();
for (UploadItem item : repository.getUploads()) {
final String captionText = item.getUploadMediaDetails().get(0).getCaptionText();
if (!TextUtils.isEmpty(captionText)) {
titleList.add(captionText);
}
}
return titleList;
}
/**
* Verifies the number of categories selected, prompts the user if none selected
*/
@Override
public void verifyCategories() {
List<CategoryItem> selectedCategories = repository.getSelectedCategories();
if (selectedCategories != null && !selectedCategories.isEmpty()) {
repository.setSelectedCategories(repository.getCategoryStringList());
view.goToNextScreen();
} else {
view.showNoCategorySelected();
}
}
/**
* ask repository to handle category clicked
*
* @param categoryItem
*/
@Override
public void onCategoryItemClicked(CategoryItem categoryItem) {
repository.onCategoryClicked(categoryItem);
}
}

View file

@ -0,0 +1,111 @@
package fr.free.nrw.commons.upload.categories
import android.text.TextUtils
import fr.free.nrw.commons.R
import fr.free.nrw.commons.category.CategoryItem
import fr.free.nrw.commons.di.CommonsApplicationModule
import fr.free.nrw.commons.repository.UploadRepository
import fr.free.nrw.commons.upload.depicts.proxy
import io.reactivex.Scheduler
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.subjects.PublishSubject
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Named
import javax.inject.Singleton
/**
* The presenter class for UploadCategoriesFragment
*/
@Singleton
class CategoriesPresenter @Inject constructor(
private val repository: UploadRepository,
@param:Named(CommonsApplicationModule.IO_THREAD) private val ioScheduler: Scheduler,
@param:Named(CommonsApplicationModule.MAIN_THREAD) private val mainThreadScheduler: Scheduler
) : CategoriesContract.UserActionListener {
companion object {
private val DUMMY: CategoriesContract.View = proxy()
}
var view = DUMMY
private val compositeDisposable = CompositeDisposable()
private val searchTerms = PublishSubject.create<String>()
override fun onAttachView(view: CategoriesContract.View) {
this.view = view
compositeDisposable.add(
searchTerms
.observeOn(mainThreadScheduler)
.doOnNext {
view.showProgress(true)
view.showError(null)
view.setCategories(null)
}
.switchMap(::searchResults)
.map { repository.selectedCategories + it }
.map { it.distinctBy { categoryItem -> categoryItem.name } }
.observeOn(mainThreadScheduler)
.subscribe(
{
view.setCategories(it)
view.showProgress(false)
if (it.isEmpty()) {
view.showError(R.string.no_categories_found)
}
},
Timber::e
)
)
}
private fun searchResults(term: String) =
repository.searchAll(term, getImageTitleList())
.subscribeOn(ioScheduler)
.map { it.filterNot { categoryItem -> repository.containsYear(categoryItem.name) } }
override fun onDetachView() {
view = DUMMY
compositeDisposable.clear()
}
/**
* asks the repository to fetch categories for the query
* @param query
*/
override fun searchForCategories(query: String) {
searchTerms.onNext(query)
}
/**
* Returns image title list from UploadItem
* @return
*/
private fun getImageTitleList(): List<String> {
return repository.uploads
.map { it.uploadMediaDetails[0].captionText }
.filterNot { TextUtils.isEmpty(it) }
}
/**
* Verifies the number of categories selected, prompts the user if none selected
*/
override fun verifyCategories() {
val selectedCategories = repository.selectedCategories
if (selectedCategories.isNotEmpty()) {
repository.setSelectedCategories(selectedCategories.map { it.name })
view.goToNextScreen()
} else {
view.showNoCategorySelected()
}
}
/**
* ask repository to handle category clicked
*
* @param categoryItem
*/
override fun onCategoryItemClicked(categoryItem: CategoryItem) {
repository.onCategoryClicked(categoryItem)
}
}

View file

@ -1,6 +1,7 @@
package fr.free.nrw.commons.upload.categories; package fr.free.nrw.commons.upload.categories;
import android.os.Bundle; import android.os.Bundle;
import android.text.Editable;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -46,8 +47,6 @@ public class UploadCategoriesFragment extends UploadBaseFragment implements Cate
CategoriesContract.UserActionListener presenter; CategoriesContract.UserActionListener presenter;
private UploadCategoryAdapter adapter; private UploadCategoryAdapter adapter;
private Disposable subscribe; private Disposable subscribe;
private List<CategoryItem> categories;
private boolean isVisible;
@Nullable @Nullable
@Override @Override
@ -69,15 +68,6 @@ public class UploadCategoriesFragment extends UploadBaseFragment implements Cate
presenter.onAttachView(this); presenter.onAttachView(this);
initRecyclerView(); initRecyclerView();
addTextChangeListenerToEtSearch(); addTextChangeListenerToEtSearch();
//get default categories for empty query
}
@Override
public void onResume() {
super.onResume();
if (presenter != null && isVisible && (categories == null || categories.isEmpty())) {
presenter.searchForCategories(null);
}
} }
private void addTextChangeListenerToEtSearch() { private void addTextChangeListenerToEtSearch() {
@ -130,7 +120,6 @@ public class UploadCategoriesFragment extends UploadBaseFragment implements Cate
adapter.clear(); adapter.clear();
} }
else{ else{
this.categories = categories;
adapter.setItems(categories); adapter.setItems(categories);
} }
} }
@ -163,12 +152,11 @@ public class UploadCategoriesFragment extends UploadBaseFragment implements Cate
} }
@Override @Override
public void setUserVisibleHint(boolean isVisibleToUser) { protected void onBecameVisible() {
super.setUserVisibleHint(isVisibleToUser); super.onBecameVisible();
isVisible = isVisibleToUser; final Editable text = etSearch.getText();
if (text != null) {
if (presenter != null && isResumed() && (categories == null || categories.isEmpty())) { presenter.searchForCategories(text.toString());
presenter.searchForCategories(null);
} }
} }
} }

View file

@ -3,14 +3,29 @@
android:id="@+id/bottom_sheet" android:id="@+id/bottom_sheet"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical" android:gravity="bottom"
app:behavior_hideable="true" app:behavior_hideable="true"
android:visibility="visible" android:visibility="visible"
app:layout_behavior="@string/bottom_sheet_behavior" app:layout_behavior="@string/bottom_sheet_behavior"
android:background="@android:color/white"> android:background="@android:color/transparent">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/white">
<TextView
android:id="@+id/no_results_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="50dp"
android:layout_centerInParent="true"
android:visibility="gone"
android:text="@string/nearby_no_results"/>
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_nearby_list" android:id="@+id/rv_nearby_list"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="wrap_content"/>
</RelativeLayout>
</RelativeLayout> </RelativeLayout>

View file

@ -49,7 +49,7 @@
<string name="provider_contributions">Моите качвания</string> <string name="provider_contributions">Моите качвания</string>
<string name="menu_share">Споделяне</string> <string name="menu_share">Споделяне</string>
<string name="menu_open_in_browser">Преглед в браузъра</string> <string name="menu_open_in_browser">Преглед в браузъра</string>
<string name="share_title_hint" fuzzy="true">Заглавие (задълж.)</string> <string name="share_title_hint">Описание (задълж.)</string>
<string name="share_description_hint">Описание</string> <string name="share_description_hint">Описание</string>
<string name="login_failed_network">Неуспешно влизане проблем в мрежата</string> <string name="login_failed_network">Неуспешно влизане проблем в мрежата</string>
<string name="login_failed_wrong_credentials">Неуспешно влизане. Проверете потребителското Ви име и паролата.</string> <string name="login_failed_wrong_credentials">Неуспешно влизане. Проверете потребителското Ви име и паролата.</string>
@ -96,7 +96,7 @@
</plurals> </plurals>
<string name="menu_download">Изтегляне</string> <string name="menu_download">Изтегляне</string>
<string name="preference_license">Лиценз по подразбиране</string> <string name="preference_license">Лиценз по подразбиране</string>
<string name="preference_theme" fuzzy="true">Нощен режим</string> <string name="preference_theme">Облик</string>
<string name="license_name_cc_by_sa_four">Признание-Споделяне на Споделеното 4.0</string> <string name="license_name_cc_by_sa_four">Признание-Споделяне на Споделеното 4.0</string>
<string name="license_name_cc_by_sa">Признание-Споделяне на Споделеното 3.0</string> <string name="license_name_cc_by_sa">Признание-Споделяне на Споделеното 3.0</string>
<string name="license_name_cc0">CC0</string> <string name="license_name_cc0">CC0</string>
@ -188,7 +188,7 @@
<string name="use_external_storage_summary">Съхраняване на направените картини в приложението с камерата на устройството Ви</string> <string name="use_external_storage_summary">Съхраняване на направените картини в приложението с камерата на устройството Ви</string>
<string name="nominate_deletion">Номиниране за изтриване</string> <string name="nominate_deletion">Номиниране за изтриване</string>
<string name="nominated_for_deletion">Изображението е предложено за изтриване.</string> <string name="nominated_for_deletion">Изображението е предложено за изтриване.</string>
<string name="nominated_see_more" fuzzy="true">&lt;u&gt;По-подробно ще намерите на уеб страницата&lt;/u&gt;</string> <string name="nominated_see_more">По-подробно ще намерите на уеб страницата</string>
<string name="skip_login">Пропускане</string> <string name="skip_login">Пропускане</string>
<string name="navigation_item_login">Влизане</string> <string name="navigation_item_login">Влизане</string>
<string name="skip_login_message">Ще трябва да влезете, за да качвате картини в бъдеще.</string> <string name="skip_login_message">Ще трябва да влезете, за да качвате картини в бъдеще.</string>
@ -196,7 +196,7 @@
<string name="copy_wikicode">Копиране на уикитекста в кеша</string> <string name="copy_wikicode">Копиране на уикитекста в кеша</string>
<string name="wikicode_copied">Уикитекстът е копиран в кеша</string> <string name="wikicode_copied">Уикитекстът е копиран в кеша</string>
<string name="nearby_location_has_not_changed">Местоположението не е променено.</string> <string name="nearby_location_has_not_changed">Местоположението не е променено.</string>
<string name="nearby_location_not_available" fuzzy="true">Местоположението не е налично.</string> <string name="nearby_location_not_available">„Близки места“ може да не работи правилно, тъй като местоположението не е налично.</string>
<string name="toggle_view_button">Превключване на изгледа</string> <string name="toggle_view_button">Превключване на изгледа</string>
<string name="nearby_wikidata">Уикиданни</string> <string name="nearby_wikidata">Уикиданни</string>
<string name="nearby_wikipedia">Уикипедия</string> <string name="nearby_wikipedia">Уикипедия</string>
@ -213,7 +213,7 @@
<string name="preference_author_name_toggle">Използване на персонализирано авторско име</string> <string name="preference_author_name_toggle">Използване на персонализирано авторско име</string>
<string name="preference_author_name_toggle_summary">При качването използвайте персонализирано авторско име вместо потребителското си име</string> <string name="preference_author_name_toggle_summary">При качването използвайте персонализирано авторско име вместо потребителското си име</string>
<string name="preference_author_name">Персонализирано авторско име</string> <string name="preference_author_name">Персонализирано авторско име</string>
<string name="read_notifications" fuzzy="true">Известия (архивирани)</string> <string name="read_notifications">Известия (прочетени)</string>
<string name="list_sheet">Списък</string> <string name="list_sheet">Списък</string>
<string name="next">Следваща</string> <string name="next">Следваща</string>
<string name="submit">Изпращане</string> <string name="submit">Изпращане</string>
@ -243,9 +243,9 @@
<string name="no_image_reverted">Няма върнати изображения</string> <string name="no_image_reverted">Няма върнати изображения</string>
<string name="no_image_uploaded">Няма качени изображения</string> <string name="no_image_uploaded">Няма качени изображения</string>
<string name="no_notification">Нямате непрочетени известия</string> <string name="no_notification">Нямате непрочетени известия</string>
<string name="no_read_notification" fuzzy="true">Нямате архивирани известия</string> <string name="no_read_notification">Нямате прочетени известия</string>
<string name="share_logs_using">Споделяне на дневници, използвайки</string> <string name="share_logs_using">Споделяне на дневници, използвайки</string>
<string name="menu_option_read" fuzzy="true">Преглеждане на архивирани</string> <string name="menu_option_read">Преглеждане на прочетени</string>
<string name="menu_option_unread">Преглеждане на непрочетени</string> <string name="menu_option_unread">Преглеждане на непрочетени</string>
<string name="error_occurred_in_picking_images">Възникна грешка при избирането на изображенията</string> <string name="error_occurred_in_picking_images">Възникна грешка при избирането на изображенията</string>
<string name="image_chooser_title">Изберете изображения за качване</string> <string name="image_chooser_title">Изберете изображения за качване</string>

View file

@ -12,6 +12,7 @@
* Nuevo Paso * Nuevo Paso
* Revi * Revi
* Ykhwong * Ykhwong
* 그냥기여자
* 아라 * 아라
--> -->
<resources> <resources>
@ -62,6 +63,7 @@
<string name="menu_share">공유</string> <string name="menu_share">공유</string>
<string name="menu_open_in_browser">브라우저로 보기</string> <string name="menu_open_in_browser">브라우저로 보기</string>
<string name="share_title_hint">캡션 (필수)</string> <string name="share_title_hint">캡션 (필수)</string>
<string name="add_caption_toast">이 파일의 설명을 작성해 주십시오</string>
<string name="share_description_hint">설명</string> <string name="share_description_hint">설명</string>
<string name="share_caption_hint">캡션 (255자 제한)</string> <string name="share_caption_hint">캡션 (255자 제한)</string>
<string name="login_failed_network">로그인할 수 없습니다 - 네트워크 오류입니다</string> <string name="login_failed_network">로그인할 수 없습니다 - 네트워크 오류입니다</string>
@ -95,6 +97,7 @@
<item quantity="one">%1$d개 업로드</item> <item quantity="one">%1$d개 업로드</item>
</plurals> </plurals>
<string name="categories_not_found">%1$s와(과) 일치하는 분류를 찾을 수 없습니다</string> <string name="categories_not_found">%1$s와(과) 일치하는 분류를 찾을 수 없습니다</string>
<string name="depictions_not_found">%1$s에 대한 위키데이터 검색 결과가 없습니다</string>
<string name="categories_skip_explanation">위키미디어 공용에서 그림을 더 찾기 쉽게 만들기 위해 분류를 추가합니다.\n분류를 추가하려면 입력을 시작하세요.</string> <string name="categories_skip_explanation">위키미디어 공용에서 그림을 더 찾기 쉽게 만들기 위해 분류를 추가합니다.\n분류를 추가하려면 입력을 시작하세요.</string>
<string name="categories_activity_title">분류</string> <string name="categories_activity_title">분류</string>
<string name="title_activity_settings">설정</string> <string name="title_activity_settings">설정</string>
@ -169,6 +172,7 @@
<string name="detail_panel_cats_label">분류</string> <string name="detail_panel_cats_label">분류</string>
<string name="detail_panel_cats_loading">불러오는 중…</string> <string name="detail_panel_cats_loading">불러오는 중…</string>
<string name="detail_panel_cats_none">선택하지 않음</string> <string name="detail_panel_cats_none">선택하지 않음</string>
<string name="detail_caption_empty">설명 없음</string>
<string name="detail_description_empty">설명 없음</string> <string name="detail_description_empty">설명 없음</string>
<string name="detail_discussion_empty">토론 없음</string> <string name="detail_discussion_empty">토론 없음</string>
<string name="detail_license_empty">알 수 없는 라이선스</string> <string name="detail_license_empty">알 수 없는 라이선스</string>
@ -187,6 +191,7 @@
<string name="upload">업로드</string> <string name="upload">업로드</string>
<string name="yes"></string> <string name="yes"></string>
<string name="no">아니오</string> <string name="no">아니오</string>
<string name="media_detail_caption">설명</string>
<string name="media_detail_title">제목</string> <string name="media_detail_title">제목</string>
<string name="media_detail_description">설명</string> <string name="media_detail_description">설명</string>
<string name="media_detail_discussion">토론</string> <string name="media_detail_discussion">토론</string>
@ -310,6 +315,7 @@
<string name="no_images_found">그림이 없습니다!</string> <string name="no_images_found">그림이 없습니다!</string>
<string name="error_loading_images">그림을 불러오는 동안 오류가 발생했습니다.</string> <string name="error_loading_images">그림을 불러오는 동안 오류가 발생했습니다.</string>
<string name="image_uploaded_by">올린이: %1$s</string> <string name="image_uploaded_by">올린이: %1$s</string>
<string name="block_notification_title">차단됨</string>
<string name="block_notification">공용 편집이 차단되어 있습니다</string> <string name="block_notification">공용 편집이 차단되어 있습니다</string>
<string name="appwidget_img">오늘의 이미지</string> <string name="appwidget_img">오늘의 이미지</string>
<string name="app_widget_heading">오늘의 이미지</string> <string name="app_widget_heading">오늘의 이미지</string>
@ -361,6 +367,7 @@
<string name="delete">삭제</string> <string name="delete">삭제</string>
<string name="Achievements">성과</string> <string name="Achievements">성과</string>
<string name="statistics">통계</string> <string name="statistics">통계</string>
<string name="statistics_thanks">받은 감사</string>
<string name="statistics_featured">알찬 그림</string> <string name="statistics_featured">알찬 그림</string>
<string name="statistics_wikidata_edits">\"주변 장소\" 경유 이미지</string> <string name="statistics_wikidata_edits">\"주변 장소\" 경유 이미지</string>
<string name="level">레벨</string> <string name="level">레벨</string>
@ -426,6 +433,8 @@
<string name="unable_to_display_nearest_place">위치 권한 없이 사진이 필요한 주변 장소를 표시할 수 없습니다</string> <string name="unable_to_display_nearest_place">위치 권한 없이 사진이 필요한 주변 장소를 표시할 수 없습니다</string>
<string name="never_ask_again">다시는 묻지 않음</string> <string name="never_ask_again">다시는 묻지 않음</string>
<string name="display_location_permission_title">위치 권한 표시</string> <string name="display_location_permission_title">위치 권한 표시</string>
<string name="display_campaigns">캠페인 표시</string>
<string name="display_campaigns_explanation">진행되고 있는 캠페인 보기</string>
<string name="this_function_needs_network_connection">이 기능에는 네트워크 연결이 필요합니다. 연결 설정을 확인해 주십시오.</string> <string name="this_function_needs_network_connection">이 기능에는 네트워크 연결이 필요합니다. 연결 설정을 확인해 주십시오.</string>
<string name="bad_token_error_proposed_solution">편집 토큰에 문제가 있어서 업로드를 실패했습니다. 로그아웃한 다음 다시 로그인해 보십시오.</string> <string name="bad_token_error_proposed_solution">편집 토큰에 문제가 있어서 업로드를 실패했습니다. 로그아웃한 다음 다시 로그인해 보십시오.</string>
<string name="error_processing_image">이미지를 처리하는 동안 오류가 발생했습니다. 다시 시도해 주십시오!</string> <string name="error_processing_image">이미지를 처리하는 동안 오류가 발생했습니다. 다시 시도해 주십시오!</string>
@ -473,10 +482,12 @@
<string name="delete_helper_show_deletion_title_success">성공</string> <string name="delete_helper_show_deletion_title_success">성공</string>
<string name="delete_helper_show_deletion_title_failed">실패</string> <string name="delete_helper_show_deletion_title_failed">실패</string>
<string name="delete_helper_ask_spam_selfie">셀카</string> <string name="delete_helper_ask_spam_selfie">셀카</string>
<string name="delete_helper_ask_spam_blurry">흐림</string>
<string name="delete_helper_ask_spam_other">기타</string> <string name="delete_helper_ask_spam_other">기타</string>
<string name="delete_helper_ask_reason_copyright_internet_photo">인터넷의 임의 사진</string> <string name="delete_helper_ask_reason_copyright_internet_photo">인터넷의 임의 사진</string>
<string name="delete_helper_ask_reason_copyright_logo">로고</string> <string name="delete_helper_ask_reason_copyright_logo">로고</string>
<string name="delete_helper_ask_reason_copyright_other">기타</string> <string name="delete_helper_ask_reason_copyright_other">기타</string>
<string name="delete_helper_ask_alert_set_positive_button_reason">왜냐하면</string>
<string name="share_image_via">다음을 통해 이미지 공유</string> <string name="share_image_via">다음을 통해 이미지 공유</string>
<string name="no_achievements_yet">아직 기여가 없습니다</string> <string name="no_achievements_yet">아직 기여가 없습니다</string>
<string name="account_created">계정을 만들었습니다!</string> <string name="account_created">계정을 만들었습니다!</string>
@ -490,10 +501,14 @@
<string name="nearby_search_hint">다리, 박물관, 호텔 등.</string> <string name="nearby_search_hint">다리, 박물관, 호텔 등.</string>
<string name="title_for_media">미디어</string> <string name="title_for_media">미디어</string>
<string name="title_app_shortcut_explore">찾아보기</string> <string name="title_app_shortcut_explore">찾아보기</string>
<string name="title_app_shortcut_bookmark">북마크</string>
<string name="title_app_shortcut_setting">설정</string> <string name="title_app_shortcut_setting">설정</string>
<string name="remove_bookmark">북마크에서 제거됨</string>
<string name="add_bookmark">북마크에 추가됨</string>
<string name="wallpaper_set_unsuccessfully">무언가 잘못되었습니다. 배경화면을 설정하지 못했습니다</string> <string name="wallpaper_set_unsuccessfully">무언가 잘못되었습니다. 배경화면을 설정하지 못했습니다</string>
<string name="setting_wallpaper_dialog_title">배경화면으로 설정</string> <string name="setting_wallpaper_dialog_title">배경화면으로 설정</string>
<string name="setting_wallpaper_dialog_message">배경화면을 설정 중입니다. 기다려 주십시오...</string> <string name="setting_wallpaper_dialog_message">배경화면을 설정 중입니다. 기다려 주십시오...</string>
<string name="theme_dark_name">어두움</string> <string name="theme_dark_name">어두움</string>
<string name="theme_light_name">밝음</string> <string name="theme_light_name">밝음</string>
<string name="ask_to_turn_location_on">위치를 켭니까?</string>
</resources> </resources>

View file

@ -64,7 +64,7 @@
<string name="provider_contributions">Carregamentos</string> <string name="provider_contributions">Carregamentos</string>
<string name="menu_share">Partilhar</string> <string name="menu_share">Partilhar</string>
<string name="menu_open_in_browser">Ver no navegador</string> <string name="menu_open_in_browser">Ver no navegador</string>
<string name="share_title_hint">Título (Obrigatório)</string> <string name="share_title_hint">Legenda (obrigatória)</string>
<string name="share_description_hint">Descrição</string> <string name="share_description_hint">Descrição</string>
<string name="login_failed_network">Não é possível iniciar uma sessão - falha de rede</string> <string name="login_failed_network">Não é possível iniciar uma sessão - falha de rede</string>
<string name="login_failed_wrong_credentials">Não é possível iniciar uma sessão - verifique o seu nome de utilizador e a palavra-passe, por favor</string> <string name="login_failed_wrong_credentials">Não é possível iniciar uma sessão - verifique o seu nome de utilizador e a palavra-passe, por favor</string>
@ -334,7 +334,7 @@
<string name="error_loading_subcategories">Ocorreu um erro ao carregar subcategorias.</string> <string name="error_loading_subcategories">Ocorreu um erro ao carregar subcategorias.</string>
<string name="search_tab_title_media">Multimédia</string> <string name="search_tab_title_media">Multimédia</string>
<string name="search_tab_title_categories">Categorias</string> <string name="search_tab_title_categories">Categorias</string>
<string name="search_tab_title_depictions">Itens</string> <string name="search_tab_title_depictions">Elementos</string>
<string name="explore_tab_title_featured">Destacadas</string> <string name="explore_tab_title_featured">Destacadas</string>
<string name="explore_tab_title_mobile">Carregada via telemóvel</string> <string name="explore_tab_title_mobile">Carregada via telemóvel</string>
<string name="successful_wikidata_edit">Imagem adicionada a %1$s na wiki Wikidata!</string> <string name="successful_wikidata_edit">Imagem adicionada a %1$s na wiki Wikidata!</string>
@ -437,7 +437,7 @@
<string name="desc_language_Asia">Ásia</string> <string name="desc_language_Asia">Ásia</string>
<string name="desc_language_Pacific">Pacífico</string> <string name="desc_language_Pacific">Pacífico</string>
<string name="no_categories_selected">Não foi selecionada nenhuma categoria</string> <string name="no_categories_selected">Não foi selecionada nenhuma categoria</string>
<string name="no_categories_selected_warning_desc">As imagens sem categorias são utilizáveis raramente. Tem a certeza que deseja continuar sem selecionar categorias?</string> <string name="no_categories_selected_warning_desc">As imagens sem categorias só raramente são utilizáveis. Tem a certeza de que deseja continuar sem selecionar categorias?</string>
<string name="upload_flow_all_images_in_set">(Para todas as imagens do conjunto)</string> <string name="upload_flow_all_images_in_set">(Para todas as imagens do conjunto)</string>
<string name="search_this_area">Pesquisar nesta área</string> <string name="search_this_area">Pesquisar nesta área</string>
<string name="nearby_card_permission_title">Pedido de permissões</string> <string name="nearby_card_permission_title">Pedido de permissões</string>

View file

@ -587,6 +587,9 @@
<string name="place_type">Тип места:</string> <string name="place_type">Тип места:</string>
<string name="nearby_search_hint">Мост, музей, гостиница и т. д.</string> <string name="nearby_search_hint">Мост, музей, гостиница и т. д.</string>
<string name="you_must_reset_your_passsword">Что-то пошло не так со входом, вы должны сбросить пароль!</string> <string name="you_must_reset_your_passsword">Что-то пошло не так со входом, вы должны сбросить пароль!</string>
<string name="title_for_media">МЕДИА</string>
<string name="title_for_child_classes">ДЕТСКИЕ КЛАССЫ</string>
<string name="title_for_parent_classes">РОДИТЕЛЬСКИЕ КЛАССЫ</string>
<string name="upload_nearby_place_found_title">Место поблизости найдено</string> <string name="upload_nearby_place_found_title">Место поблизости найдено</string>
<string name="upload_nearby_place_found_description">Является ли это фото местом %1$s?</string> <string name="upload_nearby_place_found_description">Является ли это фото местом %1$s?</string>
<string name="title_app_shortcut_explore">Обзор</string> <string name="title_app_shortcut_explore">Обзор</string>

View file

@ -189,7 +189,7 @@
<string name="cannot_be_zero">El limite de cargamento no\'l połe esare 0</string> <string name="cannot_be_zero">El limite de cargamento no\'l połe esare 0</string>
<string name="set_limit">El me limite di cargamento ultimo</string> <string name="set_limit">El me limite di cargamento ultimo</string>
<string name="login_failed_2fa_not_supported">L\'autenticasion a do fatori no ła xé pa\'l momento suportada</string> <string name="login_failed_2fa_not_supported">L\'autenticasion a do fatori no ła xé pa\'l momento suportada</string>
<string name="logout_verification">Vuto realmente ndar fora?</string> <string name="logout_verification">Vuto seriamente sevitar a ndaxer fora?</string>
<string name="commons_logo">Logo de Commons</string> <string name="commons_logo">Logo de Commons</string>
<string name="commons_website">Sito web de Commons</string> <string name="commons_website">Sito web de Commons</string>
<string name="commons_facebook">Pajina Facebook de Commons</string> <string name="commons_facebook">Pajina Facebook de Commons</string>
@ -226,7 +226,7 @@
<string name="nearby_needs_permissions">I posti cuà rente no i pole mia esar mostrai sensa ver autorixà l\'uxo de la poxision</string> <string name="nearby_needs_permissions">I posti cuà rente no i pole mia esar mostrai sensa ver autorixà l\'uxo de la poxision</string>
<string name="no_description_found">Nisuna descrision catada</string> <string name="no_description_found">Nisuna descrision catada</string>
<string name="nearby_info_menu_commons_article">Pajina de Commons del file</string> <string name="nearby_info_menu_commons_article">Pajina de Commons del file</string>
<string name="nearby_info_menu_wikidata_article">Elemento Wikidata</string> <string name="nearby_info_menu_wikidata_article">Ełemento Wikidata</string>
<string name="nearby_info_menu_wikipedia_article">Articulo so Wikipedia</string> <string name="nearby_info_menu_wikipedia_article">Articulo so Wikipedia</string>
<string name="error_while_cache">Eror co se jera drio memorixare le imajini n\'te la cache</string> <string name="error_while_cache">Eror co se jera drio memorixare le imajini n\'te la cache</string>
<string name="title_info">On unego titoło descrivitivo par el file, che el vegnarà doparà come so no,e. Te połi doparar on lenguajo senpliçe co spasi. No sta scrivere l\'estension del file</string> <string name="title_info">On unego titoło descrivitivo par el file, che el vegnarà doparà come so no,e. Te połi doparar on lenguajo senpliçe co spasi. No sta scrivere l\'estension del file</string>
@ -272,7 +272,7 @@
<string name="notifications_talk_page_message">%1$s el te ga dasà on mesajo n\'te ła to pajina de discusion</string> <string name="notifications_talk_page_message">%1$s el te ga dasà on mesajo n\'te ła to pajina de discusion</string>
<string name="notifications_thank_you_edit">Grasie par ver fato na modifega!</string> <string name="notifications_thank_you_edit">Grasie par ver fato na modifega!</string>
<string name="notifications_mention">%1$s el te ga mensionà so %2$s.</string> <string name="notifications_mention">%1$s el te ga mensionà so %2$s.</string>
<string name="toggle_view_button">Ativa/Dixativa vixuałixasion</string> <string name="toggle_view_button">Ativa/Dexativa vixuałixasion</string>
<string name="nearby_directions">Indicasion</string> <string name="nearby_directions">Indicasion</string>
<string name="nearby_wikidata">Wikidata</string> <string name="nearby_wikidata">Wikidata</string>
<string name="nearby_wikipedia">Wikipedia</string> <string name="nearby_wikipedia">Wikipedia</string>

View file

@ -629,5 +629,5 @@ Upload your first media by tapping on the add button.</string>
<string name="nearby_needs_location">Nearby needs location enabled to work properly</string> <string name="nearby_needs_location">Nearby needs location enabled to work properly</string>
<string name="use_location_from_similar_image">Did you shoot these two pictures at the same place? Do you want to use the latitude/longitude of the picture on the right?</string> <string name="use_location_from_similar_image">Did you shoot these two pictures at the same place? Do you want to use the latitude/longitude of the picture on the right?</string>
<string name="load_more">Load More</string> <string name="load_more">Load More</string>
<string name="nearby_no_results">No places found, try changing your search criteria.</string>
</resources> </resources>

View file

@ -2,14 +2,11 @@ package fr.free.nrw.commons.category
import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.whenever import com.nhaarman.mockitokotlin2.whenever
import fr.free.nrw.commons.kvstore.JsonKvStore
import fr.free.nrw.commons.upload.GpsCategoryModel import fr.free.nrw.commons.upload.GpsCategoryModel
import io.reactivex.Observable import io.reactivex.Observable
import junit.framework.Assert.assertEquals import io.reactivex.subjects.BehaviorSubject
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.mockito.ArgumentMatchers.anyString
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock import org.mockito.Mock
import org.mockito.MockitoAnnotations import org.mockito.MockitoAnnotations
@ -31,48 +28,41 @@ class CategoriesModelTest {
// Test Case for verifying that Categories search (MW api calls) are case-insensitive // Test Case for verifying that Categories search (MW api calls) are case-insensitive
@Test @Test
fun searchAllFoundCaseTest() { fun searchAllFoundCaseTest() {
val categoriesModel = CategoriesModel(categoryClient, null, null, mock()) val categoriesModel = CategoriesModel(categoryClient, mock(), mock())
whenever(categoryClient.searchCategoriesForPrefix(anyString(), eq(25))) val expectedList = listOf("Test")
.thenReturn(Observable.just("Test")) whenever(categoryClient.searchCategoriesForPrefix("tes", 25))
.thenReturn(Observable.just(expectedList))
// Checking if both return "Test" // Checking if both return "Test"
val actualCategoryName = categoriesModel.searchAll("tes", null).blockingFirst() val expectedItems = expectedList.map { CategoryItem(it, false) }
assertEquals("Test", actualCategoryName.name) categoriesModel.searchAll("tes", emptyList())
.test()
.assertValues(expectedItems)
val actualCategoryNameCaps = categoriesModel.searchAll("Tes", null).blockingFirst() categoriesModel.searchAll("Tes", emptyList())
assertEquals("Test", actualCategoryNameCaps.name) .test()
.assertValues(expectedItems)
} }
/**
* For testing the substring search algorithm for Categories search
* To be more precise it tests the In Between substring( ex: searching `atte`
* will give search suggestions: `Latte`, `Iced latte` e.t.c) which has been described
* on github repo wiki:
* https://github.com/commons-app/apps-android-commons/wiki/Category-suggestions-(readme)#user-content-3-category-search-when-typing-in-the-search-field-has-been-made-more-flexible
*/
@Test @Test
fun searchAllFoundCaseTestForSubstringSearch() { fun `searchAll with empty search terms creates results from gps, title search & recents`() {
val gpsCategoryModel: GpsCategoryModel = mock() val gpsCategoryModel: GpsCategoryModel = mock()
val kvStore: JsonKvStore = mock()
whenever(gpsCategoryModel.categoryList).thenReturn(listOf("gpsCategory")) whenever(gpsCategoryModel.categoriesFromLocation)
.thenReturn(BehaviorSubject.createDefault(listOf("gpsCategory")))
whenever(categoryClient.searchCategories("tes", 25)) whenever(categoryClient.searchCategories("tes", 25))
.thenReturn(Observable.just("tes")) .thenReturn(Observable.just(listOf("titleSearch")))
whenever(kvStore.getString("Category", "")).thenReturn("Random Value")
whenever(categoryDao.recentCategories(25)).thenReturn(listOf("recentCategories")) whenever(categoryDao.recentCategories(25)).thenReturn(listOf("recentCategories"))
CategoriesModel( CategoriesModel(categoryClient, categoryDao, gpsCategoryModel)
categoryClient, .searchAll("", listOf("tes"))
categoryDao,
kvStore,
gpsCategoryModel
).searchAll(null, listOf("tes"))
.test() .test()
.assertValues( .assertValue(
CategoryItem("gpsCategory", false), listOf(
CategoryItem("tes", false), CategoryItem("gpsCategory", false),
CategoryItem("Random Value", false), CategoryItem("titleSearch", false),
CategoryItem("recentCategories", false) CategoryItem("recentCategories", false)
)
) )
} }
} }

View file

@ -1,20 +1,26 @@
package fr.free.nrw.commons.category package fr.free.nrw.commons.category
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.whenever
import io.reactivex.Observable import io.reactivex.Observable
import junit.framework.Assert.*
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.mockito.* import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.anyString
import org.mockito.InjectMocks
import org.mockito.Mock
import org.mockito.Mockito.mock
import org.mockito.MockitoAnnotations
import org.wikipedia.dataclient.mwapi.MwQueryPage import org.wikipedia.dataclient.mwapi.MwQueryPage
import org.wikipedia.dataclient.mwapi.MwQueryResponse import org.wikipedia.dataclient.mwapi.MwQueryResponse
import org.wikipedia.dataclient.mwapi.MwQueryResult import org.wikipedia.dataclient.mwapi.MwQueryResult
class CategoryClientTest { class CategoryClientTest {
@Mock @Mock
internal var categoryInterface: CategoryInterface? = null internal lateinit var categoryInterface: CategoryInterface
@InjectMocks @InjectMocks
var categoryClient: CategoryClient? = null lateinit var categoryClient: CategoryClient
@Before @Before
@Throws(Exception::class) @Throws(Exception::class)
@ -24,132 +30,111 @@ class CategoryClientTest {
@Test @Test
fun searchCategoriesFound() { fun searchCategoriesFound() {
val mwQueryPage = Mockito.mock(MwQueryPage::class.java) val mockResponse = withMockResponse("Category:Test")
Mockito.`when`(mwQueryPage.title()).thenReturn("Category:Test") whenever(categoryInterface.searchCategories(anyString(), anyInt(), anyInt()))
val mwQueryResult = Mockito.mock(MwQueryResult::class.java) .thenReturn(Observable.just(mockResponse))
Mockito.`when`(mwQueryResult.pages()).thenReturn(listOf(mwQueryPage)) categoryClient.searchCategories("tes", 10)
val mockResponse = Mockito.mock(MwQueryResponse::class.java) .test()
Mockito.`when`(mockResponse.query()).thenReturn(mwQueryResult) .assertValues(listOf("Test"))
categoryClient.searchCategories("tes", 10, 10)
Mockito.`when`(categoryInterface!!.searchCategories(ArgumentMatchers.anyString(), ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt())) .test()
.thenReturn(Observable.just(mockResponse)) .assertValues(listOf("Test"))
val actualCategoryName = categoryClient!!.searchCategories("tes", 10).blockingFirst()
assertEquals("Test", actualCategoryName)
val actualCategoryName2 = categoryClient!!.searchCategories("tes", 10, 10).blockingFirst()
assertEquals("Test", actualCategoryName2)
} }
@Test @Test
fun searchCategoriesNull() { fun searchCategoriesNull() {
val mwQueryResult = Mockito.mock(MwQueryResult::class.java) val mockResponse = withNullPages()
Mockito.`when`(mwQueryResult.pages()).thenReturn(null) whenever(categoryInterface.searchCategories(anyString(), anyInt(), anyInt()))
val mockResponse = Mockito.mock(MwQueryResponse::class.java) .thenReturn(Observable.just(mockResponse))
Mockito.`when`(mockResponse.query()).thenReturn(mwQueryResult) categoryClient.searchCategories("tes", 10)
.test()
Mockito.`when`(categoryInterface!!.searchCategories(ArgumentMatchers.anyString(), ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt())) .assertValues(emptyList())
.thenReturn(Observable.just(mockResponse)) categoryClient.searchCategories("tes", 10, 10)
.test()
categoryClient!!.searchCategories("tes", 10).subscribe( .assertValues(emptyList())
{ fail("SearchCategories returned element when it shouldn't have.") },
{ s -> throw s })
categoryClient!!.searchCategories("tes", 10, 10).subscribe(
{ fail("SearchCategories returned element when it shouldn't have.") },
{ s -> throw s })
} }
@Test @Test
fun searchCategoriesForPrefixFound() { fun searchCategoriesForPrefixFound() {
val mwQueryPage = Mockito.mock(MwQueryPage::class.java) val mockResponse = withMockResponse("Category:Test")
Mockito.`when`(mwQueryPage.title()).thenReturn("Category:Test") whenever(categoryInterface.searchCategoriesForPrefix(anyString(), anyInt(), anyInt()))
val mwQueryResult = Mockito.mock(MwQueryResult::class.java) .thenReturn(Observable.just(mockResponse))
Mockito.`when`(mwQueryResult.pages()).thenReturn(listOf(mwQueryPage)) categoryClient.searchCategoriesForPrefix("tes", 10)
val mockResponse = Mockito.mock(MwQueryResponse::class.java) .test()
Mockito.`when`(mockResponse.query()).thenReturn(mwQueryResult) .assertValues(listOf("Test"))
categoryClient.searchCategoriesForPrefix("tes", 10, 10)
Mockito.`when`(categoryInterface!!.searchCategoriesForPrefix(ArgumentMatchers.anyString(), ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt())) .test()
.thenReturn(Observable.just(mockResponse)) .assertValues(listOf("Test"))
val actualCategoryName = categoryClient!!.searchCategoriesForPrefix("tes", 10).blockingFirst()
assertEquals("Test", actualCategoryName)
val actualCategoryName2 = categoryClient!!.searchCategoriesForPrefix("tes", 10, 10).blockingFirst()
assertEquals("Test", actualCategoryName2)
} }
@Test @Test
fun searchCategoriesForPrefixNull() { fun searchCategoriesForPrefixNull() {
val mwQueryResult = Mockito.mock(MwQueryResult::class.java) val mockResponse = withNullPages()
Mockito.`when`(mwQueryResult.pages()).thenReturn(null) whenever(categoryInterface.searchCategoriesForPrefix(anyString(), anyInt(), anyInt()))
val mockResponse = Mockito.mock(MwQueryResponse::class.java) .thenReturn(Observable.just(mockResponse))
Mockito.`when`(mockResponse.query()).thenReturn(mwQueryResult) categoryClient.searchCategoriesForPrefix("tes", 10)
.test()
Mockito.`when`(categoryInterface!!.searchCategoriesForPrefix(ArgumentMatchers.anyString(), ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt())) .assertValues(emptyList())
.thenReturn(Observable.just(mockResponse)) categoryClient.searchCategoriesForPrefix("tes", 10, 10)
categoryClient!!.searchCategoriesForPrefix("tes", 10).subscribe( .test()
{ fail("SearchCategories returned element when it shouldn't have.") }, .assertValues(emptyList())
{ s -> throw s })
categoryClient!!.searchCategoriesForPrefix("tes", 10, 10).subscribe(
{ fail("SearchCategories returned element when it shouldn't have.") },
{ s -> throw s })
} }
@Test @Test
fun getParentCategoryListFound() { fun getParentCategoryListFound() {
val mwQueryPage = Mockito.mock(MwQueryPage::class.java) val mockResponse = withMockResponse("Category:Test")
Mockito.`when`(mwQueryPage.title()).thenReturn("Category:Test") whenever(categoryInterface.getParentCategoryList(anyString()))
val mwQueryResult = Mockito.mock(MwQueryResult::class.java) .thenReturn(Observable.just(mockResponse))
Mockito.`when`(mwQueryResult.pages()).thenReturn(listOf(mwQueryPage)) categoryClient.getParentCategoryList("tes")
val mockResponse = Mockito.mock(MwQueryResponse::class.java) .test()
Mockito.`when`(mockResponse.query()).thenReturn(mwQueryResult) .assertValues(listOf("Test"))
Mockito.`when`(categoryInterface!!.getParentCategoryList(ArgumentMatchers.anyString()))
.thenReturn(Observable.just(mockResponse))
val actualCategoryName = categoryClient!!.getParentCategoryList("tes").blockingFirst()
assertEquals("Test", actualCategoryName)
} }
@Test @Test
fun getParentCategoryListNull() { fun getParentCategoryListNull() {
val mwQueryResult = Mockito.mock(MwQueryResult::class.java) val mockResponse = withNullPages()
Mockito.`when`(mwQueryResult.pages()).thenReturn(null) whenever(categoryInterface.getParentCategoryList(anyString()))
val mockResponse = Mockito.mock(MwQueryResponse::class.java) .thenReturn(Observable.just(mockResponse))
Mockito.`when`(mockResponse.query()).thenReturn(mwQueryResult) categoryClient.getParentCategoryList("tes")
.test()
Mockito.`when`(categoryInterface!!.getParentCategoryList(ArgumentMatchers.anyString())) .assertValues(emptyList())
.thenReturn(Observable.just(mockResponse))
categoryClient!!.getParentCategoryList("tes").subscribe(
{ fail("SearchCategories returned element when it shouldn't have.") },
{ s -> throw s })
} }
@Test @Test
fun getSubCategoryListFound() { fun getSubCategoryListFound() {
val mwQueryPage = Mockito.mock(MwQueryPage::class.java) val mockResponse = withMockResponse("Category:Test")
Mockito.`when`(mwQueryPage.title()).thenReturn("Category:Test") whenever(categoryInterface.getSubCategoryList("tes"))
val mwQueryResult = Mockito.mock(MwQueryResult::class.java) .thenReturn(Observable.just(mockResponse))
Mockito.`when`(mwQueryResult.pages()).thenReturn(listOf(mwQueryPage)) categoryClient.getSubCategoryList("tes")
val mockResponse = Mockito.mock(MwQueryResponse::class.java) .test()
Mockito.`when`(mockResponse.query()).thenReturn(mwQueryResult) .assertValues(listOf("Test"))
Mockito.`when`(categoryInterface!!.getSubCategoryList(ArgumentMatchers.anyString()))
.thenReturn(Observable.just(mockResponse))
val actualCategoryName = categoryClient!!.getSubCategoryList("tes").blockingFirst()
assertEquals("Test", actualCategoryName)
} }
@Test @Test
fun getSubCategoryListNull() { fun getSubCategoryListNull() {
val mwQueryResult = Mockito.mock(MwQueryResult::class.java) val mockResponse = withNullPages()
Mockito.`when`(mwQueryResult.pages()).thenReturn(null) whenever(categoryInterface.getSubCategoryList(anyString()))
val mockResponse = Mockito.mock(MwQueryResponse::class.java) .thenReturn(Observable.just(mockResponse))
Mockito.`when`(mockResponse.query()).thenReturn(mwQueryResult) categoryClient.getSubCategoryList("tes")
.test()
Mockito.`when`(categoryInterface!!.getSubCategoryList(ArgumentMatchers.anyString())) .assertValues(emptyList())
.thenReturn(Observable.just(mockResponse))
categoryClient!!.getSubCategoryList("tes").subscribe(
{ fail("SearchCategories returned element when it shouldn't have.") },
{ s -> throw s })
} }
}
private fun withMockResponse(title: String): MwQueryResponse? {
val mwQueryPage: MwQueryPage = mock()
whenever(mwQueryPage.title()).thenReturn(title)
val mwQueryResult: MwQueryResult = mock()
whenever(mwQueryResult.pages()).thenReturn(listOf(mwQueryPage))
val mockResponse = mock(MwQueryResponse::class.java)
whenever(mockResponse.query()).thenReturn(mwQueryResult)
return mockResponse
}
private fun withNullPages(): MwQueryResponse? {
val mwQueryResult = mock(MwQueryResult::class.java)
whenever(mwQueryResult.pages()).thenReturn(null)
val mockResponse = mock(MwQueryResponse::class.java)
whenever(mockResponse.query()).thenReturn(mwQueryResult)
return mockResponse
}
}

View file

@ -1,7 +1,7 @@
package fr.free.nrw.commons.upload package fr.free.nrw.commons.upload
import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.*
import com.nhaarman.mockitokotlin2.whenever import fr.free.nrw.commons.R
import fr.free.nrw.commons.category.CategoryItem import fr.free.nrw.commons.category.CategoryItem
import fr.free.nrw.commons.repository.UploadRepository import fr.free.nrw.commons.repository.UploadRepository
import fr.free.nrw.commons.upload.categories.CategoriesContract import fr.free.nrw.commons.upload.categories.CategoriesContract
@ -10,7 +10,6 @@ import io.reactivex.Observable
import io.reactivex.schedulers.TestScheduler import io.reactivex.schedulers.TestScheduler
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.mockito.ArgumentMatchers
import org.mockito.Mock import org.mockito.Mock
import org.mockito.MockitoAnnotations import org.mockito.MockitoAnnotations
@ -20,6 +19,7 @@ import org.mockito.MockitoAnnotations
class CategoriesPresenterTest { class CategoriesPresenterTest {
@Mock @Mock
internal lateinit var repository: UploadRepository internal lateinit var repository: UploadRepository
@Mock @Mock
internal lateinit var view: CategoriesContract.View internal lateinit var view: CategoriesContract.View
@ -49,30 +49,76 @@ class CategoriesPresenterTest {
* unit test case for method CategoriesPresenter.searchForCategories * unit test case for method CategoriesPresenter.searchForCategories
*/ */
@Test @Test
fun searchForCategoriesTest() { fun `searchForCategories combines selection and search results without years distinctly`() {
whenever(repository.sortBySimilarity(ArgumentMatchers.anyString())).thenReturn(Comparator<CategoryItem> { _, _ -> 1 }) val nonEmptyCaptionUploadItem = mock<UploadItem>()
whenever(repository.selectedCategories).thenReturn(categoryItems) whenever(nonEmptyCaptionUploadItem.uploadMediaDetails)
whenever(repository.searchAll(ArgumentMatchers.anyString(), ArgumentMatchers.anyList())).thenReturn(Observable.empty()) .thenReturn(listOf(UploadMediaDetail(captionText = "nonEmpty")))
val emptyCaptionUploadItem = mock<UploadItem>()
whenever(emptyCaptionUploadItem.uploadMediaDetails)
.thenReturn(listOf(UploadMediaDetail(captionText = "")))
whenever(repository.uploads).thenReturn(
listOf(
nonEmptyCaptionUploadItem,
emptyCaptionUploadItem
)
)
whenever(repository.searchAll("test", listOf("nonEmpty")))
.thenReturn(
Observable.just(
listOf(
categoryItem("selected"),
categoryItem("doesContainYear")
)
)
)
whenever(repository.containsYear("selected")).thenReturn(false)
whenever(repository.containsYear("doesContainYear")).thenReturn(true)
whenever(repository.selectedCategories).thenReturn(listOf(categoryItem("selected", true)))
categoriesPresenter.searchForCategories("test") categoriesPresenter.searchForCategories("test")
testScheduler.triggerActions()
verify(view).showProgress(true) verify(view).showProgress(true)
verify(view).showError(null) verify(view).showError(null)
verify(view).setCategories(null) verify(view).setCategories(null)
testScheduler.triggerActions() verify(view).setCategories(listOf(categoryItem("selected", true)))
verify(view).setCategories(categoryItems)
verify(view).showProgress(false) verify(view).showProgress(false)
verifyNoMoreInteractions(view)
}
@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.selectedCategories).thenReturn(listOf())
categoriesPresenter.searchForCategories("test")
testScheduler.triggerActions()
verify(view).showProgress(true)
verify(view).showError(null)
verify(view).setCategories(null)
verify(view).setCategories(listOf())
verify(view).showProgress(false)
verify(view).showError(R.string.no_categories_found)
verifyNoMoreInteractions(view)
} }
/** /**
* unit test for method CategoriesPresenter.verifyCategories * unit test for method CategoriesPresenter.verifyCategories
*/ */
@Test @Test
fun verifyCategoriesTest() { fun `verifyCategories with non empty selection goes to next screen`() {
whenever(repository.selectedCategories).thenReturn(categoryItems) val item = categoryItem()
whenever(repository.selectedCategories).thenReturn(listOf(item))
categoriesPresenter.verifyCategories() categoriesPresenter.verifyCategories()
verify(repository).setSelectedCategories(ArgumentMatchers.anyList()) verify(repository).setSelectedCategories(listOf(item.name))
verify(view).goToNextScreen() verify(view).goToNextScreen()
} }
@Test
fun `verifyCategories with empty selection show no category selected`() {
whenever(repository.selectedCategories).thenReturn(listOf())
categoriesPresenter.verifyCategories()
verify(view).showNoCategorySelected()
}
/** /**
* Test onCategory Item clicked * Test onCategory Item clicked
*/ */
@ -81,4 +127,7 @@ class CategoriesPresenterTest {
categoriesPresenter.onCategoryItemClicked(categoryItem) categoriesPresenter.onCategoryItemClicked(categoryItem)
verify(repository).onCategoryClicked(categoryItem) verify(repository).onCategoryClicked(categoryItem)
} }
private fun categoryItem(name: String = "name", selected: Boolean = false) =
CategoryItem(name, selected)
} }

View file

@ -1,76 +1,33 @@
package fr.free.nrw.commons.upload package fr.free.nrw.commons.upload
import org.junit.Assert.*
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
class GpsCategoryModelTest { class GpsCategoryModelTest {
lateinit var gpsCategoryModel: GpsCategoryModel
private lateinit var testObject: GpsCategoryModel
@Before @Before
fun setUp() { fun setUp() {
testObject = GpsCategoryModel() gpsCategoryModel = GpsCategoryModel()
} }
@Test @Test
fun initiallyTheModelIsEmpty() { fun `intial value is empty`() {
assertFalse(testObject.gpsCatExists) gpsCategoryModel.categoriesFromLocation.test().assertValues(emptyList())
assertTrue(testObject.categoryList.isEmpty())
} }
@Test @Test
fun addingCategoriesToTheModel() { fun `setCategoriesFromLocation emits the new value`() {
testObject.categoryList = listOf("one") val expectedList = listOf("category")
assertTrue(testObject.gpsCatExists) gpsCategoryModel.categoriesFromLocation.test()
assertFalse(testObject.categoryList.isEmpty()) .also { gpsCategoryModel.setCategoriesFromLocation(expectedList) }
assertEquals(listOf("one"), testObject.categoryList) .assertValues(emptyList(), expectedList)
} }
@Test @Test
fun duplicatesAreIgnored() { fun `clear emits an empty value`() {
testObject.categoryList = listOf("one", "one") gpsCategoryModel.categoriesFromLocation.test()
assertEquals(listOf("one"), testObject.categoryList) .also { gpsCategoryModel.clear() }
} .assertValues(emptyList(), emptyList())
@Test
fun modelProtectsAgainstExternalModification() {
testObject.categoryList = listOf("one")
val list = testObject.categoryList
list.add("two")
assertEquals(listOf("one"), testObject.categoryList)
}
@Test
fun clearingTheModel() {
testObject.categoryList = listOf("one")
testObject.clear()
assertFalse(testObject.gpsCatExists)
assertTrue(testObject.categoryList.isEmpty())
testObject.categoryList = listOf("two")
assertEquals(listOf("two"), testObject.categoryList)
}
@Test
fun settingTheListHandlesNull() {
testObject.categoryList = listOf("one")
testObject.categoryList = null
assertFalse(testObject.gpsCatExists)
assertTrue(testObject.categoryList.isEmpty())
}
@Test
fun settingTheListOverwritesExistingValues() {
testObject.categoryList = listOf("one")
testObject.categoryList = listOf("two")
assertEquals(listOf("two"), testObject.categoryList)
} }
} }

View file

@ -25,6 +25,7 @@ PREFERENCE_VERSION=1.1.0
CORE_KTX_VERSION=1.2.0 CORE_KTX_VERSION=1.2.0
ADAPTER_DELEGATES_VERSION=4.3.0 ADAPTER_DELEGATES_VERSION=4.3.0
PAGING_VERSION=2.1.2 PAGING_VERSION=2.1.2
MULTIDEX_VERSION=2.0.1
systemProp.http.proxyPort=0 systemProp.http.proxyPort=0
systemProp.http.proxyHost= systemProp.http.proxyHost=