Set Media legend for wikidata entity (#3838)

* Set media legends and P18

* Minor

* Make media legends work

* Add test cases

* Use statement partial

* With minor refactoring

* Fix build
This commit is contained in:
Vivek Maskara 2020-06-30 05:11:27 -07:00 committed by GitHub
parent 7caf73fb4b
commit f26784e9c3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 195 additions and 199 deletions

View file

@ -76,7 +76,7 @@ dependencies {
testImplementation "org.powermock:powermock-api-mockito2:2.0.0-beta.5"
// Unit testing
testImplementation 'junit:junit:4.12'
testImplementation 'junit:junit:4.13'
testImplementation 'org.robolectric:robolectric:4.3'
testImplementation 'androidx.test:core:1.2.0'
testImplementation 'com.squareup.okhttp3:mockwebserver:3.12.1'

View file

@ -31,6 +31,7 @@ import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.media.MediaClient;
import fr.free.nrw.commons.utils.DialogUtil;
import fr.free.nrw.commons.wikidata.WikidataEditService;
import java.util.Locale;
import javax.inject.Inject;
import javax.inject.Named;
@ -41,7 +42,8 @@ import org.wikipedia.dataclient.WikiSite;
*/
public class ContributionsListFragment extends CommonsDaggerSupportFragment implements
ContributionsListContract.View, ContributionsListAdapter.Callback, WikipediaInstructionsDialogFragment.Callback {
ContributionsListContract.View, ContributionsListAdapter.Callback,
WikipediaInstructionsDialogFragment.Callback {
private static final String RV_STATE = "rv_scroll_state";
@ -275,7 +277,6 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
}
public Media getMediaAtPosition(final int i) {
return adapter.getContributionForPosition(i);
}
@ -291,12 +292,13 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
*/
@Override
public void onConfirmClicked(@Nullable Contribution contribution, boolean copyWikicode) {
if(copyWikicode) {
if (copyWikicode) {
String wikicode = contribution.getWikiCode();
Utils.copy("wikicode", wikicode, getContext());
}
final String url = languageWikipediaSite.mobileUrl() + "/wiki/" + contribution.getWikidataPlace()
final String url =
languageWikipediaSite.mobileUrl() + "/wiki/" + contribution.getWikidataPlace()
.getWikipediaPageTitle();
Utils.handleWebUrl(getContext(), Uri.parse(url));
}

View file

@ -21,24 +21,16 @@ import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.di.CommonsApplicationModule;
import fr.free.nrw.commons.di.CommonsDaggerService;
import fr.free.nrw.commons.media.MediaClient;
import fr.free.nrw.commons.utils.CommonsDateUtil;
import fr.free.nrw.commons.wikidata.WikidataEditService;
import io.reactivex.Completable;
import io.reactivex.Observable;
import io.reactivex.Scheduler;
import io.reactivex.Single;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Action;
import io.reactivex.functions.Consumer;
import io.reactivex.processors.PublishProcessor;
import io.reactivex.schedulers.Schedulers;
import java.io.File;
import java.io.IOException;
import java.text.ParseException;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.inject.Inject;
@ -313,7 +305,7 @@ public class UploadService extends CommonsDaggerService {
.add(wikidataEditService.addDepictionsAndCaptions(uploadResult, contribution));
WikidataPlace wikidataPlace = contribution.getWikidataPlace();
if (wikidataPlace != null && wikidataPlace.getImageValue() == null) {
wikidataEditService.createImageClaim(wikidataPlace, uploadResult);
wikidataEditService.createClaim(wikidataPlace, uploadResult.getFilename(), contribution.getCaptions());
}
saveCompletedContribution(contribution, uploadResult);
}

View file

@ -1,6 +1,6 @@
package fr.free.nrw.commons.wikidata;
import fr.free.nrw.commons.upload.WikidataItem;
import com.google.gson.Gson;
import org.jetbrains.annotations.NotNull;
import javax.inject.Inject;
@ -9,64 +9,38 @@ import javax.inject.Singleton;
import fr.free.nrw.commons.wikidata.model.AddEditTagResponse;
import io.reactivex.Observable;
import io.reactivex.ObservableSource;
import okhttp3.MediaType;
import okhttp3.RequestBody;
import org.wikipedia.wikidata.Statement_partial;
@Singleton
public class WikidataClient {
private final WikidataInterface wikidataInterface;
private final Gson gson;
@Inject
public WikidataClient(WikidataInterface wikidataInterface) {
public WikidataClient(WikidataInterface wikidataInterface, final Gson gson) {
this.wikidataInterface = wikidataInterface;
this.gson = gson;
}
/**
* Create wikidata claim to add P18 value
* @param entity wikidata entity ID
* @param value value of the P18 edit
*
* @return revisionID of the edit
*/
Observable<Long> createImageClaim(WikidataItem entity, String value) {
Observable<Long> setClaim(Statement_partial claim, String tags) {
return getCsrfToken()
.flatMap(csrfToken -> wikidataInterface.postCreateClaim(
toRequestBody(entity.getId()),
toRequestBody("value"),
toRequestBody(WikidataProperties.IMAGE.getPropertyName()),
toRequestBody(value),
toRequestBody("en"),
toRequestBody(csrfToken)))
.flatMap(csrfToken -> wikidataInterface.postSetClaim(gson.toJson(claim), tags, csrfToken))
.map(mwPostResponse -> mwPostResponse.getPageinfo().getLastrevid());
}
/**
* Converts string value to RequestBody for multipart request
*/
private RequestBody toRequestBody(String value) {
return RequestBody.create(MediaType.parse("text/plain"), value);
}
/**
* Get csrf token for wikidata edit
*/
@NotNull
private Observable<String> getCsrfToken() {
return wikidataInterface.getCsrfToken().map(mwQueryResponse -> mwQueryResponse.query().csrfToken());
}
/**
* Add edit tag for a given revision ID. The app currently uses this to tag P18 edits
* @param revisionId revision ID of the page edited
* @param tag to be added
* @param reason to be mentioned
*/
ObservableSource<AddEditTagResponse> addEditTag(Long revisionId, String tag, String reason) {
return getCsrfToken()
.flatMap(csrfToken -> wikidataInterface.addEditTag(String.valueOf(revisionId),
tag,
reason,
csrfToken));
return wikidataInterface.getCsrfToken()
.map(mwQueryResponse -> mwQueryResponse.query().csrfToken());
}
}

View file

@ -20,24 +20,33 @@ import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import org.wikipedia.dataclient.mwapi.MwPostResponse;
import org.wikipedia.wikidata.DataValue;
import org.wikipedia.wikidata.DataValue.ValueString;
import org.wikipedia.wikidata.EditClaim;
import org.wikipedia.wikidata.Snak_partial;
import org.wikipedia.wikidata.Statement_partial;
import org.wikipedia.wikidata.WikiBaseMonolingualTextValue;
import timber.log.Timber;
/**
* This class is meant to handle the Wikidata edits made through the app
* It will talk with MediaWiki Apis to make the necessary calls, log the edits and fire listeners
* on successful edits
* This class is meant to handle the Wikidata edits made through the app It will talk with MediaWiki
* Apis to make the necessary calls, log the edits and fire listeners on successful edits
*/
@Singleton
public class WikidataEditService {
private static final String COMMONS_APP_TAG = "wikimedia-commons-app";
private static final String COMMONS_APP_EDIT_REASON = "Add tag for edits made using Android Commons app";
public static final String COMMONS_APP_TAG = "wikimedia-commons-app";
private final Context context;
private final WikidataEditListener wikidataEditListener;
@ -61,8 +70,8 @@ public class WikidataEditService {
}
/**
* Edits the wikibase entity by adding DEPICTS property.
* Adding DEPICTS property requires call to the wikibase API to set tag against the entity.
* Edits the wikibase entity by adding DEPICTS property. Adding DEPICTS property requires call to
* the wikibase API to set tag against the entity.
*/
@SuppressLint("CheckResult")
private Observable<Boolean> addDepictsProperty(final String fileEntityId,
@ -81,7 +90,7 @@ public class WikidataEditService {
Timber.d("Unable to set DEPICTS property for %s", fileEntityId);
}
})
.doOnError( throwable -> {
.doOnError(throwable -> {
Timber.e(throwable, "Error occurred while setting DEPICTS property");
ViewUtil.showLongToast(context, throwable.toString());
})
@ -97,12 +106,14 @@ public class WikidataEditService {
*/
private void showSuccessToast(final String wikiItemName) {
final String successStringTemplate = context.getString(R.string.successful_wikidata_edit);
final String successMessage = String.format(Locale.getDefault(), successStringTemplate, wikiItemName);
final String successMessage = String
.format(Locale.getDefault(), successStringTemplate, wikiItemName);
ViewUtil.showLongToast(context, successMessage);
}
/**
* Adds label to Wikidata using the fileEntityId and the edit token, obtained from csrfTokenClient
* Adds label to Wikidata using the fileEntityId and the edit token, obtained from
* csrfTokenClient
*
* @param fileEntityId
* @return
@ -112,7 +123,7 @@ public class WikidataEditService {
private Observable<Boolean> addCaption(final long fileEntityId, final String languageCode,
final String captionValue) {
return wikiBaseClient.addLabelstoWikidata(fileEntityId, languageCode, captionValue)
.doOnNext(mwPostResponse -> onAddCaptionResponse(fileEntityId, mwPostResponse) )
.doOnNext(mwPostResponse -> onAddCaptionResponse(fileEntityId, mwPostResponse))
.doOnError(throwable -> {
Timber.e(throwable, "Error occurred while setting Captions");
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
@ -128,29 +139,41 @@ public class WikidataEditService {
}
}
public void createImageClaim(@Nullable final WikidataPlace wikidataPlace, final UploadResult imageUpload) {
public void createClaim(@Nullable final WikidataPlace wikidataPlace, final String fileName, final
Map<String, String> captions) {
if (!(directKvStore.getBoolean("Picture_Has_Correct_Location", true))) {
Timber.d("Image location and nearby place location mismatched, so Wikidata item won't be edited");
Timber
.d("Image location and nearby place location mismatched, so Wikidata item won't be edited");
return;
}
editWikidataImageProperty(wikidataPlace, imageUpload);
addImageAndMediaLegends(wikidataPlace, fileName, captions);
}
@SuppressLint("CheckResult")
private void editWikidataImageProperty(final WikidataItem wikidataItem, final UploadResult imageUpload) {
wikidataClient.createImageClaim(wikidataItem, String.format("\"%s\"", imageUpload.getFilename()))
.flatMap(revisionId -> {
if (revisionId != -1) {
return wikidataClient.addEditTag(revisionId, COMMONS_APP_TAG, COMMONS_APP_EDIT_REASON);
public void addImageAndMediaLegends(final WikidataItem wikidataItem, final String fileName,
final Map<String, String> captions) {
final Snak_partial p18 = new Snak_partial("value", WikidataProperties.IMAGE.getPropertyName(),
new ValueString(fileName.replace("File:", "")));
final List<Snak_partial> snaks = new ArrayList<>();
for (final Map.Entry<String, String> entry : captions.entrySet()) {
snaks.add(new Snak_partial("value",
WikidataProperties.MEDIA_LEGENDS.getPropertyName(), new DataValue.MonoLingualText(
new WikiBaseMonolingualTextValue(entry.getValue(), entry.getKey()))));
}
throw new RuntimeException("Unable to edit wikidata item");
})
.subscribeOn(Schedulers.io())
final String id = wikidataItem.getId() + "$" + UUID.randomUUID().toString();
final Statement_partial claim = new Statement_partial(p18, "statement", "normal", id,
Collections.singletonMap(WikidataProperties.MEDIA_LEGENDS.getPropertyName(), snaks),
Arrays.asList(WikidataProperties.MEDIA_LEGENDS.getPropertyName()));
wikidataClient.setClaim(claim, COMMONS_APP_TAG).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(revisionId -> handleImageClaimResult(wikidataItem, String.valueOf(revisionId)), throwable -> {
.subscribe(revisionId -> handleImageClaimResult(wikidataItem, String.valueOf(revisionId)),
throwable -> {
Timber.e(throwable, "Error occurred while making claim");
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
});
;
}
private void handleImageClaimResult(final WikidataItem wikidataItem, final String revisionId) {
@ -202,6 +225,6 @@ public class WikidataEditService {
depictedItems.add(wikidataPlace);
}
return Observable.fromIterable(depictedItems)
.concatMap( wikidataItem -> addDepictsProperty(fileEntityId.toString(), wikidataItem));
.concatMap(wikidataItem -> addDepictsProperty(fileEntityId.toString(), wikidataItem));
}
}

View file

@ -2,6 +2,7 @@ package fr.free.nrw.commons.wikidata;
import androidx.annotation.NonNull;
import com.google.gson.JsonObject;
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
import fr.free.nrw.commons.wikidata.model.AddEditTagResponse;
@ -20,30 +21,6 @@ import static org.wikipedia.dataclient.Service.MW_API_PREFIX;
public interface WikidataInterface {
/**
* Wikidata create claim API. Posts a new claim for the given entity ID
*/
@Headers("Cache-Control: no-cache")
@POST("w/api.php?format=json&errorformat=plaintext&action=wbcreateclaim&errorlang=uselang")
@Multipart
Observable<WbCreateClaimResponse> postCreateClaim(@NonNull @Part("entity") RequestBody entity,
@NonNull @Part("snaktype") RequestBody snakType,
@NonNull @Part("property") RequestBody property,
@NonNull @Part("value") RequestBody value,
@NonNull @Part("uselang") RequestBody useLang,
@NonNull @Part("token") RequestBody token);
/**
* Add edit tag and reason for any revision
*/
@Headers("Cache-Control: no-cache")
@POST(MW_API_PREFIX + "action=tag")
@FormUrlEncoded
Observable<AddEditTagResponse> addEditTag(@NonNull @Field("revid") String revId,
@NonNull @Field("add") String tagName,
@NonNull @Field("reason") String reason,
@NonNull @Field("token") String token);
/**
* Get edit token for wikidata wiki site
*/
@ -51,4 +28,14 @@ public interface WikidataInterface {
@GET(MW_API_PREFIX + "action=query&meta=tokens&type=csrf")
@NonNull
Observable<MwQueryResponse> getCsrfToken();
/**
* Wikidata create claim API. Posts a new claim for the given entity ID
*/
@Headers("Cache-Control: no-cache")
@POST("w/api.php?format=json&action=wbsetclaim")
@FormUrlEncoded
Observable<WbCreateClaimResponse> postSetClaim(@NonNull @Field("claim") String request,
@NonNull @Field("tags") String tags,
@NonNull @Field("token") String token);
}

View file

@ -6,5 +6,6 @@ enum class WikidataProperties(val propertyName: String) {
IMAGE("P18"),
DEPICTS(BuildConfig.DEPICTS_PROPERTY),
COMMONS_CATEGORY("P373"),
INSTANCE_OF("P31");
INSTANCE_OF("P31"),
MEDIA_LEGENDS("P2096");
}

View file

@ -1,7 +1,9 @@
package fr.free.nrw.commons.wikidata
import com.nhaarman.mockitokotlin2.mock
import fr.free.nrw.commons.wikidata.model.AddEditTagResponse
import com.google.gson.Gson
import com.nhaarman.mockitokotlin2.whenever
import fr.free.nrw.commons.wikidata.model.PageInfo
import fr.free.nrw.commons.wikidata.model.WbCreateClaimResponse
import io.reactivex.Observable
import org.junit.Before
import org.junit.Test
@ -14,12 +16,16 @@ import org.mockito.Mockito.mock
import org.mockito.MockitoAnnotations
import org.wikipedia.dataclient.mwapi.MwQueryResponse
import org.wikipedia.dataclient.mwapi.MwQueryResult
import org.wikipedia.wikidata.Statement_partial
class WikidataClientTest {
@Mock
internal var wikidataInterface: WikidataInterface? = null
@Mock
internal var gson: Gson? = null
@InjectMocks
var wikidataClient: WikidataClient? = null
@ -35,26 +41,18 @@ class WikidataClientTest {
.thenReturn(Observable.just(mwQueryResponse))
}
@Test
fun createClaim() {
`when`(
wikidataInterface!!.postCreateClaim(
any(),
any(),
any(),
any(),
any(),
any()
)
)
.thenReturn(Observable.just(mock()))
wikidataClient!!.createImageClaim(mock(), "test.jpg")
}
@Test
fun addEditTag() {
`when`(wikidataInterface!!.addEditTag(anyString(), anyString(), anyString(), anyString()))
.thenReturn(Observable.just(mock(AddEditTagResponse::class.java)))
wikidataClient!!.addEditTag(1L, "test", "test")
val response = mock(WbCreateClaimResponse::class.java)
val pageInfo = mock(PageInfo::class.java)
whenever(pageInfo.lastrevid).thenReturn(1)
whenever(response.pageinfo).thenReturn(pageInfo)
`when`(wikidataInterface!!.postSetClaim(anyString(), anyString(), anyString()))
.thenReturn(Observable.just(response))
whenever(gson!!.toJson(any(Statement_partial::class.java))).thenReturn("claim")
val request = mock(Statement_partial::class.java)
val claim = wikidataClient!!.setClaim(request, "test").test()
.assertValue(1L)
}
}

View file

@ -1,13 +1,13 @@
package fr.free.nrw.commons.wikidata
import android.content.Context
import com.google.gson.Gson
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.verifyZeroInteractions
import com.nhaarman.mockitokotlin2.whenever
import fr.free.nrw.commons.kvstore.JsonKvStore
import fr.free.nrw.commons.upload.UploadResult
import fr.free.nrw.commons.upload.WikidataPlace
import fr.free.nrw.commons.wikidata.model.AddEditTagResponse
import io.reactivex.Observable
import org.junit.Before
import org.junit.Test
@ -15,7 +15,6 @@ import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.anyString
import org.mockito.InjectMocks
import org.mockito.Mock
import org.mockito.Mockito.*
import org.mockito.MockitoAnnotations
class WikidataEditServiceTest {
@ -31,6 +30,9 @@ class WikidataEditServiceTest {
@Mock
internal lateinit var wikibaseClient: WikiBaseClient
@Mock
internal lateinit var gson: Gson
@InjectMocks
lateinit var wikidataEditService: WikidataEditService
@ -44,7 +46,7 @@ class WikidataEditServiceTest {
fun noClaimsWhenLocationIsNotCorrect() {
whenever(directKvStore.getBoolean("Picture_Has_Correct_Location", true))
.thenReturn(false)
wikidataEditService.createImageClaim(mock(), mock())
wikidataEditService.createClaim(mock(), "Test.jpg", hashMapOf())
verifyZeroInteractions(wikidataClient)
}
@ -52,15 +54,16 @@ class WikidataEditServiceTest {
fun createImageClaim() {
whenever(directKvStore.getBoolean("Picture_Has_Correct_Location", true))
.thenReturn(true)
whenever(wikidataClient.createImageClaim(any(), any()))
.thenReturn(Observable.just(1L))
whenever(wikidataClient.addEditTag(anyLong(), anyString(), anyString()))
.thenReturn(Observable.just(mock(AddEditTagResponse::class.java)))
whenever(wikibaseClient.getFileEntityId(any())).thenReturn(Observable.just(1L))
val wikidataPlace:WikidataPlace = mock()
whenever(wikidataClient.setClaim(any(), anyString()))
.thenReturn(Observable.just(1L))
val wikidataPlace: WikidataPlace = mock()
val uploadResult = mock<UploadResult>()
whenever(uploadResult.filename).thenReturn("file")
wikidataEditService.createImageClaim(wikidataPlace, uploadResult)
verify(wikidataClient, times(1)).createImageClaim(wikidataPlace, """"file"""")
wikidataEditService.createClaim(
wikidataPlace,
uploadResult.filename,
hashMapOf<String, String>()
)
}
}

View file

@ -12,7 +12,7 @@ sealed class DataValue(val type: String) {
.registerSubtype(GlobeCoordinate_partial::class.java, GlobeCoordinate_partial.TYPE)
.registerSubtype(Time_partial::class.java, Time_partial.TYPE)
.registerSubtype(Quantity_partial::class.java, Quantity_partial.TYPE)
.registerSubtype(MonoLingualText_partial::class.java, MonoLingualText_partial.TYPE)
.registerSubtype(MonoLingualText::class.java, MonoLingualText.TYPE)
}
// "value": {
@ -87,7 +87,7 @@ sealed class DataValue(val type: String) {
// "language": "ko"
// }
// }
class MonoLingualText_partial() : DataValue(TYPE) {
class MonoLingualText(val value: WikiBaseMonolingualTextValue) : DataValue(TYPE) {
companion object {
const val TYPE = "monolingualtext"
}

View file

@ -21,5 +21,8 @@ import com.google.gson.annotations.SerializedName
data class Statement_partial(
@SerializedName("mainsnak") val mainSnak: Snak_partial,
val type: String,
val rank: String
val rank: String,
val id: String? = null,
val qualifiers: Map<String, List<Snak_partial>> = mapOf(),
@SerializedName("qualifiers-order") val qualifiersOrder: List<String> = listOf()
)

View file

@ -0,0 +1,13 @@
package org.wikipedia.wikidata
import com.google.gson.annotations.SerializedName
/*"value": {
"type": "monolingualtext",
"value": {
"text": "some value",
"language": "en"
}
}*/
data class WikiBaseMonolingualTextValue(val text: String, val language: String)