Jcasariego has uploaded a new change for review. ( https://gerrit.wikimedia.org/r/353384 )
Change subject: Retrofit SuggestionsTask ...................................................................... Retrofit SuggestionsTask Port SuggestionsTask to Retrofit and try to stay as consistent as possible with the existing code. Bug: T152413 Change-Id: I4bde23e10666ceec5eee105529e5646c14c299bc --- D app/src/androidTest/java/org/wikipedia/page/SuggestionsTaskTest.java M app/src/main/java/org/wikipedia/dataclient/mwapi/MwQueryPage.java M app/src/main/java/org/wikipedia/dataclient/mwapi/MwQueryResponse.java A app/src/main/java/org/wikipedia/page/SuggestionsClient.java D app/src/main/java/org/wikipedia/page/SuggestionsTask.java M app/src/main/java/org/wikipedia/page/bottomcontent/BottomContentHandler.java A app/src/test/java/org/wikipedia/page/SuggestionTest.java A app/src/test/res/raw/suggestion.json 8 files changed, 364 insertions(+), 143 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/apps/android/wikipedia refs/changes/84/353384/1 diff --git a/app/src/androidTest/java/org/wikipedia/page/SuggestionsTaskTest.java b/app/src/androidTest/java/org/wikipedia/page/SuggestionsTaskTest.java deleted file mode 100644 index 115265f..0000000 --- a/app/src/androidTest/java/org/wikipedia/page/SuggestionsTaskTest.java +++ /dev/null @@ -1,83 +0,0 @@ -package org.wikipedia.page; - - -import org.junit.Test; -import org.wikipedia.Constants; -import org.wikipedia.WikipediaApp; -import org.wikipedia.dataclient.WikiSite; -import org.wikipedia.search.SearchResult; -import org.wikipedia.search.SearchResults; -import org.wikipedia.testlib.TestLatch; - -import java.util.ArrayList; -import java.util.List; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.not; -import static org.hamcrest.Matchers.notNullValue; -import static org.wikipedia.test.TestUtil.runOnMainSync; - -/** - * Tests for getting suggestions for further reading. - */ -public class SuggestionsTaskTest { - private static final WikiSite WIKI = WikiSite.forLanguageCode("en"); // suggestions don't seem to work on testwiki - - private WikipediaApp app = WikipediaApp.getInstance(); - - @Test public void testTask() { - final TestLatch latch = new TestLatch(); - runOnMainSync(new Runnable() { - @Override - public void run() { - new SuggestionsTask(app.getAPIForSite(WIKI), WIKI, "test", false) { - @Override - public void onFinish(SearchResults results) { - assertThat(results, notNullValue()); - assertThat(results.getResults().size(), is(Constants.MAX_SUGGESTION_RESULTS)); - - for (SearchResult result : results.getResults()) { - assertThat(result.getPageTitle().getPrefixedText(), not("Test")); - } - latch.countDown(); - } - }.execute(); - } - }); - latch.await(); - } - - @Test public void testFilterNoResults() { - List<SearchResult> originalResults = new ArrayList<>(); - checkFilter(0, originalResults); - } - - @Test public void testFilter1ResultSameAsTitleIgnoreCase() { - List<SearchResult> originalResults = new ArrayList<>(); - originalResults.add(new SearchResult(new PageTitle("Test", WIKI, null, null))); - checkFilter(0, originalResults); - } - - @Test public void testFilter1ResultDifferentFromTitle() { - List<SearchResult> originalResults = new ArrayList<>(); - originalResults.add(new SearchResult(new PageTitle("something else", WIKI, null, null))); - checkFilter(1, originalResults); - } - - @Test public void testFilter4ResultsDifferentFromTitle() { - List<SearchResult> originalResults = new ArrayList<>(); - originalResults.add(new SearchResult(new PageTitle("something else", WIKI, null, null))); - originalResults.add(new SearchResult(new PageTitle("something else", WIKI, null, null))); - originalResults.add(new SearchResult(new PageTitle("something else", WIKI, null, null))); - originalResults.add(new SearchResult(new PageTitle("something else", WIKI, null, null))); - checkFilter(Constants.MAX_SUGGESTION_RESULTS, originalResults); - } - - private void checkFilter(int expected, List<SearchResult> originalResults) { - String title = "test"; - SearchResults searchResults = new SearchResults(originalResults, null, null); - List<SearchResult> filteredList = SearchResults.filter(searchResults, title, false).getResults(); - assertThat(expected, is(filteredList.size())); - } -} diff --git a/app/src/main/java/org/wikipedia/dataclient/mwapi/MwQueryPage.java b/app/src/main/java/org/wikipedia/dataclient/mwapi/MwQueryPage.java index 2007a62..4746105 100644 --- a/app/src/main/java/org/wikipedia/dataclient/mwapi/MwQueryPage.java +++ b/app/src/main/java/org/wikipedia/dataclient/mwapi/MwQueryPage.java @@ -17,6 +17,7 @@ public class MwQueryPage extends BaseModel { @SuppressWarnings("unused") private int pageid; @SuppressWarnings("unused") private int ns; + @SuppressWarnings("unused") private int index; @SuppressWarnings("unused,NullableProblems") @NonNull private String title; @SuppressWarnings("unused") @Nullable private List<LangLink> langlinks; @SuppressWarnings("unused") @Nullable private List<Revision> revisions; @@ -28,6 +29,10 @@ return title; } + public int index(){ + return index; + } + @Nullable public List<LangLink> langLinks() { return langlinks; } diff --git a/app/src/main/java/org/wikipedia/dataclient/mwapi/MwQueryResponse.java b/app/src/main/java/org/wikipedia/dataclient/mwapi/MwQueryResponse.java index 13642d3..58700e7 100644 --- a/app/src/main/java/org/wikipedia/dataclient/mwapi/MwQueryResponse.java +++ b/app/src/main/java/org/wikipedia/dataclient/mwapi/MwQueryResponse.java @@ -5,15 +5,24 @@ import com.google.gson.annotations.SerializedName; +import org.wikipedia.search.SearchResults; + public class MwQueryResponse<T> extends MwResponse { @SuppressWarnings("unused") @SerializedName("batchcomplete") private boolean batchComplete; + @SuppressWarnings("unused") @Nullable @SerializedName("continue") + private FTContinueOffset cont; + @Nullable private T query; public boolean batchComplete() { return batchComplete; + } + + @Nullable public FTContinueOffset cont() { + return cont; } @Nullable public T query() { @@ -28,4 +37,17 @@ protected void setQuery(@Nullable T query) { this.query = query; } + + public static class FTContinueOffset extends SearchResults.ContinueOffset { + @SuppressWarnings("unused") private String gsroffset; + @SuppressWarnings("unused") @SerializedName("continue") private String cont; + + String gsroffset() { + return gsroffset; + } + + String cont() { + return cont; + } + } } diff --git a/app/src/main/java/org/wikipedia/page/SuggestionsClient.java b/app/src/main/java/org/wikipedia/page/SuggestionsClient.java new file mode 100644 index 0000000..9a3cb30 --- /dev/null +++ b/app/src/main/java/org/wikipedia/page/SuggestionsClient.java @@ -0,0 +1,128 @@ +package org.wikipedia.page; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; + +import org.wikipedia.Constants; +import org.wikipedia.dataclient.WikiSite; +import org.wikipedia.dataclient.mwapi.MwException; +import org.wikipedia.dataclient.mwapi.MwQueryPage; +import org.wikipedia.dataclient.mwapi.MwQueryResponse; +import org.wikipedia.dataclient.retrofit.MwCachedService; +import org.wikipedia.search.SearchResult; +import org.wikipedia.search.SearchResults; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import retrofit2.Call; +import retrofit2.Response; +import retrofit2.http.GET; +import retrofit2.http.QueryMap; + +public class SuggestionsClient { + @NonNull private MwCachedService<Service> cachedService = new MwCachedService<>(Service.class); + + public interface Callback { + void success(@NonNull Call<MwQueryResponse<QueryResult>> call, @NonNull SearchResults results); + void failure(@NonNull Call<MwQueryResponse<QueryResult>> call, @NonNull Throwable caught); + } + + public Call<MwQueryResponse<QueryResult>> request(@NonNull WikiSite wiki, + @NonNull PageTitle title, + @NonNull boolean requireThumbnail, + @NonNull boolean getMoreLike, + @NonNull Callback cb) { + + return request(wiki, cachedService.service(wiki), title, requireThumbnail, getMoreLike, cb); + } + + @VisibleForTesting Call<MwQueryResponse<QueryResult>> request(final WikiSite wiki, + @NonNull Service service, + @NonNull final PageTitle title, + @NonNull final boolean requireThumbnail, + @NonNull final boolean getMoreLike, + @NonNull final Callback cb) { + + String maxResultsString = Integer.toString(Constants.MAX_SUGGESTION_RESULTS * 2); + + Map<String, String> data = new HashMap<>(); + data.put("prop", "pageterms|pageimages|pageprops"); + data.put("ppprop", "mainpage|disambiguation"); + data.put("wbptterms", "description"); // only interested in Wikidata description + data.put("generator", "search"); + data.put("gsrsearch", getMoreLike ? ("morelike:" + title.getPrefixedText()) : title.getPrefixedText()); + data.put("gsrnamespace", "0"); + data.put("gsrwhat", "text"); + data.put("gsrinfo", ""); + data.put("gsrprop", "redirecttitle"); + data.put("gsrlimit", maxResultsString); + data.put("piprop", "thumbnail"); // for thumbnail URLs + data.put("pilicense", "any"); + data.put("pithumbsize", Integer.toString(Constants.PREFERRED_THUMB_SIZE)); + data.put("pilimit", maxResultsString); + data.put("titles", title.toString()); + + Call<MwQueryResponse<QueryResult>> call = service.request(data); + + call.enqueue(new retrofit2.Callback<MwQueryResponse<QueryResult>>() { + @Override public void onResponse(Call<MwQueryResponse<QueryResult>> call, + Response<MwQueryResponse<QueryResult>> response) { + if (response.body().success()) { + // now sort the array based on the "index" property + Collections.sort(response.body().query().pages(), new Comparator<MwQueryPage>() { + @Override + public int compare(MwQueryPage mwQueryPage, MwQueryPage t1) { + int ret = ((Integer)mwQueryPage.index()).compareTo(t1.index()); + return ret; + } + }); + + // Create our list of results from the now-sorted array + List<SearchResult> resultList = new ArrayList<>(); + for (MwQueryPage mwQueryPage : response.body().query().pages()) { + //resultList.add(new SearchResult(new PageTitle(item.getString("title"), wiki, thumbUrl, description, properties))); + resultList.add(new SearchResult(new PageTitle(mwQueryPage.title(), wiki, + mwQueryPage.thumbUrl(), + mwQueryPage.description(), + null))); + } + cb.success(call, + SearchResults.filter(new SearchResults(resultList, response.body().cont(), null), + title.getPrefixedText(), + requireThumbnail)); + } else if (response.body().hasError()) { + cb.failure(call, new MwException(response.body().getError())); + } else { + cb.failure(call, new IOException("An unknown error occurred.")); + } + } + + @Override + public void onFailure(Call<MwQueryResponse<QueryResult>> call, Throwable t) { + System.out.print("\nError 1: " + t.getMessage()); + cb.failure(call, t); + } + }); + + return call; + } + + public class QueryResult { + @SuppressWarnings("unused") @Nullable private List<MwQueryPage> pages; + @Nullable List<MwQueryPage> pages() { + return pages; + } + } + + @VisibleForTesting interface Service { + @GET("w/api.php?action=query&format=json&formatversion=2") + Call<MwQueryResponse<QueryResult>> request(@QueryMap(encoded = true) Map<String, String> options); + } +} diff --git a/app/src/main/java/org/wikipedia/page/SuggestionsTask.java b/app/src/main/java/org/wikipedia/page/SuggestionsTask.java deleted file mode 100644 index 5185916..0000000 --- a/app/src/main/java/org/wikipedia/page/SuggestionsTask.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.wikipedia.page; - -import org.mediawiki.api.json.Api; -import org.mediawiki.api.json.ApiResult; -import org.wikipedia.Constants; -import org.wikipedia.dataclient.WikiSite; -import org.wikipedia.search.FullSearchArticlesTask; -import org.wikipedia.search.SearchResults; - -/** - * Task for getting suggestions for further reading. - * Currently powered by full-text search based on the given page title. - */ -public class SuggestionsTask extends FullSearchArticlesTask { - private final String title; - private final boolean requireThumbnail; - - public SuggestionsTask(Api api, WikiSite wiki, String title, boolean requireThumbnail) { - super(api, wiki, title, Constants.MAX_SUGGESTION_RESULTS * 2, null, true); - // request double the results wanted since we may filter some out. todo: change the api - // request to do some filtering - this.title = title; - this.requireThumbnail = requireThumbnail; - } - - @Override - public SearchResults processResult(final ApiResult result) throws Throwable { - return SearchResults.filter(super.processResult(result), title, requireThumbnail); - } -} diff --git a/app/src/main/java/org/wikipedia/page/bottomcontent/BottomContentHandler.java b/app/src/main/java/org/wikipedia/page/bottomcontent/BottomContentHandler.java index 816239e..70a24ae 100644 --- a/app/src/main/java/org/wikipedia/page/bottomcontent/BottomContentHandler.java +++ b/app/src/main/java/org/wikipedia/page/bottomcontent/BottomContentHandler.java @@ -28,6 +28,7 @@ import org.wikipedia.WikipediaApp; import org.wikipedia.analytics.SuggestedPagesFunnel; import org.wikipedia.bridge.CommunicationBridge; +import org.wikipedia.dataclient.mwapi.MwQueryResponse; import org.wikipedia.history.HistoryEntry; import org.wikipedia.page.LinkHandler; import org.wikipedia.page.LinkMovementMethodExt; @@ -35,7 +36,7 @@ import org.wikipedia.page.PageContainerLongPressHandler; import org.wikipedia.page.PageFragment; import org.wikipedia.page.PageTitle; -import org.wikipedia.page.SuggestionsTask; +import org.wikipedia.page.SuggestionsClient; import org.wikipedia.search.SearchResult; import org.wikipedia.search.SearchResults; import org.wikipedia.util.DimenUtil; @@ -48,13 +49,15 @@ import java.util.List; +import retrofit2.Call; + import static org.wikipedia.util.L10nUtil.formatDateRelative; import static org.wikipedia.util.L10nUtil.getStringForArticleLanguage; import static org.wikipedia.util.UriUtil.visitInExternalBrowser; public class BottomContentHandler implements BottomContentInterface, - ObservableWebView.OnScrollChangeListener, - ObservableWebView.OnContentHeightChangedListener { + ObservableWebView.OnScrollChangeListener, + ObservableWebView.OnContentHeightChangedListener { private static final String TAG = "BottomContentHandler"; private final PageFragment parentFragment; @@ -131,8 +134,7 @@ case MotionEvent.ACTION_MOVE: if (isPressed && !doingSlopEvent) { int contentHeight = (int)(webView.getContentHeight() * DimenUtil.getDensityScalar()); - int maxScroll = contentHeight - webView.getScrollY() - - webView.getHeight(); + int maxScroll = contentHeight - webView.getScrollY() - webView.getHeight(); int scrollAmount = Math.min((int) (startY - event.getY()), maxScroll); // manually scroll the WebView that's underneath us... webView.scrollBy(0, scrollAmount); @@ -323,31 +325,33 @@ return; } final long timeMillis = System.currentTimeMillis(); - new SuggestionsTask(app.getAPIForSite(entry.getTitle().getWikiSite()), - entry.getTitle().getWikiSite(), entry.getTitle().getPrefixedText(), false) { - @Override - public void onFinish(SearchResults results) { - funnel.setLatency(System.currentTimeMillis() - timeMillis); - readMoreItems = results; - if (!readMoreItems.getResults().isEmpty()) { - // If there are results, set up section and make sure it's visible - setUpReadMoreSection(layoutInflater, readMoreItems); - showReadMore(); - } else { - // If there's no results, just hide the section - hideReadMore(); - } - layoutContent(); - } + new SuggestionsClient().request(entry.getTitle().getWikiSite(), entry.getTitle(), + true, true, new SuggestionsClient.Callback() { + @Override + public void success(@NonNull Call<MwQueryResponse<SuggestionsClient.QueryResult>> call, + @NonNull SearchResults results) { + funnel.setLatency(System.currentTimeMillis() - timeMillis); + readMoreItems = results; + if (!readMoreItems.getResults().isEmpty()) { + // If there are results, set up section and make sure it's visible + setUpReadMoreSection(layoutInflater, readMoreItems); + showReadMore(); + } else { + // If there's no results, just hide the section + hideReadMore(); + } + layoutContent(); + } - @Override - public void onCatch(Throwable caught) { - // Read More titles are expendable. - Log.w(TAG, "Error while fetching Read More titles.", caught); - // but lay out the bottom content anyway: - layoutContent(); - } - }.execute(); + @Override + public void failure(@NonNull Call<MwQueryResponse<SuggestionsClient.QueryResult>> call, + @NonNull Throwable caught) { + // Read More titles are expendable. + Log.w(TAG, "Error while fetching Read More titles.", caught); + // but lay out the bottom content anyway: + layoutContent(); + } + }); } private void hideReadMore() { @@ -358,7 +362,7 @@ if (parentFragment.isAdded()) { ((ConfigurableTextView) readMoreContainer.findViewById(R.id.read_more_header)) .setText(getStringForArticleLanguage(parentFragment.getTitle(), R.string.read_more_section), - pageTitle.getWikiSite().languageCode()); + pageTitle.getWikiSite().languageCode()); } readMoreContainer.setVisibility(View.VISIBLE); } diff --git a/app/src/test/java/org/wikipedia/page/SuggestionTest.java b/app/src/test/java/org/wikipedia/page/SuggestionTest.java new file mode 100644 index 0000000..e9f66d1 --- /dev/null +++ b/app/src/test/java/org/wikipedia/page/SuggestionTest.java @@ -0,0 +1,98 @@ +package org.wikipedia.page; + +import android.support.annotation.NonNull; + +import com.google.gson.stream.MalformedJsonException; + +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.wikipedia.Constants; +import org.wikipedia.dataclient.WikiSite; +import org.wikipedia.dataclient.mwapi.MwException; +import org.wikipedia.dataclient.mwapi.MwQueryResponse; +import org.wikipedia.dataclient.okhttp.HttpStatusException; +import org.wikipedia.search.SearchResults; +import org.wikipedia.test.MockWebServerTest; + +import retrofit2.Call; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Matchers.isA; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +public class SuggestionTest extends MockWebServerTest { + private static final WikiSite WIKISITE_TEST = WikiSite.forLanguageCode("test"); + private static final PageTitle PAGE_TITLE_MARK_SELBY = new PageTitle("French presidential election, 2017", WIKISITE_TEST); + + @NonNull private final SuggestionsClient subject = new SuggestionsClient(); + + @Test public void testRequestSuccess() throws Throwable { + enqueueFromFile("suggestion.json"); + + SuggestionsClient.Callback cb = mock(SuggestionsClient.Callback.class); + Call<MwQueryResponse<SuggestionsClient.QueryResult>> call = request(cb); + + server().takeRequest(); + ArgumentCaptor<SearchResults> captor = ArgumentCaptor.forClass(SearchResults.class); + verify(cb).success(eq(call), captor.capture()); + + SearchResults searchResults = captor.getValue(); + + assertThat(searchResults, notNullValue()); + assertThat(searchResults.getResults().size(), is(Constants.MAX_SUGGESTION_RESULTS)); + + assertThat(searchResults.getResults().get(0).getPageTitle().getDisplayText(), is("French Socialist Party presidential primary, 2017")); + assertThat(searchResults.getResults().get(1).getPageTitle().getDisplayText(), is("French legislative election, 2017")); + assertThat(searchResults.getResults().get(2).getPageTitle().getDisplayText(), is("Jean-Luc Mélenchon")); + } + + @Test public void testRequestResponseApiError() throws Throwable { + enqueueFromFile("api_error.json"); + + SuggestionsClient.Callback cb = mock(SuggestionsClient.Callback.class); + Call<MwQueryResponse<SuggestionsClient.QueryResult>> call = request(cb); + + server().takeRequest(); + assertCallbackFailure(call, cb, MwException.class); + } + + @Test public void testRequestResponseFailure() throws Throwable { + enqueue404(); + + SuggestionsClient.Callback cb = mock(SuggestionsClient.Callback.class); + Call<MwQueryResponse<SuggestionsClient.QueryResult>> call = request(cb); + + server().takeRequest(); + assertCallbackFailure(call, cb, HttpStatusException.class); + } + + @Test public void testRequestResponseMalformed() throws Throwable { + server().enqueue("'"); + + SuggestionsClient.Callback cb = mock(SuggestionsClient.Callback.class); + Call<MwQueryResponse<SuggestionsClient.QueryResult>> call = request(cb); + + server().takeRequest(); + assertCallbackFailure(call, cb, MalformedJsonException.class); + } + + private void assertCallbackFailure(@NonNull Call<MwQueryResponse<SuggestionsClient.QueryResult>> call, + @NonNull SuggestionsClient.Callback cb, + @NonNull Class<? extends Throwable> throwable) { + //noinspection unchecked + verify(cb, never()).success(any(Call.class), any(SearchResults.class)); + verify(cb).failure(eq(call), isA(throwable)); + } + + private Call<MwQueryResponse<SuggestionsClient.QueryResult>> request( + @NonNull SuggestionsClient.Callback cb) { + return subject.request(WIKISITE_TEST, service(SuggestionsClient.Service.class), PAGE_TITLE_MARK_SELBY, true, true, cb); + } + +} diff --git a/app/src/test/res/raw/suggestion.json b/app/src/test/res/raw/suggestion.json new file mode 100644 index 0000000..020fbc0 --- /dev/null +++ b/app/src/test/res/raw/suggestion.json @@ -0,0 +1,77 @@ +{ + "batchcomplete":true, + "continue":{ + "gsroffset":6, + "continue":"gsroffset||" + }, + "query": { + "pages": [ + { + "pageid":20473979, + "ns":0, + "title":"François de Rugy", + "index":6, + "terms": { + "description": [ + "French politician" + ] + }, + "thumbnail": { + "source":"https:\/\/upload.wikimedia.org\/wikipedia\/commons\/thumb\/3\/39\/Fran%C3%A7ois_De_Rugy_-_Assembl%C3%A9e_Nationale_-_20111025-02_%28cropped%29.jpg\/239px-Fran%C3%A7ois_De_Rugy_-_Assembl%C3%A9e_Nationale_-_20111025-02_%28cropped%29.jpg", + "width":239, + "height":320 + } + }, + { + "pageid":50872696, + "ns":0, + "title":"French Socialist Party presidential primary, 2017", + "index":2, + "thumbnail":{ + "source":"https:\/\/upload.wikimedia.org\/wikipedia\/commons\/thumb\/3\/35\/Benoit_Hamon_%2823761616132%29_%28cropped%29.jpg\/229px-Benoit_Hamon_%2823761616132%29_%28cropped%29.jpg", + "width":229, + "height":320 + } + }, + { + "pageid":48815336, + "ns":0, + "title":"French legislative election, 2017", + "index":3, + "thumbnail":{ + "source":"https:\/\/upload.wikimedia.org\/wikipedia\/commons\/thumb\/9\/9a\/National_Assembly_of_France_2012.svg\/320px-National_Assembly_of_France_2012.svg.png", + "width":320, + "height":181 + } + }, + { + "pageid":10893787, + "ns":0, + "title":"French presidential debates", + "index":1 + }, + { + "pageid":20298042, + "ns":0, + "title":"Jean-Luc Mélenchon", + "index":5, + "terms":{ + "description":[ + "French politician and national Minister of Vocational Education from 2000 to 2002" + ] + }, + "thumbnail":{ + "source":"https:\/\/upload.wikimedia.org\/wikipedia\/commons\/thumb\/a\/a7\/Meeting_M%C3%A9lenchon_Toulouse_-_2017-04-16_-_Jean-Luc_M%C3%A9lenchon_-_41_%28cropped_2%29.jpg\/225px-Meeting_M%C3%A9lenchon_Toulouse_-_2017-04-16_-_Jean-Luc_M%C3%A9lenchon_-_41_%28cropped_2%29.jpg", + "width":225, + "height":320 + } + }, + { + "pageid":39150978, + "ns":0, + "title":"Opinion polling for the French presidential election, 2017", + "index":4 + } + ] + } +} \ No newline at end of file -- To view, visit https://gerrit.wikimedia.org/r/353384 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I4bde23e10666ceec5eee105529e5646c14c299bc Gerrit-PatchSet: 1 Gerrit-Project: apps/android/wikipedia Gerrit-Branch: master Gerrit-Owner: Jcasariego <jorgek...@gmail.com> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits