Include previous Wikimedia hackathon (2018) task, peer review, to codebase (#2602)

* Add new activity to manifest

* Create review activity layout base

* Add a new menu item to drawer for peer review

* Add a top menu with randomizer icon to review activity

* Add strings for review button

* Add activity to ActivityBuilderModule for injection

* Add a new drawer item to start review acitivty

* Create base of the Review Activity

* Add fragment pager

* Add new fragment for injection

* Create a fragment pager layout

* Wikimedia hackathon 2018 (#1533)

* First draft of fn to get random recent image

* Use log entries for requests to beta, try to connect refresh button

FIXME: runs http request on main thread, breaks

* Tweak button connection

* Add ReviewController class

* Fix fragments

* Wmhack2018 (#1534)

* tiny fixes

* Load pictures into activities

* Re-use same class for all review fragments (#1537)

And try to add pager indicator

* [WIP] category check

* [WIP] add on-click actions to ReviewActivity

* [WIP] add SendThankTask

* Make it beautiful

* Add some category stuff back in to review (#1538)

* Use standalone category extraction code in MediaDataExtractor

* Add categories to category review page

* Change category question text sizes

* Call randomizer whenever the activity is ready

* Add progressbar

* [WIP] add DeleteTask.askReasonAndExecute

* Fix refresh button string

* Typo: "nominate *for* deletion"

* Add formatting to categories and put them in the same textView

* Pass context and adapters as parameters to controller

* Add actions to controller

* Make everyting work

* Add another fragment to thank

* Fix npe

* Add missing execute method

* Some codes

* Add a funy text

* More random recent image selection (#1542)

time-based randomness is biased - if someone uploaded 100 images in
hour, one week ago, and I select a random point in time, their last
image is way more likely to come up than anything else.

With this, there is still bias towards choosing one of the last N
in any burst of uploads (where N is the number of recent changes
fetched) but it's a bit better than before.

* Create Revision class

* Add meaningluf strings

* Error handling for review image/category fetch (#1543)

* Add information layout for username and filename

* Use Single to get firstRevision

* try to add username and filename

* Ensure caption is shown on every review fragment

* Fix build

* Fixes missing import

* Change button text,show current category, add skip image button

* Modify texts, fix night mode issues

* Positive Wording

* fix landscape issue

* Add checkbox popup,rewording

* Spelling Correction

* Fix merge

* Remove commented out code, use lambda

* Simplify toolbar include
This commit is contained in:
Silky Priya 2019-03-21 17:35:23 +05:30 committed by neslihanturan
parent a1a65d0832
commit a32ba452ec
33 changed files with 1594 additions and 33 deletions

View file

@ -3,8 +3,6 @@ package fr.free.nrw.commons.mwapi;
import android.content.Context;
import android.net.Uri;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.text.TextUtils;
import com.google.gson.Gson;
@ -34,9 +32,12 @@ import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Random;
import java.util.TimeZone;
import java.util.concurrent.Callable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
@ -44,6 +45,7 @@ import fr.free.nrw.commons.auth.AccountUtil;
import fr.free.nrw.commons.category.CategoryImageUtils;
import fr.free.nrw.commons.category.QueryContinue;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.media.RecentChangesImageUtils;
import fr.free.nrw.commons.notification.Notification;
import fr.free.nrw.commons.notification.NotificationUtils;
import fr.free.nrw.commons.utils.ConfigUtils;
@ -61,6 +63,14 @@ import static fr.free.nrw.commons.utils.ContinueUtils.getQueryContinue;
*/
public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
private static final String THUMB_SIZE = "640";
// Give up if no random recent image found after 5 tries
private static final int MAX_RANDOM_TRIES = 5;
// Random image request is for some time in the past 30 days
private static final int RANDOM_SECONDS = 60 * 60 * 24 * 30;
// Assuming MW always gives me UTC
private static final SimpleDateFormat isoFormat =
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH);
private static final String FILE_NAMESPACE = "6";
private AbstractHttpClient httpClient;
private CustomMwApi api;
private CustomMwApi wikidataApi;
@ -256,6 +266,19 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
.getString("/api/query/pages/page/@_idx")) != -1;
}
@Override
public boolean thank(String editToken, String revision) throws IOException {
CustomApiResult res = api.action("thank")
.param("rev", revision)
.param("token", editToken)
.param("source", getUserAgent())
.post();
String r = res.getString("/api/result/@success");
// Does this correctly check the success/failure?
// The docs https://www.mediawiki.org/wiki/Extension:Thanks seems unclear about that.
return r.equals("success");
}
@Override
@Nullable
public String edit(String editToken, String processedPageContent, String filename, String summary) throws IOException {
@ -563,6 +586,24 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
.getString("/api/query/pages/page/revisions/rev");
}
@Override
@Nullable
public Single<Revision> firstRevisionOfFile(String filename) {
return Single.fromCallable(() -> {
CustomApiResult res = api.action("query")
.param("prop", "revisions")
.param("rvprop", "timestamp|ids|user")
.param("titles", filename)
.param("rvdir", "newer")
.param("rvlimit", "1")
.get();
return new Revision(
res.getString("/api/query/pages/page/revisions/rev/@revid"),
res.getString("/api/query/pages/page/revisions/rev/@user"),
filename);
});
}
@Override
@NonNull
public List<Notification> getNotifications(boolean archived) {
@ -758,6 +799,50 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
return CategoryImageUtils.getMediaList(childNodes);
}
/**
* This method takes search keyword as input and returns a list of Media objects filtered using search query
* It uses the generator query API to get the images searched using a query, 25 at a time.
* @param query keyword to search images on commons
* @return
*/
// @Override
@NonNull
public List<Media> searchImages(String query, int offset) {
List<CustomApiResult> imageNodes = null;
List<CustomApiResult> authorNodes = null;
CustomApiResult customApiResult;
try {
customApiResult= api.action("query")
.param("format", "xml")
.param("generator", "search")
.param("gsrwhat", "text")
.param("gsrnamespace", "6")
.param("gsrlimit", "25")
.param("gsroffset",offset)
.param("gsrsearch", query)
.param("prop", "imageinfo")
.get();
imageNodes= customApiResult.getNodes("/api/query/pages/page/@title");
authorNodes= customApiResult.getNodes("/api/query/pages/page/imageinfo/ii/@user");
} catch (IOException e) {
Timber.e(e, "Failed to obtain searchImages");
}
if (imageNodes == null) {
return new ArrayList<>();
}
List<Media> images = new ArrayList<>();
for (int i=0; i< imageNodes.size();i++){
String imgName = imageNodes.get(i).getDocument().getTextContent();
Media media = new Media(imgName);
media.setCreator(authorNodes.get(i).getDocument().getTextContent());
images.add(media);
}
return images;
}
/**
* This method takes search keyword as input and returns a list of categories objects filtered using search query
* It uses the generator query API to get the categories searched using a query, 25 at a time.
@ -947,6 +1032,78 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
return userBlocked;
}
// /**
// * This takes userName as input, which is then used to fetch the feedback/achievements
// * statistics using OkHttp and JavaRx. This function return JSONObject
// * @param userName MediaWiki user name
// * @return
// */
// @Override
// public Single<FeedbackResponse> getAchievements(String userName) {
// final String fetchAchievementUrlTemplate =
// wikiMediaToolforgeUrl + "urbanecmbot/commonsmisc/feedback.py";
// return Single.fromCallable(() -> {
// String url = String.format(
// Locale.ENGLISH,
// fetchAchievementUrlTemplate,
// new PageTitle(userName).getText());
// HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
// urlBuilder.addQueryParameter("user", userName);
// Timber.i("Url %s", urlBuilder.toString());
// Request request = new Request.Builder()
// .url(urlBuilder.toString())
// .build();
// Response response = okHttpClient.newCall(request).execute();
// if (response != null && response.body() != null && response.isSuccessful()) {
// String json = response.body().string();
// if (json == null) {
// return null;
// }
// return gson.fromJson(json, FeedbackResponse.class);
// }
// return null;
// });
//
// }
// /**
// * The method returns the picture of the day
// *
// * @return Media object corresponding to the picture of the day
// */
// @Override
// @Nullable
// public Single<Media> getPictureOfTheDay() {
// return Single.fromCallable(() -> {
// CustomApiResult apiResult = null;
// try {
// String template = "Template:Potd/" + DateUtils.getCurrentDate();
// CustomMwApi.RequestBuilder requestBuilder = api.action("query")
// .param("generator", "images")
// .param("format", "xml")
// .param("titles", template)
// .param("prop", "imageinfo")
// .param("iiprop", "url|extmetadata");
//
// apiResult = requestBuilder.get();
// } catch (IOException e) {
// Timber.e(e, "Failed to obtain searchCategories");
// }
//
// if (apiResult == null) {
// return null;
// }
//
// CustomApiResult imageNode = apiResult.getNode("/api/query/pages/page");
// if (imageNode == null
// || imageNode.getDocument() == null) {
// return null;
// }
//
// return CategoryImageUtils.getMediaFromPage(imageNode.getDocument());
// });
// }
private Date parseMWDate(String mwDate) {
SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH); // Assuming MW always gives me UTC
isoFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
@ -967,4 +1124,68 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
Timber.e(e, "Error occurred while logging out");
}
}
// @Override public Single<CampaignResponseDTO> getCampaigns() {
// return Single.fromCallable(() -> {
// Request request = new Request.Builder().url(WIKIMEDIA_CAMPAIGNS_BASE_URL).build();
// Response response = okHttpClient.newCall(request).execute();
// if (response != null && response.body() != null && response.isSuccessful()) {
// String json = response.body().string();
// if (json == null) {
// return null;
// }
// return gson.fromJson(json, CampaignResponseDTO.class);
// }
// return null;
// });
// }
private String formatMWDate(Date date) {
return isoFormat.format(date);
}
public Media getRecentRandomImage() throws IOException {
Media media = null;
int tries = 0;
Random r = new Random();
while (media == null && tries < MAX_RANDOM_TRIES) {
Date now = new Date();
Date startDate = new Date(now.getTime() - r.nextInt(RANDOM_SECONDS) * 1000L);
CustomApiResult apiResult = null;
try {
CustomMwApi.RequestBuilder requestBuilder = api.action("query")
.param("list", "recentchanges")
.param("rcstart", formatMWDate(startDate))
.param("rcnamespace", FILE_NAMESPACE)
.param("rcprop", "title|ids")
.param("rctype", "new|log")
.param("rctoponly", "1");
apiResult = requestBuilder.get();
} catch (IOException e) {
Timber.e(e, "Failed to obtain recent random");
}
if (apiResult != null) {
CustomApiResult recentChangesNode = apiResult.getNode("/api/query/recentchanges");
if (recentChangesNode != null
&& recentChangesNode.getDocument() != null
&& recentChangesNode.getDocument().getChildNodes() != null
&& recentChangesNode.getDocument().getChildNodes().getLength() > 0) {
NodeList childNodes = recentChangesNode.getDocument().getChildNodes();
String imageTitle = RecentChangesImageUtils.findImageInRecentChanges(childNodes);
if (imageTitle != null) {
boolean deletionStatus = pageExists("Commons:Deletion_requests/" + imageTitle);
if (!deletionStatus) {
// strip File: prefix
imageTitle = imageTitle.replace("File:", "");
media = new Media(imageTitle);
}
}
}
}
tries++;
}
return media;
}
}

View file

@ -9,6 +9,7 @@ import java.io.InputStream;
import java.util.List;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.campaigns.CampaignResponseDTO;
import fr.free.nrw.commons.notification.Notification;
import io.reactivex.Observable;
import io.reactivex.Single;
@ -104,7 +105,16 @@ public interface MediaWikiApi {
void logout();
// Single<CampaignResponseDTO> getCampaigns();
boolean thank(String editToken, String revision) throws IOException;
Single<Revision> firstRevisionOfFile(String filename);
interface ProgressListener {
void onProgress(long transferred, long total);
}
@Nullable
Media getRecentRandomImage() throws IOException;
}

View file

@ -0,0 +1,15 @@
package fr.free.nrw.commons.mwapi;
import fr.free.nrw.commons.PageTitle;
public class Revision {
public final String revisionId;
public final String username;
public final PageTitle pageTitle;
public Revision(String revisionId, String username, String pageTitle) {
this.revisionId = revisionId;
this.username = username;
this.pageTitle = new PageTitle(pageTitle);
}
}