Merge branch 'master' into macgills/3847-convert-media-and-contribution

This commit is contained in:
Vivek Maskara 2020-06-30 12:35:11 -07:00 committed by GitHub
commit f6267577f4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 575 additions and 302 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

@ -14,7 +14,7 @@ import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.rule.ActivityTestRule
import androidx.test.runner.AndroidJUnit4
import fr.free.nrw.commons.utils.ConfigUtils
import fr.free.nrw.commons.utils.ConfigUtils.getVersionNameWithSha
import org.hamcrest.CoreMatchers
import org.junit.Before
import org.junit.Rule
@ -36,7 +36,9 @@ class AboutActivityTest {
@Test
fun testBuildNumber() {
Espresso.onView(ViewMatchers.withId(R.id.about_version))
.check(ViewAssertions.matches(withText(ConfigUtils.getVersionNameWithSha(getApplicationContext()))))
.check(ViewAssertions.matches(
withText(getApplicationContext<CommonsApplication>().getVersionNameWithSha())
))
}
@Test

View file

@ -26,7 +26,7 @@ import androidx.test.rule.ActivityTestRule
import androidx.test.rule.GrantPermissionRule
import androidx.test.runner.AndroidJUnit4
import fr.free.nrw.commons.auth.LoginActivity
import fr.free.nrw.commons.upload.DescriptionsAdapter
import fr.free.nrw.commons.upload.UploadMediaDetailAdapter
import fr.free.nrw.commons.util.MyViewAction
import fr.free.nrw.commons.utils.ConfigUtils
import org.hamcrest.core.AllOf.allOf
@ -78,7 +78,7 @@ class UploadTest {
@Test
fun testUploadWithDescription() {
if (!ConfigUtils.isBetaFlavour()) {
if (!ConfigUtils.isBetaFlavour) {
throw Error("This test should only be run in Beta!")
}
@ -96,7 +96,7 @@ class UploadTest {
// Try to dismiss the error, if there is one (probably about duplicate files on Commons)
dismissWarning("Yes")
onView(allOf<View>(isDisplayed(), withId(R.id.et_title)))
onView(allOf<View>(isDisplayed(), withId(R.id.tv_title)))
.perform(replaceText(commonsFileName))
onView(allOf<View>(isDisplayed(), withId(R.id.description_item_edit_text)))
@ -150,7 +150,7 @@ class UploadTest {
@Test
fun testUploadWithoutDescription() {
if (!ConfigUtils.isBetaFlavour()) {
if (!ConfigUtils.isBetaFlavour) {
throw Error("This test should only be run in Beta!")
}
@ -168,7 +168,7 @@ class UploadTest {
// Try to dismiss the error, if there is one (probably about duplicate files on Commons)
dismissWarning("Yes")
onView(allOf<View>(isDisplayed(), withId(R.id.et_title)))
onView(allOf<View>(isDisplayed(), withId(R.id.tv_title)))
.perform(replaceText(commonsFileName))
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
@ -209,7 +209,7 @@ class UploadTest {
@Test
fun testUploadWithMultilingualDescription() {
if (!ConfigUtils.isBetaFlavour()) {
if (!ConfigUtils.isBetaFlavour) {
throw Error("This test should only be run in Beta!")
}
@ -227,12 +227,12 @@ class UploadTest {
// Try to dismiss the error, if there is one (probably about duplicate files on Commons)
dismissWarningDialog()
onView(allOf<View>(isDisplayed(), withId(R.id.et_title)))
onView(allOf<View>(isDisplayed(), withId(R.id.tv_title)))
.perform(replaceText(commonsFileName))
onView(withId(R.id.rv_descriptions)).perform(
RecyclerViewActions
.actionOnItemAtPosition<DescriptionsAdapter.ViewHolder>(0,
.actionOnItemAtPosition<UploadMediaDetailAdapter.ViewHolder>(0,
MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Test description")))
onView(withId(R.id.btn_add_description))
@ -240,12 +240,12 @@ class UploadTest {
onView(withId(R.id.rv_descriptions)).perform(
RecyclerViewActions
.actionOnItemAtPosition<DescriptionsAdapter.ViewHolder>(1,
.actionOnItemAtPosition<UploadMediaDetailAdapter.ViewHolder>(1,
MyViewAction.selectSpinnerItemInChildViewWithId(R.id.spinner_description_languages, 2)))
onView(withId(R.id.rv_descriptions)).perform(
RecyclerViewActions
.actionOnItemAtPosition<DescriptionsAdapter.ViewHolder>(1,
.actionOnItemAtPosition<UploadMediaDetailAdapter.ViewHolder>(1,
MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Description")))
onView(allOf(isDisplayed(), withId(R.id.btn_next)))

View file

@ -23,7 +23,7 @@ class WelcomeActivityTest {
@Test
fun ifBetaShowsSkipButton() {
if (ConfigUtils.isBetaFlavour()) {
if (ConfigUtils.isBetaFlavour) {
onView(withId(R.id.finishTutorialButton))
.check(matches(isDisplayed()))
}
@ -31,7 +31,7 @@ class WelcomeActivityTest {
@Test
fun ifProdHidesSkipButton() {
if (!ConfigUtils.isBetaFlavour()) {
if (!ConfigUtils.isBetaFlavour) {
onView(withId(R.id.finishTutorialButton))
.check(matches(not(isDisplayed())))
}
@ -39,7 +39,7 @@ class WelcomeActivityTest {
@Test
fun testBetaSkipButton() {
if (ConfigUtils.isBetaFlavour()) {
if (ConfigUtils.isBetaFlavour) {
onView(withId(R.id.finishTutorialButton))
.perform(ViewActions.click())
assert(activityRule.activity.isDestroyed)

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).getMedia();
}
@ -291,13 +292,14 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
*/
@Override
public void onConfirmClicked(@Nullable Contribution contribution, boolean copyWikicode) {
if(copyWikicode) {
String wikicode = contribution.getMedia().getWikiCode();
if (copyWikicode) {
String wikicode = contribution.getWikiCode();
Utils.copy("wikicode", wikicode, getContext());
}
final String url = languageWikipediaSite.mobileUrl() + "/wiki/" + contribution.getWikidataPlace()
.getWikipediaPageTitle();
final String url =
languageWikipediaSite.mobileUrl() + "/wiki/" + contribution.getWikidataPlace()
.getWikipediaPageTitle();
Utils.handleWebUrl(getContext(), Uri.parse(url));
}

View file

@ -127,7 +127,8 @@ public class MainActivity extends NavigationBaseActivity implements FragmentMana
tabLayout.getTabAt(1).setCustomView(nearbyTabLinearLayout);
nearbyInfo.setOnClickListener(view ->
new AlertDialog.Builder(MainActivity.this).setTitle(R.string.title_activity_nearby).setMessage(R.string.showcase_view_whole_nearby_activity)
new AlertDialog.Builder(MainActivity.this).setTitle(R.string.title_activity_nearby)
.setView(getLayoutInflater().inflate(R.layout.dialog_nearby, null))
.setCancelable(true)
.setPositiveButton(android.R.string.ok, (dialog, id) -> dialog.cancel())
.create()

View file

@ -12,7 +12,12 @@ import java.io.IOException
*
* @author Ashish Kumar
*/
class CountingRequestBody(protected var delegate: RequestBody, protected var listener: Listener) : RequestBody() {
class CountingRequestBody(
protected var delegate: RequestBody,
protected var listener: Listener,
var offset: Long,
var totalContentLength: Long
) : RequestBody() {
protected var countingSink: CountingSink? = null
override fun contentType(): MediaType? {
return delegate.contentType()
@ -37,11 +42,12 @@ class CountingRequestBody(protected var delegate: RequestBody, protected var lis
protected inner class CountingSink(delegate: Sink?) : ForwardingSink(delegate!!) {
private var bytesWritten: Long = 0
@Throws(IOException::class)
override fun write(source: Buffer, byteCount: Long) {
super.write(source, byteCount)
bytesWritten += byteCount
listener.onRequestProgress(bytesWritten, contentLength())
listener.onRequestProgress(offset + bytesWritten, totalContentLength)
}
}

View file

@ -1,33 +1,84 @@
package fr.free.nrw.commons.upload;
import android.content.Context;
import io.reactivex.Observable;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
import timber.log.Timber;
@Singleton
public class FileUtilsWrapper {
@Inject
public FileUtilsWrapper() {
@Inject
public FileUtilsWrapper() {
}
}
public String getFileExt(String fileName) {
return FileUtils.getFileExt(fileName);
}
public String getFileExt(String fileName) {
return FileUtils.getFileExt(fileName);
}
public String getSHA1(InputStream is) {
return FileUtils.getSHA1(is);
}
public String getSHA1(InputStream is) {
return FileUtils.getSHA1(is);
}
public FileInputStream getFileInputStream(String filePath) throws FileNotFoundException {
return FileUtils.getFileInputStream(filePath);
}
public FileInputStream getFileInputStream(String filePath) throws FileNotFoundException {
return FileUtils.getFileInputStream(filePath);
}
public String getGeolocationOfFile(String filePath) {
return FileUtils.getGeolocationOfFile(filePath);
public String getGeolocationOfFile(String filePath) {
return FileUtils.getGeolocationOfFile(filePath);
}
/**
* Takes a file as input and returns an Observable of files with the specified chunk size
*/
public Observable<File> getFileChunks(Context context, File file, final int chunkSize)
throws IOException {
final byte[] buffer = new byte[chunkSize];
//try-with-resources to ensure closing stream
try (final FileInputStream fis = new FileInputStream(file);
final BufferedInputStream bis = new BufferedInputStream(fis)) {
final List<File> buffers = new ArrayList<>();
int size;
while ((size = bis.read(buffer)) > 0) {
buffers.add(writeToFile(context, Arrays.copyOf(buffer, size), file.getName(),
getFileExt(file.getName())));
}
return Observable.fromIterable(buffers);
}
}
/**
* Create a temp file containing the passed byte data.
*/
private File writeToFile(Context context, final byte[] data, final String fileName,
String fileExtension)
throws IOException {
final File file = File.createTempFile(fileName, fileExtension, context.getCacheDir());
try {
if (!file.exists()) {
file.createNewFile();
}
final FileOutputStream fos = new FileOutputStream(file);
fos.write(data);
fos.close();
} catch (final Exception throwable) {
Timber.e(throwable, "Failed to create file");
}
return file;
}
}

View file

@ -4,11 +4,15 @@ import static fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_CSRF;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.Nullable;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.upload.UploadService.NotificationUpdateProgressListener;
import io.reactivex.Observable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicReference;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
@ -16,59 +20,112 @@ import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
import org.wikipedia.csrf.CsrfTokenClient;
import timber.log.Timber;
@Singleton
public class UploadClient {
private final UploadInterface uploadInterface;
private final CsrfTokenClient csrfTokenClient;
private final PageContentsCreator pageContentsCreator;
private final int CHUNK_SIZE = 256 * 1024; // 256 KB
@Inject
public UploadClient(UploadInterface uploadInterface,
@Named(NAMED_COMMONS_CSRF) CsrfTokenClient csrfTokenClient,
PageContentsCreator pageContentsCreator) {
this.uploadInterface = uploadInterface;
this.csrfTokenClient = csrfTokenClient;
this.pageContentsCreator = pageContentsCreator;
private final UploadInterface uploadInterface;
private final CsrfTokenClient csrfTokenClient;
private final PageContentsCreator pageContentsCreator;
private final FileUtilsWrapper fileUtilsWrapper;
@Inject
public UploadClient(final UploadInterface uploadInterface,
@Named(NAMED_COMMONS_CSRF) final CsrfTokenClient csrfTokenClient,
final PageContentsCreator pageContentsCreator,
final FileUtilsWrapper fileUtilsWrapper) {
this.uploadInterface = uploadInterface;
this.csrfTokenClient = csrfTokenClient;
this.pageContentsCreator = pageContentsCreator;
this.fileUtilsWrapper = fileUtilsWrapper;
}
/**
* Upload file to stash in chunks of specified size. Uploading files in chunks will make handling
* of large files easier. Also, it will be useful in supporting pause/resume of uploads
*/
Observable<UploadResult> uploadFileToStash(
final Context context, final String filename, final File file,
final NotificationUpdateProgressListener notificationUpdater) throws IOException {
final Observable<File> fileChunks = fileUtilsWrapper.getFileChunks(context, file, CHUNK_SIZE);
final MediaType mediaType = MediaType
.parse(FileUtils.getMimeType(context, Uri.parse(file.getPath())));
final long[] offset = {0};
final String[] fileKey = {null};
final AtomicReference<UploadResult> result = new AtomicReference<>();
fileChunks.blockingForEach(chunkFile -> {
final RequestBody requestBody = RequestBody
.create(mediaType, chunkFile);
final CountingRequestBody countingRequestBody = new CountingRequestBody(requestBody,
notificationUpdater::onProgress, offset[0], file.length());
uploadChunkToStash(filename,
file.length(),
offset[0],
fileKey[0],
countingRequestBody).blockingSubscribe(uploadResult -> {
result.set(uploadResult);
offset[0] = uploadResult.getOffset();
fileKey[0] = uploadResult.getFilekey();
});
});
return Observable.just(result.get());
}
/**
* Uploads a file chunk to stash
*
* @param filename The name of the file being uploaded
* @param fileSize The total size of the file
* @param offset The offset returned by the previous chunk upload
* @param fileKey The filekey returned by the previous chunk upload
* @param countingRequestBody Request body with chunk file
* @return
*/
Observable<UploadResult> uploadChunkToStash(final String filename,
final long fileSize,
final long offset,
final String fileKey,
final CountingRequestBody countingRequestBody) {
final MultipartBody.Part filePart = MultipartBody.Part
.createFormData("chunk", filename, countingRequestBody);
try {
return uploadInterface.uploadFileToStash(toRequestBody(filename),
toRequestBody(String.valueOf(fileSize)),
toRequestBody(String.valueOf(offset)),
toRequestBody(fileKey),
toRequestBody(csrfTokenClient.getTokenBlocking()),
filePart)
.map(UploadResponse::getUpload);
} catch (final Throwable throwable) {
Timber.e(throwable, "Failed to upload chunk to stash");
return Observable.error(throwable);
}
}
Observable<UploadResult> uploadFileToStash(Context context, String filename, File file,
NotificationUpdateProgressListener notificationUpdater) {
RequestBody requestBody = RequestBody
.create(MediaType.parse(FileUtils.getMimeType(context, Uri.parse(file.getPath()))), file);
@Nullable
private RequestBody toRequestBody(@Nullable final String value) {
return value == null ? null : RequestBody.create(okhttp3.MultipartBody.FORM, value);
}
CountingRequestBody countingRequestBody = new CountingRequestBody(requestBody,
(bytesWritten, contentLength) -> notificationUpdater
.onProgress(bytesWritten, contentLength));
MultipartBody.Part filePart = MultipartBody.Part.createFormData("file", filename, countingRequestBody);
RequestBody fileNameRequestBody = RequestBody.create(okhttp3.MultipartBody.FORM, filename);
RequestBody tokenRequestBody;
try {
tokenRequestBody = RequestBody.create(MultipartBody.FORM, csrfTokenClient.getTokenBlocking());
return uploadInterface.uploadFileToStash(fileNameRequestBody, tokenRequestBody, filePart)
.map(stashUploadResponse -> stashUploadResponse.getUpload());
} catch (Throwable throwable) {
throwable.printStackTrace();
return Observable.error(throwable);
}
}
Observable<UploadResult> uploadFileFromStash(Context context,
Contribution contribution,
String uniqueFileName,
String fileKey) {
try {
return uploadInterface
.uploadFileFromStash(csrfTokenClient.getTokenBlocking(),
pageContentsCreator.createFrom(contribution),
CommonsApplication.DEFAULT_EDIT_SUMMARY,
uniqueFileName,
fileKey).map(uploadResponse -> uploadResponse.getUpload());
} catch (Throwable throwable) {
throwable.printStackTrace();
return Observable.error(throwable);
}
Observable<UploadResult> uploadFileFromStash(final Context context,
final Contribution contribution,
final String uniqueFileName,
final String fileKey) {
try {
return uploadInterface
.uploadFileFromStash(csrfTokenClient.getTokenBlocking(),
pageContentsCreator.createFrom(contribution),
CommonsApplication.DEFAULT_EDIT_SUMMARY,
uniqueFileName,
fileKey).map(UploadResponse::getUpload);
} catch (final Throwable throwable) {
throwable.printStackTrace();
return Observable.error(throwable);
}
}
}

View file

@ -16,19 +16,22 @@ import static org.wikipedia.dataclient.Service.MW_API_PREFIX;
public interface UploadInterface {
@Multipart
@POST(MW_API_PREFIX + "action=upload&stash=1&ignorewarnings=1")
Observable<UploadResponse> uploadFileToStash(@Part("filename") RequestBody filename,
@Part("token") RequestBody token,
@Part MultipartBody.Part filePart);
@Multipart
@POST(MW_API_PREFIX + "action=upload&stash=1&ignorewarnings=1")
Observable<UploadResponse> uploadFileToStash(@Part("filename") RequestBody filename,
@Part("filesize") RequestBody totalFileSize,
@Part("offset") RequestBody offset,
@Part("filekey") RequestBody fileKey,
@Part("token") RequestBody token,
@Part MultipartBody.Part filePart);
@Headers("Cache-Control: no-cache")
@POST(MW_API_PREFIX + "action=upload&ignorewarnings=1")
@FormUrlEncoded
@NonNull
Observable<UploadResponse> uploadFileFromStash(@NonNull @Field("token") String token,
@NonNull @Field("text") String text,
@NonNull @Field("comment") String comment,
@NonNull @Field("filename") String filename,
@NonNull @Field("filekey") String filekey);
@Headers("Cache-Control: no-cache")
@POST(MW_API_PREFIX + "action=upload&ignorewarnings=1")
@FormUrlEncoded
@NonNull
Observable<UploadResponse> uploadFileFromStash(@NonNull @Field("token") String token,
@NonNull @Field("text") String text,
@NonNull @Field("comment") String comment,
@NonNull @Field("filename") String filename,
@NonNull @Field("filekey") String filekey);
}

View file

@ -7,6 +7,7 @@ private const val RESULT_SUCCESS = "Success"
data class UploadResult(
val result: String,
val filekey: String,
val offset: Int,
val filename: String,
val sessionkey: String,
val imageinfo: ImageInfo

View file

@ -22,24 +22,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;
@ -316,7 +308,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 WikidataInterface wikidataInterface;
private final Gson gson;
@Inject
public WikidataClient(WikidataInterface wikidataInterface) {
this.wikidataInterface = wikidataInterface;
}
@Inject
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) {
return getCsrfToken()
.flatMap(csrfToken -> wikidataInterface.postCreateClaim(
toRequestBody(entity.getId()),
toRequestBody("value"),
toRequestBody(WikidataProperties.IMAGE.getPropertyName()),
toRequestBody(value),
toRequestBody("en"),
toRequestBody(csrfToken)))
.map(mwPostResponse -> mwPostResponse.getPageinfo().getLastrevid());
}
/**
* Create wikidata claim to add P18 value
*
* @return revisionID of the edit
*/
Observable<Long> setClaim(Statement_partial claim, String tags) {
return getCsrfToken()
.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));
}
/**
* Get csrf token for wikidata edit
*/
@NotNull
private Observable<String> getCsrfToken() {
return wikidataInterface.getCsrfToken()
.map(mwQueryResponse -> mwQueryResponse.query().csrfToken());
}
}

View file

@ -20,49 +20,58 @@ 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;
private final JsonKvStore directKvStore;
private final WikiBaseClient wikiBaseClient;
private final WikidataClient wikidataClient;
private final Gson gson;
private final Context context;
private final WikidataEditListener wikidataEditListener;
private final JsonKvStore directKvStore;
private final WikiBaseClient wikiBaseClient;
private final WikidataClient wikidataClient;
private final Gson gson;
@Inject
public WikidataEditService(final Context context,
public WikidataEditService(final Context context,
final WikidataEditListener wikidataEditListener,
@Named("default_preferences") final JsonKvStore directKvStore,
final WikiBaseClient wikiBaseClient,
final WikidataClient wikidataClient, final Gson gson) {
this.context = context;
this.wikidataEditListener = wikidataEditListener;
this.directKvStore = directKvStore;
this.wikiBaseClient = wikiBaseClient;
this.wikidataClient = wikidataClient;
this.context = context;
this.wikidataEditListener = wikidataEditListener;
this.directKvStore = directKvStore;
this.wikiBaseClient = wikiBaseClient;
this.wikidataClient = wikidataClient;
this.gson = gson;
}
/**
* 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,
@ -70,7 +79,7 @@ public class WikidataEditService {
final EditClaim data = editClaim(
ConfigUtils.isBetaFlavour() ? "Q10" // Wikipedia:Sandbox (Q10)
: depictedItem.getId()
: depictedItem.getId()
);
return wikiBaseClient.postEditEntity(PAGE_ID_PREFIX + fileEntityId, gson.toJson(data))
@ -81,44 +90,46 @@ 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());
})
.subscribeOn(Schedulers.io());
}
}
private EditClaim editClaim(final String entityId) {
return EditClaim.from(entityId, WikidataProperties.DEPICTS.getPropertyName());
}
/**
* Show a success toast when the edit is made successfully
*/
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);
ViewUtil.showLongToast(context, successMessage);
}
* Show a success toast when the edit is made successfully
*/
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);
ViewUtil.showLongToast(context, successMessage);
}
/**
* Adds label to Wikidata using the fileEntityId and the edit token, obtained from csrfTokenClient
*
* @param fileEntityId
* @return
*/
* Adds label to Wikidata using the fileEntityId and the edit token, obtained from
* csrfTokenClient
*
* @param fileEntityId
* @return
*/
@SuppressLint("CheckResult")
private Observable<Boolean> addCaption(final long fileEntityId, final String languageCode,
final String captionValue) {
return wikiBaseClient.addLabelstoWikidata(fileEntityId, languageCode, captionValue)
.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));
})
.map(mwPostResponse -> mwPostResponse != null);
}
@SuppressLint("CheckResult")
private Observable<Boolean> addCaption(final long fileEntityId, final String languageCode,
final String captionValue) {
return wikiBaseClient.addLabelstoWikidata(fileEntityId, languageCode, captionValue)
.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));
})
.map(mwPostResponse -> mwPostResponse != null);
}
private void onAddCaptionResponse(Long fileEntityId, MwPostResponse response) {
if (response != null) {
@ -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);
}
throw new RuntimeException("Unable to edit wikidata item");
})
.subscribeOn(Schedulers.io())
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()))));
}
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 -> {
Timber.e(throwable, "Error occurred while making claim");
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
});
.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) {
@ -185,9 +208,9 @@ public class WikidataEditService {
}
}
).subscribe(
success -> Timber.d("edit response: %s", success),
throwable -> Timber.e(throwable, "posting edits failed")
);
success -> Timber.d("edit response: %s", success),
throwable -> Timber.e(throwable, "posting edits failed")
);
}
private Observable<Boolean> captionEdits(Contribution contribution, Long fileEntityId) {
@ -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,35 +21,21 @@ 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);
/**
* Get edit token for wikidata wiki site
*/
@Headers("Cache-Control: no-cache")
@GET(MW_API_PREFIX + "action=query&meta=tokens&type=csrf")
@NonNull
Observable<MwQueryResponse> getCsrfToken();
/**
* 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
*/
@Headers("Cache-Control: no-cache")
@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

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingLeft="20dp"
android:paddingRight="20dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/showcase_view_whole_nearby_activity"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:orientation="horizontal">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="0"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_custom_map_marker" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="0"
android:text="@string/showcase_view_needs_photo"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="0"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_custom_map_marker_green" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="0"
android:text="@string/showcase_view_has_photo"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="0"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_custom_map_marker_grey" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="0"
android:text="@string/showcase_view_no_longer_exists"/>
</LinearLayout>
</LinearLayout>

View file

@ -4,6 +4,7 @@
* Abijeet Patro
* Amirsara
* Arash.pt
* BaRaN6161 TURK
* Ebraminio
* Eshagh79
* FarsiNevis
@ -28,7 +29,11 @@
<item quantity="one">%1$d پرونده در حال بارگذاری</item>
<item quantity="other">%1$d پرونده در حال بارگذاری</item>
</plurals>
<string name="contributions_subtitle">{{%1$d|zero=@string/contributions_subtitle_zero|one=(%1$d)|(%1$d)}}</string>
<plurals name="contributions_subtitle">
<item quantity="zero">\@string/contributions_subtitle_zero</item>
<item quantity="one">(%1$d)</item>
<item quantity="other">(%1$d)</item>
</plurals>
<plurals name="starting_multiple_uploads">
<item quantity="one">شروع %1$d بارگذاری پرونده</item>
<item quantity="other">شروع بارگذاری %1$d پرونده</item>

View file

@ -19,6 +19,7 @@
</plurals>
<plurals name="multiple_uploads_title">
<item quantity="one">%1$d개 올리적재</item>
<item quantity="other">%1$d개 올리적재</item>
</plurals>
<string name="share_license_summary">이 그림은 %1$s에 따라 사용이 허가됩니다</string>
<string name="title_activity_explore">찾아보기</string>

View file

@ -33,6 +33,7 @@
</plurals>
<plurals name="multiple_uploads_title">
<item quantity="one">%1$d개 업로드</item>
<item quantity="other">%1$d개 업로드</item>
</plurals>
<plurals name="share_license_summary">
<item quantity="one">이 그림은 %1$s에 따라 사용이 허가됩니다</item>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Authors:
* Lancine.kounfantoh.fofana
-->
<resources>
<string name="crash_dialog_title">ߞߐߡߐ߲ ߓߘߊ߫ ߗߌߙߏ߲߫</string>
<string name="crash_dialog_text">ߋߜߋ߫. ߞߏ ߘߏ߫ ߓߍ߲߬ߣߍ߲߫ ߕߎ߲߬ ߕߍ߫߹</string>
<string name="crash_dialog_comment_prompt">ߊ߲ ߠߎ߬ ߘߍ߬ߡߍ߲߬ ߊ߬ ߘߐߓߍ߲߬ߠߌ߲ ߡߊ߬߹</string>
<string name="crash_dialog_ok_toast">ߌ ߣߌ߫ ߗߋ߫߹</string>
</resources>

View file

@ -50,10 +50,14 @@
<string name="provider_contributions">ߒ ߠߊ߫ ߟߊ߬ߦߟߍ߬ߣߍ߲ ߠߎ߬</string>
<string name="menu_share">ߊ߬ ߟߊߖߍ߲ߛߍ߲߫</string>
<string name="menu_open_in_browser">ߊ߬ ߘߐߜߍ߫ ߛߏ߲߯ߓߊߟߊ߲ ߠߊ߫</string>
<string name="share_title_hint" fuzzy="true">ߞߎ߲߬ߕߐ߰ ߞߊ߬ߣߌ߲߬ߣߍ߲</string>
<string name="share_title_hint">ߝߍ߬ߛߓߍߟߌ (ߡߊߢߌ߬ߣߌ߲߬ߞߊ߬ߣߍ߲)</string>
<string name="add_caption_toast">ߝߍ߬ߛߓߍߟߌ ߘߏ߫ ߡߊߛߐ߫ ߞߐߕߐ߮ ߣߌ߲߬ ߠߊ߫ ߖߊ߰ߣߌ߲߫</string>
<string name="share_description_hint">ߞߊ߲߬ߛߓߍߟߌ</string>
<string name="share_caption_hint">ߝߍ߬ߛߓߍߟߌ (ߞߐߘߊ߲ ߦߋ߫ ߛߓߍߘߋ߲߫ ߂߅߅ ߟߋ߬ ߘߌ߫)</string>
<string name="login_failed_network">ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߛߐ߲߬ߣߍ߲߫ ߕߍ߫ ߞߍ߫ ߟߊ߫ - ߞߙߏߝߏ ߟߊ߫ ߗߌߙߏ߲ߠߌ߲</string>
<string name="login_failed_wrong_credentials">ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߛߐ߲߬ߣߍ߲߫ ߕߍ߫ ߞߍ߫ ߟߊ߫ - ߌ ߟߊ߫ ߕߐ߯ ߟߊߓߊ߯ߕߊ ߣߌ߫ ߕߊ߬ߡߌ߲߬ߞߊ߲ ߠߎ߬ ߡߊߝߟߍ߫ ߖߊ߰ߣߌ߲߬</string>
<string name="login_failed_throttled">ߛߊ߯ߛߊ߯ߟߌ߫ ߛߎߘߊ߲ߓߊߟߌ߫ ߛߘߍߡߊ߲߫ ߓߘߊ߫ ߞߍ߫ ߢߐ߲߮ ߞߐ߫ ߞߏߖߎ߯ߦߊ߫.ߊ߬ ߡߊߝߍߣߍ߲߫ ߡߌ߬ߛߍ߲߬ ߘߊ߲ߘߐ߫ ߞߐ߫ ߖߊ߰ߣߌ߲߫.</string>
<string name="login_failed_blocked">ߤߊߞߍ߬ߕߏ߫߸ ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߣߌ߲߬ ߓߘߊ߫ ߓߊ߬ߟߌ߬ ߞߐ߬ߡߐ߲ ߞߊ߲߬</string>
<string name="login_failed_generic">ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߓߘߊ߫ ߗߌߙߏ߲߫</string>
<string name="share_upload_button">ߊ߬ ߟߊߦߟߍ߬</string>
<string name="multiple_share_base_title">ߟߊ߬ߘߏ߲߬ߠߌ߲ ߣߌ߲߬ ߕߐ߯ߟߊ߫</string>
@ -74,7 +78,7 @@
<string name="title_activity_featured_images">ߟߊߓߊ߯ߙߊߟߌ ߖߌ߬ߦߊ߬ߓߍ ߟߎ߬</string>
<string name="title_activity_category_details">ߦߌߟߡߊ</string>
<string name="menu_about">ߞߊ߬ ߓߍ߲߬</string>
<string name="about_privacy_policy" fuzzy="true">&lt;u&gt;ߜߎ߲߬ߘߎ߬ߢߐ߲߰ߦߊ ߞߎߙߎ߲߬ߘߎ&lt;/u&gt;</string>
<string name="about_privacy_policy">ߜߎ߲߬ߘߎ߬ߢߐ߲߰ߦߊ ߞߎߙߎ߲߬ߘߎ</string>
<string name="title_activity_about">ߞߊ߬ ߓߍ߲߬</string>
<string name="menu_feedback">ߞߙߐ߬ߛߌ߬ߕߊ ߗߋ߫ (ߢ:ߞߏ߲ߘߏ ߟߊ߫)</string>
<string name="no_email_client">ߢߎߡߍߙߋ߲ߞߏ߲ߘߏ ߛߊ߲߬ߠߌ߲߬ߢߐ߲߰ ߡߊߞߍߣߍ߲߫ ߕߴߦߋ߲߬</string>
@ -107,15 +111,21 @@
<string name="detail_panel_cats_label">ߦߌߟߡߊ ߟߎ߬</string>
<string name="detail_panel_cats_loading">ߟߊ߬ߢߎ߲߬ߠߌ߲ ߦߵߌ ߘߐ߫...</string>
<string name="detail_panel_cats_none">ߊ߬ ߡߊ߫ ߓߊߕߐ߬ߡߐ߲߬</string>
<string name="detail_caption_empty">ߝߍ߬ߛߓߍߟߌ߫ ߕߍ߫ ߦߋ߲߬</string>
<string name="detail_description_empty">ߞߊ߲߬ߛߓߍߟߌ߫ ߕߴߦߋ߲߬</string>
<string name="detail_discussion_empty">ߘߊߘߐߖߊߥߏ߫ ߕߴߦߋ߲߬</string>
<string name="ok">ߏ߬ߞߍ߫</string>
<string name="title_activity_nearby">ߛߌ߰ߢߐ߲߰ ߦߙߐ ߟߎ߬</string>
<string name="no_nearby">ߛߌ߰ߢߐ߲߰ ߦߙߐ߫ ߡߊ߫ ߛߐ߬ߘߐ߲߫</string>
<string name="warning">ߖߊ߲߬ߓߌ߬ߟߊ߬ߟߌ</string>
<string name="duplicate_image_found">ߖߌ߬ߦߊ߬ߓߍ߫ ߓߊߟߌߣߍ߲ ߠߎ߬ ߦߋ߫ ߦߋ߲߬</string>
<string name="upload_image_duplicate">ߞߐߕߐ߮ ߣߌ߲߬ ߓߘߊ߫ ߘߐߕߌߢߍ߫ ߞߐߡߐ߲ ߞߊ߲߬. ߌ ߟߊߣߴߊ߬ ߟߊ߫ ߞߴߌ ߦߴߊ߬ ߝߍ߬ ߞߵߊ߬ ߘߊߓߊ߲߫؟</string>
<string name="upload">ߊ߬ ߟߊߦߟߍ߬</string>
<string name="yes">ߐ߲߬ߐ߲߬ߐ߲߫</string>
<string name="no">ߍ߲߬ߍ߲߫</string>
<string name="media_detail_caption">ߝߍ߬ߛߓߍߟߌ</string>
<string name="media_detail_title">ߞߎ߲߬ߕߐ߮</string>
<string name="media_detail_depiction">ߞߐߦߌߘߊߟߌ</string>
<string name="media_detail_description">ߞߊ߲߬ߛߓߍߟߌ</string>
<string name="media_detail_discussion">ߘߊߘߐߖߊߥߏ</string>
<string name="media_detail_author">ߛߓߍߦߟߊ</string>
@ -166,11 +176,15 @@
<string name="upload_problem_different_geolocation">ߖߌ߬ߦߊ߬ߓߍ ߣߌ߲߬ ߕߊ߬ߣߍ߲߬ ߦߋ߫ ߦߙߐ߫ ߜߘߍ߫ ߟߋ߬</string>
<string name="upload_problem_fbmd">ߌ ߦߋ߫ ߖߌ߬ߦߊ߬ߓߍ ߟߋ߬ ߟߊߦߍ߬ߟߍ߫ ߖߊ߰ߣߌ߲߫ ߌ ߖߍ߬ߘߍ ߞߊ߬ ߡߍ߲ ߠߎ߬ ߕߊ߬. ߒ߬ߞߊ߬ ߌ ߞߊ߫ ߖߌ߬ߦߊ߬ߓߍ߫ ߟߊߦߍ߬ߟߍ߫ ߡߎ߰ߡߍ߫ ߌ ߞߊ߬ ߡߍ߲ ߠߎ߬ ߕߊ߬ ߡߐ߱ ߟߎ߬ ߟߊ߫ ߝߋߛߑߓߎߞ ߞߊ߲߬.</string>
<string name="upload_problem_do_you_continue">ߌ ߦߴߊ߬ ߝߍ߬ ߞߊ߬ ߖߌ߬ߦߊ߬ߓߍ ߣߌ߲߬ ߠߊߦߟߍ߬ ߡߎߣߎ߲߬؟</string>
<string name="upload_problem_image">ߝߙߋߞߋ ߓߘߊ߫ ߦߋ߫ ߖߌ߬ߦߊ߬ߓߍ ߟߊ߫</string>
<string name="internet_downloaded">ߖߊ߰ߣߌ߲߬ ߌ ߦߋ߫ ߖߌ߬ߦߊ߬ߓߍ߬ ߟߎ߫ ߟߋ߬ ߟߊߦߟߍ߬ ߌ ߖߍ߬ߘߍ ߞߊ߬ ߡߍ߲ ߠߎ߬ ߕߊ߬. ߞߏ߬ߣߌ߲߬ ߌ ߞߊߣߊ߬ ߖߌ߬ߦߊ߬ߓߍ߬ ߟߊߦߟߍ߬ ߌ ߣߊ߬ ߡߍ߲ ߠߊߖߌ߰ ߟߴߌ ߞߎ߲߬ ߓߟߐߟߐ ߟߊ߫.</string>
<string name="give_permission">ߟߊ߬ߘߌ߬ߢߍ߬ߟߌ ߦߌ߬ߘߊ߬</string>
<string name="login_to_your_account">ߌ ߜߊ߲߬ߞߎ߲߫ ߌ ߟߊ߫ ߖߊ߬ߕߋ߬ߘߊ ߟߊ߫.</string>
<string name="no_web_browser">ߓߟߐߟߐ ߛߏ߲߯ߓߊߟߊ߲߫ ߡߊ߫ ߛߐ߬ߘߐ߲߬ ߞߊ߬ URL ߟߊߞߊ߬</string>
<string name="null_url">ߝߎ߬ߕߎ߲߬ߕߌ߹ URL ߡߊ߫ ߛߐ߬ߘߐ߲߬</string>
<string name="nominated_see_more">ߞߍߦߙߐ߫ ߞߐߜߍ ߘߐߜߍ߫ ߝߊߙߊ߲ߝߊ߯ߛߌ߫ ߞߏ ߘߐ߫</string>
<string name="nominating_file_for_deletion">ߕߐ߯ߦߊߟߌ ߖߏ߰ߛߌ߬ߟߌ ߞߊ߲ߡߊ߬</string>
<string name="nominating_for_deletion_status">ߞߐߕߐ߮ ߕߐ߯ߦߊߟߌ ߖߏ߬ߛߟߌ߬ ߞߊ߲ߡߊ߬: %1$s</string>
<string name="view_browser">ߊ߬ ߘߐߜߍ߫ ߛߏ߲߯ߓߊߟߊ߲ ߠߊ߫</string>
<string name="skip_login">ߊ߬ ߟߊߜߊ߲߫</string>
<string name="navigation_item_login">ߌ ߜߊ߲߬ߞߎ߲߬</string>
@ -187,11 +201,11 @@
<string name="nearby_wikidata">ߥߞߌߘߕߊ</string>
<string name="nearby_wikipedia">ߥߞߌߔߋߘߌߦߊ߫</string>
<string name="nearby_commons">ߞߐߡߐ߲</string>
<string name="about_rate_us" fuzzy="true">&lt;u&gt;ߊ߲ ߡߐ߬ߟߐ߲ ߦߌ߬ߘߊ߬&lt;/u&gt;</string>
<string name="about_faq" fuzzy="true">&lt;u&gt;ߢ.ߡ&lt;/u&gt;</string>
<string name="about_rate_us">ߡߐ߬ߟߐ߲ ߘߴߊ߲ ߡߊ߬</string>
<string name="about_faq">ߢ.ߡ</string>
<string name="no_internet">ߓߟߐߟߐ߫ ߕߴߦߋ߲߬</string>
<string name="internet_established">ߓߟߐߟߐ ߦߋ߫ ߦߋ߲߬</string>
<string name="about_translate" fuzzy="true">&lt;u&gt;ߘߟߊߡߌߘߊߟߌ&lt;/u&gt;</string>
<string name="about_translate">ߘߟߊߡߌߘߊߟߌ</string>
<string name="about_translate_title">ߞߊ߲ ߠߎ߬</string>
<string name="about_translate_message">ߞߊ߲ ߘߏ߫ ߓߊߕߐ߬ߡߐ߲߬ ߌ ߦߴߊ߬ ߝߍ߬ ߞߵߊ߬ ߘߟߊߡߌ߬ߘߊ߬ ߡߍ߲ ߘߐ߫.</string>
<string name="about_translate_cancel">ߊ߬ ߘߐߛߊ߬</string>
@ -274,6 +288,11 @@
<string name="review_thanks">ߊ߬ ߟߐ߯ ߦߴߌ ߟߊ߫ ߞߊ߬ ߓߟߏߓߌߟߊߢߐ߲߯ߞߊ߲ߠߊ ߟߎ߬ ߞߎߟߎ߲ߖߋ߫ ߓߊ߬؟</string>
<string name="review_no_category">ߐ߲߬ߤߐ߲߯߹ ߣߌ߲߬ ߝߊ߲߭ ߡߊ߫ ߦߌߟߡߊߦߊ߫ ߟߋ߬߹</string>
<string name="review_category_explanation">ߖߌ߬ߦߊ߬ߓߍ ߣߌ߲߬ ߦߋ߫ %1$s ߦߌߟߡߊ ߟߎ߬ ߞߘߐ߫.</string>
<string name="review_c_violation_report_question">ߊ߬ ߦߋ߫ ߓߊߦߟߍߡߊ߲ߠߌ߲ ߤߊߞߍ ߞߎ߬ߙߎ߲߬ߘߎ ߕߌߢߍ ߟߋ߬ ߘߌ߫ ߓߊߏ߬ ߊ߬ ߦߋ߫</string>
<string name="review_category_yes_button_text">ߒ߬ߒ߫߸ ߦߌߟߡߦߊߟߌ ߞߐߢߌ߬ߣߊ߬ߣߍ߲߹</string>
<string name="review_category_no_button_text">ߊ߬ ߞߍߣߍ߲߫ ߦߴߊ߬ ߢߊ߬ߣߍ߲߫</string>
<string name="review_spam_yes_button_text">ߒ߬ߒ߫߸ ߊ߬ ߞߍߣߍ߲߫ ߦߏ߫ ߞߣߍ ߕߴߦߋ߲߬</string>
<string name="review_spam_no_button_text">ߊ߬ ߞߍߣߍ߲߫ ߦߴߊ߬ ߢߊ߬ߣߍ߲߫</string>
<string name="review_copyright_yes_button_text">ߒ߬ߒ߫߸ ߓߊߦߟߍߡߊ߲ ߤߊߞߍ ߡߊ߫ ߕߌߢߍ߫</string>
<string name="review_copyright_no_button_text">ߊ߬ ߞߍߣߍ߲߫ ߦߴߊ߬ ߓߍ߲߬ߣߍ߲߫</string>
<string name="review_thanks_yes_button_text">ߐ߲߬ߐ߲ߐ߲߫߸ ߡߎ߲߬ߠߊ߫ ߍ߲߬ߍ߲߫</string>
@ -287,10 +306,16 @@
<string name="exif_tag_name_copyright">ߓߊߦߟߍߡߊ߲ ߤߊߞߍ</string>
<string name="exif_tag_name_location">ߘߌ߲߬ߞߌߙߊ</string>
<string name="exif_tag_name_cameraModel">ߖߌ߬ߦߊ߬ߕߊ߬ߟߊ߲ ߛߎ߮ߦߊ</string>
<string name="image_info">ߖߌ߬ߦߊ߬ߓߍ ߞߌ߬ߓߊ߬ߙߏ߬ߦߊ</string>
<string name="no_categories_found">ߦߌߟߡߊߙߋ߲߫ ߕߴߦߋ߲߬</string>
<string name="no_depiction_found">ߘߊ߲߬ߠߊ߬ߕߍ߰ߟߌ ߡߊ߫ ߛߐ߬ߘߐ߲߬</string>
<string name="upload_cancelled">ߟߊ߬ߦߟߍ߬ߟߌ ߘߊߓߌ߬ߟߊ߬</string>
<string name="dialog_box_text_nomination">ߡߎ߲߬ߠߊ߫ %1$s ߖߏ߬ߛߌ߬ߕߐ߫؟</string>
<string name="review_is_uploaded_by">%1$s ߟߊߦߟߍ߬ߣߍ߲߬ ߦߋ߫: %2$s ߟߋ߬ ߓߟߏ߫</string>
<string name="default_description_language">ߞߊ߲߬ߛߓߍߟߌ ߞߍ߫ ߞߊ߲ ߓߊߖߎߡߊ</string>
<string name="delete_helper_make_deletion_toast">ߌ ߦߴߌ ߞߊߘߊ߲ ߞߊ߲߬ ߞߊ߬ %1$s ߕߐ߯ߦߊ߫ ߖߏ߬ߛߟߌ߬ ߞߊ߲ߡߊ߬</string>
<string name="delete_helper_show_deletion_title">ߕߐ߯ߦߊߟߌ ߖߏ߰ߛߌ߬ߟߌ ߞߊ߲ߡߊ߬</string>
<string name="delete_helper_show_deletion_title_success">ߊ߬ ߓߘߊ߫ ߛߎߘߊ߲߫</string>
<string name="delete_helper_show_deletion_message_if">%1$s ߕߐ߯ߦߊߟߌ ߦߴߌ ߘߐ߫ ߖߏ߬ߛߌ߬ߟߌ ߞߊ߲ߡߊ߬</string>
<string name="delete_helper_show_deletion_title_failed">ߊ߬ ߓߘߊ߫ ߗߌߙߏ߲߫</string>
<string name="delete_helper_show_deletion_message_else">ߌ ߕߍ߫ ߛߋ߫ ߖߏ߰ߛߌ߬ߟߌ ߡߊߢߌߣߌ߲߫ ߠߊ߫</string>
@ -302,5 +327,14 @@
<string name="delete_helper_ask_alert_set_positive_button_reason">ߓߊߏ߬ ߊ߬ ߦߋ߫</string>
<string name="share_image_via">ߖߌ߬ߦߊ߬ߓߍ ߟߊߖߍ߲ߛߍ߲߫ ߞߊߕߙߍ߬</string>
<string name="no_achievements_yet">ߌ ߡߊ߫ ߓߟߏߓߌߟߊߢߐ߲߯ߞߊ߲ ߛߌ߫ ߞߍ߫ ߡߎߣߎ߲߬</string>
<string name="account_created">ߖߊ߬ߕߋ߬ߘߊ ߓߘߊ߫ ߛߌ߲ߘߌ߫߹</string>
<string name="some_error">ߝߎ߬ߕߎ߲߬ߕߌ ߘߏ߫ ߕߘߍ߬ ߦߋ߫ ߦߋ߲߬߹</string>
<string name="theme_dark_name">ߘߌ߬ߓߌ</string>
<string name="load_more">ߘߏߜߘߍ߫ ߟߎ߫ ߟߊߢߎ߲߫</string>
<string name="add_picture_to_wikipedia_article_title">ߖߌ߬ߦߊ߬ߓߍ ߝߙߊ߬ ߥߞߌߔߋߘߌߦߊ ߞߊ߲߬</string>
<string name="add_picture_to_wikipedia_article_desc">ߌ ߦߴߊ߬ ߝߍ߬ ߞߊ߬ ߖߌ߬ߦߊ߬ߓߍ ߣߌ߲߬ ߓߌ߬ߟߊ߬ %1$ ߞߊ߲ ߥߞߌߔߋߘߌߦߊ ߞߎߡߘߊ ߟߊ߫ ߓߊ߬؟</string>
<string name="confirm">ߊ߬ ߟߊߛߙߋߦߊ߫</string>
<string name="wikipedia_instructions_step_1">߁߭. ߥߞߌߛߓߍߟߌ ߢߌ߲߬ ߠߎ߬ ߟߊߓߊ߯ߙߊ߫:</string>
<string name="wikipedia_instructions_step_2">߂.ߟߊ߬ߛߙߋ߬ߦߊ߬ߟߌ ߛߐ߲߬ߞߌ߲ ߓߍߣߊ߬ ߥߞߌߔߋߘߌߦߊ ߞߎߡߘߊ ߘߊߦߟߍ߬</string>
<string name="wikipedia_instructions_step_7">߇߲. ߞߎߡߘߊ ߟߊߥߊ߲߬ߞߊ߫</string>
</resources>

View file

@ -600,4 +600,18 @@
<string name="use_location_from_similar_image">Tog du dessa två bilder på samma plats? Vill du använda den högra bildens latitud/longitud?</string>
<string name="load_more">Läs in fler</string>
<string name="nearby_no_results">Inga platser hittades, försök ändra dina sökkriterier.</string>
<string name="add_picture_to_wikipedia_article_title">Lägg till bild på Wikipedia</string>
<string name="add_picture_to_wikipedia_article_desc">Vill du lägga till denna bild i Wikipedia-artikeln på %1$s?</string>
<string name="add_picture_to_wikipedia_instructions_title">Instruktioner</string>
<string name="add_picture_to_wikipedia_instructions_desc">Se till att följa riktlinjerna för hur man redigerar!</string>
<string name="confirm">Bekräfta</string>
<string name="instructions_title">Instruktioner</string>
<string name="wikipedia_instructions_step_1">1. Använd följande wikitext:</string>
<string name="wikipedia_instructions_step_2">2. Klicka på \"Bekräfta\" för att öppna Wikipedia-artikeln</string>
<string name="wikipedia_instructions_step_3">3. Hitta ett lämplig avsnitt i artikeln för din bild</string>
<string name="wikipedia_instructions_step_4">4. Klicka på redigeringsikonen (ser ut som en penna) för detta avsnitt.</string>
<string name="wikipedia_instructions_step_5">5. Klistra in wikitexten på det lämpliga stället.</string>
<string name="wikipedia_instructions_step_6">6. Redigera wikitexten för att placera den mer lämpligt vid behov. För mer information, se &lt;a href=\"https://en.wikipedia.org/wiki/Wikipedia:Manual_of_Style/Images#How_to_place_an_image\"&gt;här&lt;/a&gt;.</string>
<string name="wikipedia_instructions_step_7">7. Publicera artikeln</string>
<string name="copy_wikicode_to_clipboard">Kopiera wikikod till urklipp</string>
</resources>

View file

@ -419,7 +419,7 @@
<string name="preference_author_name_toggle_summary">Fotoğraf yüklerken kullanıcı adınız yerine özel bir yazar adı kullanın</string>
<string name="preference_author_name">Özel yazar adı</string>
<string name="contributions_fragment">Katkılar</string>
<string name="nearby_fragment">Yakınımdakiler</string>
<string name="nearby_fragment">Yakınındakiler</string>
<string name="notifications">Bildirimler</string>
<string name="read_notifications">Bildirimler (okunmuş)</string>
<string name="display_nearby_notification">Yakınımdakiler bildirimi görüntüle</string>

View file

@ -14,10 +14,12 @@
<plurals name="starting_multiple_uploads">
<item quantity="one">იჭყაფუ %1$d ეხარგუა</item>
<item quantity="few">იჭყაფუ %1$d ეხარგუა</item>
<item quantity="other">იჭყაფუ %1$d ეხარგუა</item>
</plurals>
<plurals name="multiple_uploads_title">
<item quantity="one">%1$d ეხარგუა</item>
<item quantity="few">%1$d ეხარგუა</item>
<item quantity="other">%1$d ეხარგუა</item>
</plurals>
<plurals name="share_license_summary">
<item quantity="one">თე სურათი გიბჟინუ %1$s ლიცენზიათ</item>

View file

@ -28,9 +28,11 @@
</plurals>
<plurals name="starting_multiple_uploads">
<item quantity="one">開始 %1$d 次上傳</item>
<item quantity="other">開始 %1$d 次上傳</item>
</plurals>
<plurals name="multiple_uploads_title">
<item quantity="one">%1$d 次上傳</item>
<item quantity="other">%1$d 次上傳</item>
</plurals>
<plurals name="share_license_summary">
<item quantity="one">此圖片會按 %1$s 協議授權上載</item>

View file

@ -99,9 +99,10 @@
<string name="provider_contributions">我的上传</string>
<string name="menu_share">分享</string>
<string name="menu_open_in_browser">在浏览器中查看</string>
<string name="share_title_hint" fuzzy="true">标题 (要求)</string>
<string name="share_title_hint">说明 (要求)</string>
<string name="add_caption_toast">请提供此文件的描述</string>
<string name="share_description_hint">描述</string>
<string name="share_caption_hint">说明255个字符以内</string>
<string name="login_failed_network">无法登录 - 网络故障</string>
<string name="login_failed_wrong_credentials">无法登录——请检查您的用户名和密码</string>
<string name="login_failed_throttled">失败次数过多。请在几分钟后重试。</string>
@ -206,8 +207,10 @@
<string name="no_nearby">找不到附近地点</string>
<string name="warning">警告</string>
<string name="upload_image_duplicate">此文件已在共享资源下存在。您确定要继续吗?</string>
<string name="upload">上传</string>
<string name="yes"></string>
<string name="no"></string>
<string name="media_detail_caption">说明</string>
<string name="media_detail_title">标题</string>
<string name="media_detail_description">描述</string>
<string name="media_detail_discussion">讨论</string>
@ -315,8 +318,8 @@
<string name="nearby_wikidata">维基数据</string>
<string name="nearby_wikipedia">维基百科</string>
<string name="nearby_commons">共享资源</string>
<string name="about_rate_us" fuzzy="true">&lt;u&gt;评价我们&lt;/u&gt;</string>
<string name="about_faq" fuzzy="true">&lt;u&gt;常见问题&lt;/u&gt;</string>
<string name="about_rate_us">评价我们</string>
<string name="about_faq">常见问题</string>
<string name="welcome_skip_button">跳过指导</string>
<string name="no_internet">互联网不可用</string>
<string name="internet_established">互联网可用</string>
@ -324,7 +327,7 @@
<string name="error_review">获取审查图片错误。按刷新键重试。</string>
<string name="error_review_categories">获取审查图片类别错误。按刷新按键重试。</string>
<string name="no_notifications">找不到通知</string>
<string name="about_translate" fuzzy="true">&lt;u&gt;翻译&lt;/u&gt;</string>
<string name="about_translate">翻译</string>
<string name="about_translate_title">语言</string>
<string name="about_translate_message">选择您希望提交翻译的语言</string>
<string name="about_translate_proceed">已处理</string>
@ -351,6 +354,7 @@
<string name="error_loading_subcategories">加载子分类时发生错误。</string>
<string name="search_tab_title_media">媒体</string>
<string name="search_tab_title_categories">分类</string>
<string name="search_tab_title_depictions">项目</string>
<string name="explore_tab_title_featured">特色</string>
<string name="explore_tab_title_mobile">通过移动端上传</string>
<string name="successful_wikidata_edit">图片已添加到维基数据上的%1$s</string>
@ -575,6 +579,9 @@
<string name="place_type">地点类型:</string>
<string name="nearby_search_hint">桥梁、博物馆、旅馆等</string>
<string name="you_must_reset_your_passsword">登录时出现一些问题,您必须重新设置您的密码!</string>
<string name="title_app_shortcut_explore">探索</string>
<string name="title_app_shortcut_bookmark">书签</string>
<string name="title_app_shortcut_setting">设置</string>
<string name="wallpaper_set_unsuccessfully">出错了。无法设置壁纸</string>
<string name="setting_wallpaper_dialog_title">设为壁纸</string>
<string name="setting_wallpaper_dialog_message">正在设置壁纸。请稍等…</string>
@ -582,4 +589,5 @@
<string name="add_picture_to_wikipedia_instructions_title">说明</string>
<string name="confirm">确认</string>
<string name="instructions_title">说明</string>
<string name="copy_wikicode_to_clipboard">复制维基代码到剪贴板</string>
</resources>

View file

@ -331,6 +331,9 @@
<string name="retry">Retry</string>
<string name="showcase_view_got_it_button">Got it!</string>
<string name="showcase_view_whole_nearby_activity">These are the places near you that need pictures to illustrate their Wikipedia articles.\n\nClicking on \'SEARCH THIS AREA\' locks the map and launches a nearby search around that location.</string>
<string name="showcase_view_needs_photo">This place needs a photo.</string>
<string name="showcase_view_has_photo">This place already has a photo.</string>
<string name="showcase_view_no_longer_exists">This place no longer exists.</string>
<string name="showcase_view_list_icon">Tapping this button brings up a list of these places</string>
<string name="showcase_view_plus_fab">You can upload a picture for any place from your gallery or camera</string>

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)