Skip to content

Commit 04b796b

Browse files
sivaraamashishkumar468
authored andcommitted
Revert "Fixes: commons-app#3179 Make category search non case-sensitive (commons-app#3326)" (commons-app#3636)
Simply lower casing the name of the category sent to the server doesn't result in the server doing a case insensitive category search. In fact, it reduces the category search space as only categories that has a lower case character is searched even if the search text contains upper case characters. The test case did not catch this issue as the first character of the title is case insensitive[1]. So, revert the changes done in commit afdeaae. See further disucssion in the issue thread of commons-app#3179 starting from [2]. [1]: https://www.mediawiki.org/wiki/Manual:Page_title [2]: commons-app#3179 (comment)
1 parent a1be13f commit 04b796b

File tree

4 files changed

+556
-74
lines changed

4 files changed

+556
-74
lines changed
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
package fr.free.nrw.commons.category;
2+
3+
import android.text.TextUtils;
4+
5+
import java.util.ArrayList;
6+
import java.util.Calendar;
7+
import java.util.Comparator;
8+
import java.util.Date;
9+
import java.util.HashMap;
10+
import java.util.List;
11+
12+
import javax.inject.Inject;
13+
import javax.inject.Named;
14+
15+
import fr.free.nrw.commons.kvstore.JsonKvStore;
16+
import fr.free.nrw.commons.upload.GpsCategoryModel;
17+
import fr.free.nrw.commons.utils.StringSortingUtils;
18+
import io.reactivex.Observable;
19+
import timber.log.Timber;
20+
21+
/**
22+
* The model class for categories in upload
23+
*/
24+
public class CategoriesModel{
25+
private static final int SEARCH_CATS_LIMIT = 25;
26+
27+
private final CategoryClient categoryClient;
28+
private final CategoryDao categoryDao;
29+
private final JsonKvStore directKvStore;
30+
31+
private HashMap<String, ArrayList<String>> categoriesCache;
32+
private List<CategoryItem> selectedCategories;
33+
34+
@Inject GpsCategoryModel gpsCategoryModel;
35+
@Inject
36+
public CategoriesModel(CategoryClient categoryClient,
37+
CategoryDao categoryDao,
38+
@Named("default_preferences") JsonKvStore directKvStore) {
39+
this.categoryClient = categoryClient;
40+
this.categoryDao = categoryDao;
41+
this.directKvStore = directKvStore;
42+
this.categoriesCache = new HashMap<>();
43+
this.selectedCategories = new ArrayList<>();
44+
}
45+
46+
/**
47+
* Sorts CategoryItem by similarity
48+
* @param filter
49+
* @return
50+
*/
51+
public Comparator<CategoryItem> sortBySimilarity(final String filter) {
52+
Comparator<String> stringSimilarityComparator = StringSortingUtils.sortBySimilarity(filter);
53+
return (firstItem, secondItem) -> stringSimilarityComparator
54+
.compare(firstItem.getName(), secondItem.getName());
55+
}
56+
57+
/**
58+
* Returns if the item contains an year
59+
* @param item
60+
* @return
61+
*/
62+
public boolean containsYear(String item) {
63+
//Check for current and previous year to exclude these categories from removal
64+
Calendar now = Calendar.getInstance();
65+
int year = now.get(Calendar.YEAR);
66+
String yearInString = String.valueOf(year);
67+
68+
int prevYear = year - 1;
69+
String prevYearInString = String.valueOf(prevYear);
70+
Timber.d("Previous year: %s", prevYearInString);
71+
72+
//Check if item contains a 4-digit word anywhere within the string (.* is wildcard)
73+
//And that item does not equal the current year or previous year
74+
//And if it is an irrelevant category such as Media_needing_categories_as_of_16_June_2017(Issue #750)
75+
//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
76+
return ((item.matches(".*(19|20)\\d{2}.*") && !item.contains(yearInString) && !item.contains(prevYearInString))
77+
|| item.matches("(.*)needing(.*)") || item.matches("(.*)taken on(.*)")
78+
|| (item.matches(".*0s.*") && !item.matches(".*(200|201)0s.*")));
79+
}
80+
81+
/**
82+
* Updates category count in category dao
83+
* @param item
84+
*/
85+
public void updateCategoryCount(CategoryItem item) {
86+
Category category = categoryDao.find(item.getName());
87+
88+
// Newly used category...
89+
if (category == null) {
90+
category = new Category(null, item.getName(), new Date(), 0);
91+
}
92+
93+
category.incTimesUsed();
94+
categoryDao.save(category);
95+
}
96+
97+
boolean cacheContainsKey(String term) {
98+
return categoriesCache.containsKey(term);
99+
}
100+
//endregion
101+
102+
/**
103+
* Regional category search
104+
* @param term
105+
* @param imageTitleList
106+
* @return
107+
*/
108+
public Observable<CategoryItem> searchAll(String term, List<String> imageTitleList) {
109+
//If query text is empty, show him category based on gps and title and recent searches
110+
if (TextUtils.isEmpty(term)) {
111+
Observable<CategoryItem> categoryItemObservable = gpsCategories()
112+
.concatWith(titleCategories(imageTitleList));
113+
if (hasDirectCategories()) {
114+
categoryItemObservable.concatWith(directCategories().concatWith(recentCategories()));
115+
}
116+
return categoryItemObservable;
117+
}
118+
119+
//if user types in something that is in cache, return cached category
120+
if (cacheContainsKey(term)) {
121+
return Observable.fromIterable(getCachedCategories(term))
122+
.map(name -> new CategoryItem(name, false));
123+
}
124+
125+
//otherwise, search API for matching categories
126+
return categoryClient
127+
.searchCategoriesForPrefix(term, SEARCH_CATS_LIMIT)
128+
.map(name -> new CategoryItem(name, false));
129+
}
130+
131+
132+
/**
133+
* Returns cached categories
134+
* @param term
135+
* @return
136+
*/
137+
private ArrayList<String> getCachedCategories(String term) {
138+
return categoriesCache.get(term);
139+
}
140+
141+
/**
142+
* Returns if we have a category in DirectKV Store
143+
* @return
144+
*/
145+
private boolean hasDirectCategories() {
146+
return !directKvStore.getString("Category", "").equals("");
147+
}
148+
149+
/**
150+
* Returns categories in DirectKVStore
151+
* @return
152+
*/
153+
private Observable<CategoryItem> directCategories() {
154+
String directCategory = directKvStore.getString("Category", "");
155+
List<String> categoryList = new ArrayList<>();
156+
Timber.d("Direct category found: " + directCategory);
157+
158+
if (!directCategory.equals("")) {
159+
categoryList.add(directCategory);
160+
Timber.d("DirectCat does not equal emptyString. Direct Cat list has " + categoryList);
161+
}
162+
return Observable.fromIterable(categoryList).map(name -> new CategoryItem(name, false));
163+
}
164+
165+
/**
166+
* Returns GPS categories
167+
* @return
168+
*/
169+
Observable<CategoryItem> gpsCategories() {
170+
return Observable.fromIterable(gpsCategoryModel.getCategoryList())
171+
.map(name -> new CategoryItem(name, false));
172+
}
173+
174+
/**
175+
* Returns title based categories
176+
* @param titleList
177+
* @return
178+
*/
179+
private Observable<CategoryItem> titleCategories(List<String> titleList) {
180+
return Observable.fromIterable(titleList)
181+
.concatMap(this::getTitleCategories);
182+
}
183+
184+
/**
185+
* Return category for single title
186+
* @param title
187+
* @return
188+
*/
189+
private Observable<CategoryItem> getTitleCategories(String title) {
190+
return categoryClient.searchCategories(title, SEARCH_CATS_LIMIT)
191+
.map(name -> new CategoryItem(name, false));
192+
}
193+
194+
/**
195+
* Returns recent categories
196+
* @return
197+
*/
198+
private Observable<CategoryItem> recentCategories() {
199+
return Observable.fromIterable(categoryDao.recentCategories(SEARCH_CATS_LIMIT))
200+
.map(s -> new CategoryItem(s, false));
201+
}
202+
203+
/**
204+
* Handles category item selection
205+
* @param item
206+
*/
207+
public void onCategoryItemClicked(CategoryItem item) {
208+
if (item.isSelected()) {
209+
selectCategory(item);
210+
updateCategoryCount(item);
211+
} else {
212+
unselectCategory(item);
213+
}
214+
}
215+
216+
/**
217+
* Select's category
218+
* @param item
219+
*/
220+
public void selectCategory(CategoryItem item) {
221+
selectedCategories.add(item);
222+
}
223+
224+
/**
225+
* Unselect Category
226+
* @param item
227+
*/
228+
public void unselectCategory(CategoryItem item) {
229+
selectedCategories.remove(item);
230+
}
231+
232+
233+
/**
234+
* Get Selected Categories
235+
* @return
236+
*/
237+
public List<CategoryItem> getSelectedCategories() {
238+
return selectedCategories;
239+
}
240+
241+
/**
242+
* Get Categories String List
243+
* @return
244+
*/
245+
public List<String> getCategoryStringList() {
246+
List<String> output = new ArrayList<>();
247+
for (CategoryItem item : selectedCategories) {
248+
output.add(item.getName());
249+
}
250+
return output;
251+
}
252+
253+
/**
254+
* Cleanup the existing in memory cache's
255+
*/
256+
public void cleanUp() {
257+
this.categoriesCache.clear();
258+
this.selectedCategories.clear();
259+
}
260+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package fr.free.nrw.commons.category;
2+
3+
4+
import androidx.annotation.NonNull;
5+
6+
import org.wikipedia.dataclient.mwapi.MwQueryPage;
7+
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
8+
import org.wikipedia.dataclient.mwapi.MwQueryResult;
9+
10+
import java.util.List;
11+
12+
import javax.inject.Inject;
13+
import javax.inject.Singleton;
14+
15+
import io.reactivex.Observable;
16+
import timber.log.Timber;
17+
18+
/**
19+
* Category Client to handle custom calls to Commons MediaWiki APIs
20+
*/
21+
@Singleton
22+
public class CategoryClient {
23+
24+
private final CategoryInterface CategoryInterface;
25+
26+
@Inject
27+
public CategoryClient(CategoryInterface CategoryInterface) {
28+
this.CategoryInterface = CategoryInterface;
29+
}
30+
31+
/**
32+
* Searches for categories containing the specified string.
33+
*
34+
* @param filter The string to be searched
35+
* @param itemLimit How many results are returned
36+
* @param offset Starts returning items from the nth result. If offset is 9, the response starts with the 9th item of the search result
37+
* @return
38+
*/
39+
public Observable<String> searchCategories(String filter, int itemLimit, int offset) {
40+
return responseToCategoryName(CategoryInterface.searchCategories(filter, itemLimit, offset));
41+
42+
}
43+
44+
/**
45+
* Searches for categories containing the specified string.
46+
*
47+
* @param filter The string to be searched
48+
* @param itemLimit How many results are returned
49+
* @return
50+
*/
51+
public Observable<String> searchCategories(String filter, int itemLimit) {
52+
return searchCategories(filter, itemLimit, 0);
53+
54+
}
55+
56+
/**
57+
* Searches for categories starting with the specified string.
58+
*
59+
* @param prefix The prefix to be searched
60+
* @param itemLimit How many results are returned
61+
* @param offset Starts returning items from the nth result. If offset is 9, the response starts with the 9th item of the search result
62+
* @return
63+
*/
64+
public Observable<String> searchCategoriesForPrefix(String prefix, int itemLimit, int offset) {
65+
return responseToCategoryName(CategoryInterface.searchCategoriesForPrefix(prefix, itemLimit, offset));
66+
}
67+
68+
/**
69+
* Searches for categories starting with the specified string.
70+
*
71+
* @param prefix The prefix to be searched
72+
* @param itemLimit How many results are returned
73+
* @return
74+
*/
75+
public Observable<String> searchCategoriesForPrefix(String prefix, int itemLimit) {
76+
return searchCategoriesForPrefix(prefix, itemLimit, 0);
77+
}
78+
79+
80+
/**
81+
* The method takes categoryName as input and returns a List of Subcategories
82+
* It uses the generator query API to get the subcategories in a category, 500 at a time.
83+
*
84+
* @param categoryName Category name as defined on commons
85+
* @return Observable emitting the categories returned. If our search yielded "Category:Test", "Test" is emitted.
86+
*/
87+
public Observable<String> getSubCategoryList(String categoryName) {
88+
return responseToCategoryName(CategoryInterface.getSubCategoryList(categoryName));
89+
}
90+
91+
/**
92+
* The method takes categoryName as input and returns a List of parent categories
93+
* It uses the generator query API to get the parent categories of a category, 500 at a time.
94+
*
95+
* @param categoryName Category name as defined on commons
96+
* @return
97+
*/
98+
@NonNull
99+
public Observable<String> getParentCategoryList(String categoryName) {
100+
return responseToCategoryName(CategoryInterface.getParentCategoryList(categoryName));
101+
}
102+
103+
/**
104+
* Internal function to reduce code reuse. Extracts the categories returned from MwQueryResponse.
105+
*
106+
* @param responseObservable The query response observable
107+
* @return Observable emitting the categories returned. If our search yielded "Category:Test", "Test" is emitted.
108+
*/
109+
private Observable<String> responseToCategoryName(Observable<MwQueryResponse> responseObservable) {
110+
return responseObservable
111+
.flatMap(mwQueryResponse -> {
112+
MwQueryResult query;
113+
List<MwQueryPage> pages;
114+
if ((query = mwQueryResponse.query()) == null ||
115+
(pages = query.pages()) == null) {
116+
Timber.d("No categories returned.");
117+
return Observable.empty();
118+
} else
119+
return Observable.fromIterable(pages);
120+
})
121+
.map(MwQueryPage::title)
122+
.doOnEach(s -> Timber.d("Category returned: %s", s))
123+
.map(cat -> cat.replace("Category:", ""));
124+
}
125+
}

0 commit comments

Comments
 (0)