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" testImplementation "org.powermock:powermock-api-mockito2:2.0.0-beta.5"
// Unit testing // Unit testing
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.13'
testImplementation 'org.robolectric:robolectric:4.3' testImplementation 'org.robolectric:robolectric:4.3'
testImplementation 'androidx.test:core:1.2.0' testImplementation 'androidx.test:core:1.2.0'
testImplementation 'com.squareup.okhttp3:mockwebserver:3.12.1' 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.espresso.matcher.ViewMatchers.withText
import androidx.test.rule.ActivityTestRule import androidx.test.rule.ActivityTestRule
import androidx.test.runner.AndroidJUnit4 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.hamcrest.CoreMatchers
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
@ -36,7 +36,9 @@ class AboutActivityTest {
@Test @Test
fun testBuildNumber() { fun testBuildNumber() {
Espresso.onView(ViewMatchers.withId(R.id.about_version)) Espresso.onView(ViewMatchers.withId(R.id.about_version))
.check(ViewAssertions.matches(withText(ConfigUtils.getVersionNameWithSha(getApplicationContext())))) .check(ViewAssertions.matches(
withText(getApplicationContext<CommonsApplication>().getVersionNameWithSha())
))
} }
@Test @Test

View file

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

View file

@ -23,7 +23,7 @@ class WelcomeActivityTest {
@Test @Test
fun ifBetaShowsSkipButton() { fun ifBetaShowsSkipButton() {
if (ConfigUtils.isBetaFlavour()) { if (ConfigUtils.isBetaFlavour) {
onView(withId(R.id.finishTutorialButton)) onView(withId(R.id.finishTutorialButton))
.check(matches(isDisplayed())) .check(matches(isDisplayed()))
} }
@ -31,7 +31,7 @@ class WelcomeActivityTest {
@Test @Test
fun ifProdHidesSkipButton() { fun ifProdHidesSkipButton() {
if (!ConfigUtils.isBetaFlavour()) { if (!ConfigUtils.isBetaFlavour) {
onView(withId(R.id.finishTutorialButton)) onView(withId(R.id.finishTutorialButton))
.check(matches(not(isDisplayed()))) .check(matches(not(isDisplayed())))
} }
@ -39,7 +39,7 @@ class WelcomeActivityTest {
@Test @Test
fun testBetaSkipButton() { fun testBetaSkipButton() {
if (ConfigUtils.isBetaFlavour()) { if (ConfigUtils.isBetaFlavour) {
onView(withId(R.id.finishTutorialButton)) onView(withId(R.id.finishTutorialButton))
.perform(ViewActions.click()) .perform(ViewActions.click())
assert(activityRule.activity.isDestroyed) 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.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.media.MediaClient; import fr.free.nrw.commons.media.MediaClient;
import fr.free.nrw.commons.utils.DialogUtil; import fr.free.nrw.commons.utils.DialogUtil;
import fr.free.nrw.commons.wikidata.WikidataEditService;
import java.util.Locale; import java.util.Locale;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
@ -41,7 +42,8 @@ import org.wikipedia.dataclient.WikiSite;
*/ */
public class ContributionsListFragment extends CommonsDaggerSupportFragment implements 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"; 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) { public Media getMediaAtPosition(final int i) {
return adapter.getContributionForPosition(i).getMedia(); return adapter.getContributionForPosition(i).getMedia();
} }
@ -291,13 +292,14 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
*/ */
@Override @Override
public void onConfirmClicked(@Nullable Contribution contribution, boolean copyWikicode) { public void onConfirmClicked(@Nullable Contribution contribution, boolean copyWikicode) {
if(copyWikicode) { if (copyWikicode) {
String wikicode = contribution.getMedia().getWikiCode(); String wikicode = contribution.getWikiCode();
Utils.copy("wikicode", wikicode, getContext()); Utils.copy("wikicode", wikicode, getContext());
} }
final String url = languageWikipediaSite.mobileUrl() + "/wiki/" + contribution.getWikidataPlace() final String url =
.getWikipediaPageTitle(); languageWikipediaSite.mobileUrl() + "/wiki/" + contribution.getWikidataPlace()
.getWikipediaPageTitle();
Utils.handleWebUrl(getContext(), Uri.parse(url)); Utils.handleWebUrl(getContext(), Uri.parse(url));
} }

View file

@ -127,7 +127,8 @@ public class MainActivity extends NavigationBaseActivity implements FragmentMana
tabLayout.getTabAt(1).setCustomView(nearbyTabLinearLayout); tabLayout.getTabAt(1).setCustomView(nearbyTabLinearLayout);
nearbyInfo.setOnClickListener(view -> 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) .setCancelable(true)
.setPositiveButton(android.R.string.ok, (dialog, id) -> dialog.cancel()) .setPositiveButton(android.R.string.ok, (dialog, id) -> dialog.cancel())
.create() .create()

View file

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

View file

@ -1,33 +1,84 @@
package fr.free.nrw.commons.upload; 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.FileInputStream;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import timber.log.Timber;
@Singleton @Singleton
public class FileUtilsWrapper { public class FileUtilsWrapper {
@Inject @Inject
public FileUtilsWrapper() { public FileUtilsWrapper() {
} }
public String getFileExt(String fileName) { public String getFileExt(String fileName) {
return FileUtils.getFileExt(fileName); return FileUtils.getFileExt(fileName);
} }
public String getSHA1(InputStream is) { public String getSHA1(InputStream is) {
return FileUtils.getSHA1(is); return FileUtils.getSHA1(is);
} }
public FileInputStream getFileInputStream(String filePath) throws FileNotFoundException { public FileInputStream getFileInputStream(String filePath) throws FileNotFoundException {
return FileUtils.getFileInputStream(filePath); return FileUtils.getFileInputStream(filePath);
} }
public String getGeolocationOfFile(String filePath) { public String getGeolocationOfFile(String filePath) {
return FileUtils.getGeolocationOfFile(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.content.Context;
import android.net.Uri; import android.net.Uri;
import androidx.annotation.Nullable;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.upload.UploadService.NotificationUpdateProgressListener; import fr.free.nrw.commons.upload.UploadService.NotificationUpdateProgressListener;
import io.reactivex.Observable; import io.reactivex.Observable;
import java.io.File; 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.Inject;
import javax.inject.Named; import javax.inject.Named;
import javax.inject.Singleton; import javax.inject.Singleton;
@ -16,59 +20,112 @@ import okhttp3.MediaType;
import okhttp3.MultipartBody; import okhttp3.MultipartBody;
import okhttp3.RequestBody; import okhttp3.RequestBody;
import org.wikipedia.csrf.CsrfTokenClient; import org.wikipedia.csrf.CsrfTokenClient;
import timber.log.Timber;
@Singleton @Singleton
public class UploadClient { public class UploadClient {
private final UploadInterface uploadInterface; private final int CHUNK_SIZE = 256 * 1024; // 256 KB
private final CsrfTokenClient csrfTokenClient;
private final PageContentsCreator pageContentsCreator;
@Inject private final UploadInterface uploadInterface;
public UploadClient(UploadInterface uploadInterface, private final CsrfTokenClient csrfTokenClient;
@Named(NAMED_COMMONS_CSRF) CsrfTokenClient csrfTokenClient, private final PageContentsCreator pageContentsCreator;
PageContentsCreator pageContentsCreator) { private final FileUtilsWrapper fileUtilsWrapper;
this.uploadInterface = uploadInterface;
this.csrfTokenClient = csrfTokenClient; @Inject
this.pageContentsCreator = pageContentsCreator; 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, @Nullable
NotificationUpdateProgressListener notificationUpdater) { private RequestBody toRequestBody(@Nullable final String value) {
RequestBody requestBody = RequestBody return value == null ? null : RequestBody.create(okhttp3.MultipartBody.FORM, value);
.create(MediaType.parse(FileUtils.getMimeType(context, Uri.parse(file.getPath()))), file); }
CountingRequestBody countingRequestBody = new CountingRequestBody(requestBody,
(bytesWritten, contentLength) -> notificationUpdater
.onProgress(bytesWritten, contentLength));
MultipartBody.Part filePart = MultipartBody.Part.createFormData("file", filename, countingRequestBody); Observable<UploadResult> uploadFileFromStash(final Context context,
RequestBody fileNameRequestBody = RequestBody.create(okhttp3.MultipartBody.FORM, filename); final Contribution contribution,
RequestBody tokenRequestBody; final String uniqueFileName,
try { final String fileKey) {
tokenRequestBody = RequestBody.create(MultipartBody.FORM, csrfTokenClient.getTokenBlocking()); try {
return uploadInterface.uploadFileToStash(fileNameRequestBody, tokenRequestBody, filePart) return uploadInterface
.map(stashUploadResponse -> stashUploadResponse.getUpload()); .uploadFileFromStash(csrfTokenClient.getTokenBlocking(),
} catch (Throwable throwable) { pageContentsCreator.createFrom(contribution),
throwable.printStackTrace(); CommonsApplication.DEFAULT_EDIT_SUMMARY,
return Observable.error(throwable); uniqueFileName,
} fileKey).map(UploadResponse::getUpload);
} } catch (final Throwable throwable) {
throwable.printStackTrace();
Observable<UploadResult> uploadFileFromStash(Context context, return Observable.error(throwable);
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);
}
} }
}
} }

View file

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

View file

@ -7,6 +7,7 @@ private const val RESULT_SUCCESS = "Success"
data class UploadResult( data class UploadResult(
val result: String, val result: String,
val filekey: String, val filekey: String,
val offset: Int,
val filename: String, val filename: String,
val sessionkey: String, val sessionkey: String,
val imageinfo: ImageInfo 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.CommonsApplicationModule;
import fr.free.nrw.commons.di.CommonsDaggerService; import fr.free.nrw.commons.di.CommonsDaggerService;
import fr.free.nrw.commons.media.MediaClient; import fr.free.nrw.commons.media.MediaClient;
import fr.free.nrw.commons.utils.CommonsDateUtil;
import fr.free.nrw.commons.wikidata.WikidataEditService; import fr.free.nrw.commons.wikidata.WikidataEditService;
import io.reactivex.Completable;
import io.reactivex.Observable; import io.reactivex.Observable;
import io.reactivex.Scheduler; import io.reactivex.Scheduler;
import io.reactivex.Single;
import io.reactivex.disposables.CompositeDisposable; 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.processors.PublishProcessor;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.text.ParseException;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.util.concurrent.Callable;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import javax.inject.Inject; import javax.inject.Inject;
@ -316,7 +308,7 @@ public class UploadService extends CommonsDaggerService {
.add(wikidataEditService.addDepictionsAndCaptions(uploadResult, contribution)); .add(wikidataEditService.addDepictionsAndCaptions(uploadResult, contribution));
WikidataPlace wikidataPlace = contribution.getWikidataPlace(); WikidataPlace wikidataPlace = contribution.getWikidataPlace();
if (wikidataPlace != null && wikidataPlace.getImageValue() == null) { if (wikidataPlace != null && wikidataPlace.getImageValue() == null) {
wikidataEditService.createImageClaim(wikidataPlace, uploadResult); wikidataEditService.createClaim(wikidataPlace, uploadResult.getFilename(), contribution.getCaptions());
} }
saveCompletedContribution(contribution, uploadResult); saveCompletedContribution(contribution, uploadResult);
} }

View file

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

View file

@ -20,49 +20,58 @@ import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable; import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import java.util.ArrayList; 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.Locale;
import java.util.Map;
import java.util.UUID;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
import javax.inject.Singleton; import javax.inject.Singleton;
import org.wikipedia.dataclient.mwapi.MwPostResponse; 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.EditClaim;
import org.wikipedia.wikidata.Snak_partial;
import org.wikipedia.wikidata.Statement_partial;
import org.wikipedia.wikidata.WikiBaseMonolingualTextValue;
import timber.log.Timber; import timber.log.Timber;
/** /**
* This class is meant to handle the Wikidata edits made through the app * This class is meant to handle the Wikidata edits made through the app It will talk with MediaWiki
* It will talk with MediaWiki Apis to make the necessary calls, log the edits and fire listeners * Apis to make the necessary calls, log the edits and fire listeners on successful edits
* on successful edits
*/ */
@Singleton @Singleton
public class WikidataEditService { public class WikidataEditService {
private static final String COMMONS_APP_TAG = "wikimedia-commons-app"; public 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";
private final Context context; private final Context context;
private final WikidataEditListener wikidataEditListener; private final WikidataEditListener wikidataEditListener;
private final JsonKvStore directKvStore; private final JsonKvStore directKvStore;
private final WikiBaseClient wikiBaseClient; private final WikiBaseClient wikiBaseClient;
private final WikidataClient wikidataClient; private final WikidataClient wikidataClient;
private final Gson gson; private final Gson gson;
@Inject @Inject
public WikidataEditService(final Context context, public WikidataEditService(final Context context,
final WikidataEditListener wikidataEditListener, final WikidataEditListener wikidataEditListener,
@Named("default_preferences") final JsonKvStore directKvStore, @Named("default_preferences") final JsonKvStore directKvStore,
final WikiBaseClient wikiBaseClient, final WikiBaseClient wikiBaseClient,
final WikidataClient wikidataClient, final Gson gson) { final WikidataClient wikidataClient, final Gson gson) {
this.context = context; this.context = context;
this.wikidataEditListener = wikidataEditListener; this.wikidataEditListener = wikidataEditListener;
this.directKvStore = directKvStore; this.directKvStore = directKvStore;
this.wikiBaseClient = wikiBaseClient; this.wikiBaseClient = wikiBaseClient;
this.wikidataClient = wikidataClient; this.wikidataClient = wikidataClient;
this.gson = gson; this.gson = gson;
} }
/** /**
* Edits the wikibase entity by adding DEPICTS property. * Edits the wikibase entity by adding DEPICTS property. Adding DEPICTS property requires call to
* Adding DEPICTS property requires call to the wikibase API to set tag against the entity. * the wikibase API to set tag against the entity.
*/ */
@SuppressLint("CheckResult") @SuppressLint("CheckResult")
private Observable<Boolean> addDepictsProperty(final String fileEntityId, private Observable<Boolean> addDepictsProperty(final String fileEntityId,
@ -70,7 +79,7 @@ public class WikidataEditService {
final EditClaim data = editClaim( final EditClaim data = editClaim(
ConfigUtils.isBetaFlavour() ? "Q10" // Wikipedia:Sandbox (Q10) ConfigUtils.isBetaFlavour() ? "Q10" // Wikipedia:Sandbox (Q10)
: depictedItem.getId() : depictedItem.getId()
); );
return wikiBaseClient.postEditEntity(PAGE_ID_PREFIX + fileEntityId, gson.toJson(data)) 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); Timber.d("Unable to set DEPICTS property for %s", fileEntityId);
} }
}) })
.doOnError( throwable -> { .doOnError(throwable -> {
Timber.e(throwable, "Error occurred while setting DEPICTS property"); Timber.e(throwable, "Error occurred while setting DEPICTS property");
ViewUtil.showLongToast(context, throwable.toString()); ViewUtil.showLongToast(context, throwable.toString());
}) })
.subscribeOn(Schedulers.io()); .subscribeOn(Schedulers.io());
} }
private EditClaim editClaim(final String entityId) { private EditClaim editClaim(final String entityId) {
return EditClaim.from(entityId, WikidataProperties.DEPICTS.getPropertyName()); return EditClaim.from(entityId, WikidataProperties.DEPICTS.getPropertyName());
} }
/** /**
* Show a success toast when the edit is made successfully * Show a success toast when the edit is made successfully
*/ */
private void showSuccessToast(final String wikiItemName) { private void showSuccessToast(final String wikiItemName) {
final String successStringTemplate = context.getString(R.string.successful_wikidata_edit); final String successStringTemplate = context.getString(R.string.successful_wikidata_edit);
final String successMessage = String.format(Locale.getDefault(), successStringTemplate, wikiItemName); final String successMessage = String
ViewUtil.showLongToast(context, successMessage); .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 * @param fileEntityId
*/ * @return
*/
@SuppressLint("CheckResult") @SuppressLint("CheckResult")
private Observable<Boolean> addCaption(final long fileEntityId, final String languageCode, private Observable<Boolean> addCaption(final long fileEntityId, final String languageCode,
final String captionValue) { final String captionValue) {
return wikiBaseClient.addLabelstoWikidata(fileEntityId, languageCode, captionValue) return wikiBaseClient.addLabelstoWikidata(fileEntityId, languageCode, captionValue)
.doOnNext(mwPostResponse -> onAddCaptionResponse(fileEntityId, mwPostResponse) ) .doOnNext(mwPostResponse -> onAddCaptionResponse(fileEntityId, mwPostResponse))
.doOnError(throwable -> { .doOnError(throwable -> {
Timber.e(throwable, "Error occurred while setting Captions"); Timber.e(throwable, "Error occurred while setting Captions");
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure)); ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
}) })
.map(mwPostResponse -> mwPostResponse != null); .map(mwPostResponse -> mwPostResponse != null);
} }
private void onAddCaptionResponse(Long fileEntityId, MwPostResponse response) { private void onAddCaptionResponse(Long fileEntityId, MwPostResponse response) {
if (response != null) { 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))) { 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; return;
} }
editWikidataImageProperty(wikidataPlace, imageUpload); addImageAndMediaLegends(wikidataPlace, fileName, captions);
} }
@SuppressLint("CheckResult") public void addImageAndMediaLegends(final WikidataItem wikidataItem, final String fileName,
private void editWikidataImageProperty(final WikidataItem wikidataItem, final UploadResult imageUpload) { final Map<String, String> captions) {
wikidataClient.createImageClaim(wikidataItem, String.format("\"%s\"", imageUpload.getFilename())) final Snak_partial p18 = new Snak_partial("value", WikidataProperties.IMAGE.getPropertyName(),
.flatMap(revisionId -> { new ValueString(fileName.replace("File:", "")));
if (revisionId != -1) {
return wikidataClient.addEditTag(revisionId, COMMONS_APP_TAG, COMMONS_APP_EDIT_REASON); final List<Snak_partial> snaks = new ArrayList<>();
} for (final Map.Entry<String, String> entry : captions.entrySet()) {
throw new RuntimeException("Unable to edit wikidata item"); snaks.add(new Snak_partial("value",
}) WikidataProperties.MEDIA_LEGENDS.getPropertyName(), new DataValue.MonoLingualText(
.subscribeOn(Schedulers.io()) 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()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(revisionId -> handleImageClaimResult(wikidataItem, String.valueOf(revisionId)), throwable -> { .subscribe(revisionId -> handleImageClaimResult(wikidataItem, String.valueOf(revisionId)),
Timber.e(throwable, "Error occurred while making claim"); throwable -> {
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure)); 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) { private void handleImageClaimResult(final WikidataItem wikidataItem, final String revisionId) {
@ -185,9 +208,9 @@ public class WikidataEditService {
} }
} }
).subscribe( ).subscribe(
success -> Timber.d("edit response: %s", success), success -> Timber.d("edit response: %s", success),
throwable -> Timber.e(throwable, "posting edits failed") throwable -> Timber.e(throwable, "posting edits failed")
); );
} }
private Observable<Boolean> captionEdits(Contribution contribution, Long fileEntityId) { private Observable<Boolean> captionEdits(Contribution contribution, Long fileEntityId) {
@ -202,6 +225,6 @@ public class WikidataEditService {
depictedItems.add(wikidataPlace); depictedItems.add(wikidataPlace);
} }
return Observable.fromIterable(depictedItems) 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 androidx.annotation.NonNull;
import com.google.gson.JsonObject;
import org.wikipedia.dataclient.mwapi.MwQueryResponse; import org.wikipedia.dataclient.mwapi.MwQueryResponse;
import fr.free.nrw.commons.wikidata.model.AddEditTagResponse; import fr.free.nrw.commons.wikidata.model.AddEditTagResponse;
@ -20,35 +21,21 @@ import static org.wikipedia.dataclient.Service.MW_API_PREFIX;
public interface WikidataInterface { public interface WikidataInterface {
/** /**
* Wikidata create claim API. Posts a new claim for the given entity ID * Get edit token for wikidata wiki site
*/ */
@Headers("Cache-Control: no-cache") @Headers("Cache-Control: no-cache")
@POST("w/api.php?format=json&errorformat=plaintext&action=wbcreateclaim&errorlang=uselang") @GET(MW_API_PREFIX + "action=query&meta=tokens&type=csrf")
@Multipart @NonNull
Observable<WbCreateClaimResponse> postCreateClaim(@NonNull @Part("entity") RequestBody entity, Observable<MwQueryResponse> getCsrfToken();
@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 * Wikidata create claim API. Posts a new claim for the given entity ID
*/ */
@Headers("Cache-Control: no-cache") @Headers("Cache-Control: no-cache")
@POST(MW_API_PREFIX + "action=tag") @POST("w/api.php?format=json&action=wbsetclaim")
@FormUrlEncoded @FormUrlEncoded
Observable<AddEditTagResponse> addEditTag(@NonNull @Field("revid") String revId, Observable<WbCreateClaimResponse> postSetClaim(@NonNull @Field("claim") String request,
@NonNull @Field("add") String tagName, @NonNull @Field("tags") String tags,
@NonNull @Field("reason") String reason, @NonNull @Field("token") String token);
@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();
} }

View file

@ -6,5 +6,6 @@ enum class WikidataProperties(val propertyName: String) {
IMAGE("P18"), IMAGE("P18"),
DEPICTS(BuildConfig.DEPICTS_PROPERTY), DEPICTS(BuildConfig.DEPICTS_PROPERTY),
COMMONS_CATEGORY("P373"), 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 * Abijeet Patro
* Amirsara * Amirsara
* Arash.pt * Arash.pt
* BaRaN6161 TURK
* Ebraminio * Ebraminio
* Eshagh79 * Eshagh79
* FarsiNevis * FarsiNevis
@ -28,7 +29,11 @@
<item quantity="one">%1$d پرونده در حال بارگذاری</item> <item quantity="one">%1$d پرونده در حال بارگذاری</item>
<item quantity="other">%1$d پرونده در حال بارگذاری</item> <item quantity="other">%1$d پرونده در حال بارگذاری</item>
</plurals> </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"> <plurals name="starting_multiple_uploads">
<item quantity="one">شروع %1$d بارگذاری پرونده</item> <item quantity="one">شروع %1$d بارگذاری پرونده</item>
<item quantity="other">شروع بارگذاری %1$d پرونده</item> <item quantity="other">شروع بارگذاری %1$d پرونده</item>

View file

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

View file

@ -33,6 +33,7 @@
</plurals> </plurals>
<plurals name="multiple_uploads_title"> <plurals name="multiple_uploads_title">
<item quantity="one">%1$d개 업로드</item> <item quantity="one">%1$d개 업로드</item>
<item quantity="other">%1$d개 업로드</item>
</plurals> </plurals>
<plurals name="share_license_summary"> <plurals name="share_license_summary">
<item quantity="one">이 그림은 %1$s에 따라 사용이 허가됩니다</item> <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="provider_contributions">ߒ ߠߊ߫ ߟߊ߬ߦߟߍ߬ߣߍ߲ ߠߎ߬</string>
<string name="menu_share">ߊ߬ ߟߊߖߍ߲ߛߍ߲߫</string> <string name="menu_share">ߊ߬ ߟߊߖߍ߲ߛߍ߲߫</string>
<string name="menu_open_in_browser">ߊ߬ ߘߐߜߍ߫ ߛߏ߲߯ߓߊߟߊ߲ ߠߊ߫</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_description_hint">ߞߊ߲߬ߛߓߍߟߌ</string>
<string name="share_caption_hint">ߝߍ߬ߛߓߍߟߌ (ߞߐߘߊ߲ ߦߋ߫ ߛߓߍߘߋ߲߫ ߂߅߅ ߟߋ߬ ߘߌ߫)</string>
<string name="login_failed_network">ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߛߐ߲߬ߣߍ߲߫ ߕߍ߫ ߞߍ߫ ߟߊ߫ - ߞߙߏߝߏ ߟߊ߫ ߗߌߙߏ߲ߠߌ߲</string> <string name="login_failed_network">ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߛߐ߲߬ߣߍ߲߫ ߕߍ߫ ߞߍ߫ ߟߊ߫ - ߞߙߏߝߏ ߟߊ߫ ߗߌߙߏ߲ߠߌ߲</string>
<string name="login_failed_wrong_credentials">ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߛߐ߲߬ߣߍ߲߫ ߕߍ߫ ߞߍ߫ ߟߊ߫ - ߌ ߟߊ߫ ߕߐ߯ ߟߊߓߊ߯ߕߊ ߣߌ߫ ߕߊ߬ߡߌ߲߬ߞߊ߲ ߠߎ߬ ߡߊߝߟߍ߫ ߖߊ߰ߣߌ߲߬</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="login_failed_generic">ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߓߘߊ߫ ߗߌߙߏ߲߫</string>
<string name="share_upload_button">ߊ߬ ߟߊߦߟߍ߬</string> <string name="share_upload_button">ߊ߬ ߟߊߦߟߍ߬</string>
<string name="multiple_share_base_title">ߟߊ߬ߘߏ߲߬ߠߌ߲ ߣߌ߲߬ ߕߐ߯ߟߊ߫</string> <string name="multiple_share_base_title">ߟߊ߬ߘߏ߲߬ߠߌ߲ ߣߌ߲߬ ߕߐ߯ߟߊ߫</string>
@ -74,7 +78,7 @@
<string name="title_activity_featured_images">ߟߊߓߊ߯ߙߊߟߌ ߖߌ߬ߦߊ߬ߓߍ ߟߎ߬</string> <string name="title_activity_featured_images">ߟߊߓߊ߯ߙߊߟߌ ߖߌ߬ߦߊ߬ߓߍ ߟߎ߬</string>
<string name="title_activity_category_details">ߦߌߟߡߊ</string> <string name="title_activity_category_details">ߦߌߟߡߊ</string>
<string name="menu_about">ߞߊ߬ ߓߍ߲߬</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="title_activity_about">ߞߊ߬ ߓߍ߲߬</string>
<string name="menu_feedback">ߞߙߐ߬ߛߌ߬ߕߊ ߗߋ߫ (ߢ:ߞߏ߲ߘߏ ߟߊ߫)</string> <string name="menu_feedback">ߞߙߐ߬ߛߌ߬ߕߊ ߗߋ߫ (ߢ:ߞߏ߲ߘߏ ߟߊ߫)</string>
<string name="no_email_client">ߢߎߡߍߙߋ߲ߞߏ߲ߘߏ ߛߊ߲߬ߠߌ߲߬ߢߐ߲߰ ߡߊߞߍߣߍ߲߫ ߕߴߦߋ߲߬</string> <string name="no_email_client">ߢߎߡߍߙߋ߲ߞߏ߲ߘߏ ߛߊ߲߬ߠߌ߲߬ߢߐ߲߰ ߡߊߞߍߣߍ߲߫ ߕߴߦߋ߲߬</string>
@ -107,15 +111,21 @@
<string name="detail_panel_cats_label">ߦߌߟߡߊ ߟߎ߬</string> <string name="detail_panel_cats_label">ߦߌߟߡߊ ߟߎ߬</string>
<string name="detail_panel_cats_loading">ߟߊ߬ߢߎ߲߬ߠߌ߲ ߦߵߌ ߘߐ߫...</string> <string name="detail_panel_cats_loading">ߟߊ߬ߢߎ߲߬ߠߌ߲ ߦߵߌ ߘߐ߫...</string>
<string name="detail_panel_cats_none">ߊ߬ ߡߊ߫ ߓߊߕߐ߬ߡߐ߲߬</string> <string name="detail_panel_cats_none">ߊ߬ ߡߊ߫ ߓߊߕߐ߬ߡߐ߲߬</string>
<string name="detail_caption_empty">ߝߍ߬ߛߓߍߟߌ߫ ߕߍ߫ ߦߋ߲߬</string>
<string name="detail_description_empty">ߞߊ߲߬ߛߓߍߟߌ߫ ߕߴߦߋ߲߬</string> <string name="detail_description_empty">ߞߊ߲߬ߛߓߍߟߌ߫ ߕߴߦߋ߲߬</string>
<string name="detail_discussion_empty">ߘߊߘߐߖߊߥߏ߫ ߕߴߦߋ߲߬</string> <string name="detail_discussion_empty">ߘߊߘߐߖߊߥߏ߫ ߕߴߦߋ߲߬</string>
<string name="ok">ߏ߬ߞߍ߫</string> <string name="ok">ߏ߬ߞߍ߫</string>
<string name="title_activity_nearby">ߛߌ߰ߢߐ߲߰ ߦߙߐ ߟߎ߬</string> <string name="title_activity_nearby">ߛߌ߰ߢߐ߲߰ ߦߙߐ ߟߎ߬</string>
<string name="no_nearby">ߛߌ߰ߢߐ߲߰ ߦߙߐ߫ ߡߊ߫ ߛߐ߬ߘߐ߲߫</string> <string name="no_nearby">ߛߌ߰ߢߐ߲߰ ߦߙߐ߫ ߡߊ߫ ߛߐ߬ߘߐ߲߫</string>
<string name="warning">ߖߊ߲߬ߓߌ߬ߟߊ߬ߟߌ</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="yes">ߐ߲߬ߐ߲߬ߐ߲߫</string>
<string name="no">ߍ߲߬ߍ߲߫</string> <string name="no">ߍ߲߬ߍ߲߫</string>
<string name="media_detail_caption">ߝߍ߬ߛߓߍߟߌ</string>
<string name="media_detail_title">ߞߎ߲߬ߕߐ߮</string> <string name="media_detail_title">ߞߎ߲߬ߕߐ߮</string>
<string name="media_detail_depiction">ߞߐߦߌߘߊߟߌ</string>
<string name="media_detail_description">ߞߊ߲߬ߛߓߍߟߌ</string> <string name="media_detail_description">ߞߊ߲߬ߛߓߍߟߌ</string>
<string name="media_detail_discussion">ߘߊߘߐߖߊߥߏ</string> <string name="media_detail_discussion">ߘߊߘߐߖߊߥߏ</string>
<string name="media_detail_author">ߛߓߍߦߟߊ</string> <string name="media_detail_author">ߛߓߍߦߟߊ</string>
@ -166,11 +176,15 @@
<string name="upload_problem_different_geolocation">ߖߌ߬ߦߊ߬ߓߍ ߣߌ߲߬ ߕߊ߬ߣߍ߲߬ ߦߋ߫ ߦߙߐ߫ ߜߘߍ߫ ߟߋ߬</string> <string name="upload_problem_different_geolocation">ߖߌ߬ߦߊ߬ߓߍ ߣߌ߲߬ ߕߊ߬ߣߍ߲߬ ߦߋ߫ ߦߙߐ߫ ߜߘߍ߫ ߟߋ߬</string>
<string name="upload_problem_fbmd">ߌ ߦߋ߫ ߖߌ߬ߦߊ߬ߓߍ ߟߋ߬ ߟߊߦߍ߬ߟߍ߫ ߖߊ߰ߣߌ߲߫ ߌ ߖߍ߬ߘߍ ߞߊ߬ ߡߍ߲ ߠߎ߬ ߕߊ߬. ߒ߬ߞߊ߬ ߌ ߞߊ߫ ߖߌ߬ߦߊ߬ߓߍ߫ ߟߊߦߍ߬ߟߍ߫ ߡߎ߰ߡߍ߫ ߌ ߞߊ߬ ߡߍ߲ ߠߎ߬ ߕߊ߬ ߡߐ߱ ߟߎ߬ ߟߊ߫ ߝߋߛߑߓߎߞ ߞߊ߲߬.</string> <string name="upload_problem_fbmd">ߌ ߦߋ߫ ߖߌ߬ߦߊ߬ߓߍ ߟߋ߬ ߟߊߦߍ߬ߟߍ߫ ߖߊ߰ߣߌ߲߫ ߌ ߖߍ߬ߘߍ ߞߊ߬ ߡߍ߲ ߠߎ߬ ߕߊ߬. ߒ߬ߞߊ߬ ߌ ߞߊ߫ ߖߌ߬ߦߊ߬ߓߍ߫ ߟߊߦߍ߬ߟߍ߫ ߡߎ߰ߡߍ߫ ߌ ߞߊ߬ ߡߍ߲ ߠߎ߬ ߕߊ߬ ߡߐ߱ ߟߎ߬ ߟߊ߫ ߝߋߛߑߓߎߞ ߞߊ߲߬.</string>
<string name="upload_problem_do_you_continue">ߌ ߦߴߊ߬ ߝߍ߬ ߞߊ߬ ߖߌ߬ߦߊ߬ߓߍ ߣߌ߲߬ ߠߊߦߟߍ߬ ߡߎߣߎ߲߬؟</string> <string name="upload_problem_do_you_continue">ߌ ߦߴߊ߬ ߝߍ߬ ߞߊ߬ ߖߌ߬ߦߊ߬ߓߍ ߣߌ߲߬ ߠߊߦߟߍ߬ ߡߎߣߎ߲߬؟</string>
<string name="upload_problem_image">ߝߙߋߞߋ ߓߘߊ߫ ߦߋ߫ ߖߌ߬ߦߊ߬ߓߍ ߟߊ߫</string>
<string name="internet_downloaded">ߖߊ߰ߣߌ߲߬ ߌ ߦߋ߫ ߖߌ߬ߦߊ߬ߓߍ߬ ߟߎ߫ ߟߋ߬ ߟߊߦߟߍ߬ ߌ ߖߍ߬ߘߍ ߞߊ߬ ߡߍ߲ ߠߎ߬ ߕߊ߬. ߞߏ߬ߣߌ߲߬ ߌ ߞߊߣߊ߬ ߖߌ߬ߦߊ߬ߓߍ߬ ߟߊߦߟߍ߬ ߌ ߣߊ߬ ߡߍ߲ ߠߊߖߌ߰ ߟߴߌ ߞߎ߲߬ ߓߟߐߟߐ ߟߊ߫.</string> <string name="internet_downloaded">ߖߊ߰ߣߌ߲߬ ߌ ߦߋ߫ ߖߌ߬ߦߊ߬ߓߍ߬ ߟߎ߫ ߟߋ߬ ߟߊߦߟߍ߬ ߌ ߖߍ߬ߘߍ ߞߊ߬ ߡߍ߲ ߠߎ߬ ߕߊ߬. ߞߏ߬ߣߌ߲߬ ߌ ߞߊߣߊ߬ ߖߌ߬ߦߊ߬ߓߍ߬ ߟߊߦߟߍ߬ ߌ ߣߊ߬ ߡߍ߲ ߠߊߖߌ߰ ߟߴߌ ߞߎ߲߬ ߓߟߐߟߐ ߟߊ߫.</string>
<string name="give_permission">ߟߊ߬ߘߌ߬ߢߍ߬ߟߌ ߦߌ߬ߘߊ߬</string> <string name="give_permission">ߟߊ߬ߘߌ߬ߢߍ߬ߟߌ ߦߌ߬ߘߊ߬</string>
<string name="login_to_your_account">ߌ ߜߊ߲߬ߞߎ߲߫ ߌ ߟߊ߫ ߖߊ߬ߕߋ߬ߘߊ ߟߊ߫.</string> <string name="login_to_your_account">ߌ ߜߊ߲߬ߞߎ߲߫ ߌ ߟߊ߫ ߖߊ߬ߕߋ߬ߘߊ ߟߊ߫.</string>
<string name="no_web_browser">ߓߟߐߟߐ ߛߏ߲߯ߓߊߟߊ߲߫ ߡߊ߫ ߛߐ߬ߘߐ߲߬ ߞߊ߬ URL ߟߊߞߊ߬</string> <string name="no_web_browser">ߓߟߐߟߐ ߛߏ߲߯ߓߊߟߊ߲߫ ߡߊ߫ ߛߐ߬ߘߐ߲߬ ߞߊ߬ URL ߟߊߞߊ߬</string>
<string name="null_url">ߝߎ߬ߕߎ߲߬ߕߌ߹ 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="view_browser">ߊ߬ ߘߐߜߍ߫ ߛߏ߲߯ߓߊߟߊ߲ ߠߊ߫</string>
<string name="skip_login">ߊ߬ ߟߊߜߊ߲߫</string> <string name="skip_login">ߊ߬ ߟߊߜߊ߲߫</string>
<string name="navigation_item_login">ߌ ߜߊ߲߬ߞߎ߲߬</string> <string name="navigation_item_login">ߌ ߜߊ߲߬ߞߎ߲߬</string>
@ -187,11 +201,11 @@
<string name="nearby_wikidata">ߥߞߌߘߕߊ</string> <string name="nearby_wikidata">ߥߞߌߘߕߊ</string>
<string name="nearby_wikipedia">ߥߞߌߔߋߘߌߦߊ߫</string> <string name="nearby_wikipedia">ߥߞߌߔߋߘߌߦߊ߫</string>
<string name="nearby_commons">ߞߐߡߐ߲</string> <string name="nearby_commons">ߞߐߡߐ߲</string>
<string name="about_rate_us" fuzzy="true">&lt;u&gt;ߊ߲ ߡߐ߬ߟߐ߲ ߦߌ߬ߘߊ߬&lt;/u&gt;</string> <string name="about_rate_us">ߡߐ߬ߟߐ߲ ߘߴߊ߲ ߡߊ߬</string>
<string name="about_faq" fuzzy="true">&lt;u&gt;ߢ.ߡ&lt;/u&gt;</string> <string name="about_faq">ߢ.ߡ</string>
<string name="no_internet">ߓߟߐߟߐ߫ ߕߴߦߋ߲߬</string> <string name="no_internet">ߓߟߐߟߐ߫ ߕߴߦߋ߲߬</string>
<string name="internet_established">ߓߟߐߟߐ ߦߋ߫ ߦߋ߲߬</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_title">ߞߊ߲ ߠߎ߬</string>
<string name="about_translate_message">ߞߊ߲ ߘߏ߫ ߓߊߕߐ߬ߡߐ߲߬ ߌ ߦߴߊ߬ ߝߍ߬ ߞߵߊ߬ ߘߟߊߡߌ߬ߘߊ߬ ߡߍ߲ ߘߐ߫.</string> <string name="about_translate_message">ߞߊ߲ ߘߏ߫ ߓߊߕߐ߬ߡߐ߲߬ ߌ ߦߴߊ߬ ߝߍ߬ ߞߵߊ߬ ߘߟߊߡߌ߬ߘߊ߬ ߡߍ߲ ߘߐ߫.</string>
<string name="about_translate_cancel">ߊ߬ ߘߐߛߊ߬</string> <string name="about_translate_cancel">ߊ߬ ߘߐߛߊ߬</string>
@ -274,6 +288,11 @@
<string name="review_thanks">ߊ߬ ߟߐ߯ ߦߴߌ ߟߊ߫ ߞߊ߬ ߓߟߏߓߌߟߊߢߐ߲߯ߞߊ߲ߠߊ ߟߎ߬ ߞߎߟߎ߲ߖߋ߫ ߓߊ߬؟</string> <string name="review_thanks">ߊ߬ ߟߐ߯ ߦߴߌ ߟߊ߫ ߞߊ߬ ߓߟߏߓߌߟߊߢߐ߲߯ߞߊ߲ߠߊ ߟߎ߬ ߞߎߟߎ߲ߖߋ߫ ߓߊ߬؟</string>
<string name="review_no_category">ߐ߲߬ߤߐ߲߯߹ ߣߌ߲߬ ߝߊ߲߭ ߡߊ߫ ߦߌߟߡߊߦߊ߫ ߟߋ߬߹</string> <string name="review_no_category">ߐ߲߬ߤߐ߲߯߹ ߣߌ߲߬ ߝߊ߲߭ ߡߊ߫ ߦߌߟߡߊߦߊ߫ ߟߋ߬߹</string>
<string name="review_category_explanation">ߖߌ߬ߦߊ߬ߓߍ ߣߌ߲߬ ߦߋ߫ %1$s ߦߌߟߡߊ ߟߎ߬ ߞߘߐ߫.</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_yes_button_text">ߒ߬ߒ߫߸ ߓߊߦߟߍߡߊ߲ ߤߊߞߍ ߡߊ߫ ߕߌߢߍ߫</string>
<string name="review_copyright_no_button_text">ߊ߬ ߞߍߣߍ߲߫ ߦߴߊ߬ ߓߍ߲߬ߣߍ߲߫</string> <string name="review_copyright_no_button_text">ߊ߬ ߞߍߣߍ߲߫ ߦߴߊ߬ ߓߍ߲߬ߣߍ߲߫</string>
<string name="review_thanks_yes_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_copyright">ߓߊߦߟߍߡߊ߲ ߤߊߞߍ</string>
<string name="exif_tag_name_location">ߘߌ߲߬ߞߌߙߊ</string> <string name="exif_tag_name_location">ߘߌ߲߬ߞߌߙߊ</string>
<string name="exif_tag_name_cameraModel">ߖߌ߬ߦߊ߬ߕߊ߬ߟߊ߲ ߛߎ߮ߦߊ</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="dialog_box_text_nomination">ߡߎ߲߬ߠߊ߫ %1$s ߖߏ߬ߛߌ߬ߕߐ߫؟</string>
<string name="review_is_uploaded_by">%1$s ߟߊߦߟߍ߬ߣߍ߲߬ ߦߋ߫: %2$s ߟߋ߬ ߓߟߏ߫</string> <string name="review_is_uploaded_by">%1$s ߟߊߦߟߍ߬ߣߍ߲߬ ߦߋ߫: %2$s ߟߋ߬ ߓߟߏ߫</string>
<string name="default_description_language">ߞߊ߲߬ߛߓߍߟߌ ߞߍ߫ ߞߊ߲ ߓߊߖߎߡߊ</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">ߕߐ߯ߦߊߟߌ ߖߏ߰ߛߌ߬ߟߌ ߞߊ߲ߡߊ߬</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_message_if">%1$s ߕߐ߯ߦߊߟߌ ߦߴߌ ߘߐ߫ ߖߏ߬ߛߌ߬ߟߌ ߞߊ߲ߡߊ߬</string>
<string name="delete_helper_show_deletion_title_failed">ߊ߬ ߓߘߊ߫ ߗߌߙߏ߲߫</string> <string name="delete_helper_show_deletion_title_failed">ߊ߬ ߓߘߊ߫ ߗߌߙߏ߲߫</string>
<string name="delete_helper_show_deletion_message_else">ߌ ߕߍ߫ ߛߋ߫ ߖߏ߰ߛߌ߬ߟߌ ߡߊߢߌߣߌ߲߫ ߠߊ߫</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="delete_helper_ask_alert_set_positive_button_reason">ߓߊߏ߬ ߊ߬ ߦߋ߫</string>
<string name="share_image_via">ߖߌ߬ߦߊ߬ߓߍ ߟߊߖߍ߲ߛߍ߲߫ ߞߊߕߙߍ߬</string> <string name="share_image_via">ߖߌ߬ߦߊ߬ߓߍ ߟߊߖߍ߲ߛߍ߲߫ ߞߊߕߙߍ߬</string>
<string name="no_achievements_yet">ߌ ߡߊ߫ ߓߟߏߓߌߟߊߢߐ߲߯ߞߊ߲ ߛߌ߫ ߞߍ߫ ߡߎߣߎ߲߬</string> <string name="no_achievements_yet">ߌ ߡߊ߫ ߓߟߏߓߌߟߊߢߐ߲߯ߞߊ߲ ߛߌ߫ ߞߍ߫ ߡߎߣߎ߲߬</string>
<string name="account_created">ߖߊ߬ߕߋ߬ߘߊ ߓߘߊ߫ ߛߌ߲ߘߌ߫߹</string>
<string name="some_error">ߝߎ߬ߕߎ߲߬ߕߌ ߘߏ߫ ߕߘߍ߬ ߦߋ߫ ߦߋ߲߬߹</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> </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="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="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="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> </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_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="preference_author_name">Özel yazar adı</string>
<string name="contributions_fragment">Katkılar</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="notifications">Bildirimler</string>
<string name="read_notifications">Bildirimler (okunmuş)</string> <string name="read_notifications">Bildirimler (okunmuş)</string>
<string name="display_nearby_notification">Yakınımdakiler bildirimi görüntüle</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"> <plurals name="starting_multiple_uploads">
<item quantity="one">იჭყაფუ %1$d ეხარგუა</item> <item quantity="one">იჭყაფუ %1$d ეხარგუა</item>
<item quantity="few">იჭყაფუ %1$d ეხარგუა</item> <item quantity="few">იჭყაფუ %1$d ეხარგუა</item>
<item quantity="other">იჭყაფუ %1$d ეხარგუა</item>
</plurals> </plurals>
<plurals name="multiple_uploads_title"> <plurals name="multiple_uploads_title">
<item quantity="one">%1$d ეხარგუა</item> <item quantity="one">%1$d ეხარგუა</item>
<item quantity="few">%1$d ეხარგუა</item> <item quantity="few">%1$d ეხარგუა</item>
<item quantity="other">%1$d ეხარგუა</item>
</plurals> </plurals>
<plurals name="share_license_summary"> <plurals name="share_license_summary">
<item quantity="one">თე სურათი გიბჟინუ %1$s ლიცენზიათ</item> <item quantity="one">თე სურათი გიბჟინუ %1$s ლიცენზიათ</item>

View file

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

View file

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

View file

@ -331,6 +331,9 @@
<string name="retry">Retry</string> <string name="retry">Retry</string>
<string name="showcase_view_got_it_button">Got it!</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_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_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> <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 package fr.free.nrw.commons.wikidata
import com.nhaarman.mockitokotlin2.mock import com.google.gson.Gson
import fr.free.nrw.commons.wikidata.model.AddEditTagResponse 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 io.reactivex.Observable
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
@ -14,12 +16,16 @@ import org.mockito.Mockito.mock
import org.mockito.MockitoAnnotations import org.mockito.MockitoAnnotations
import org.wikipedia.dataclient.mwapi.MwQueryResponse import org.wikipedia.dataclient.mwapi.MwQueryResponse
import org.wikipedia.dataclient.mwapi.MwQueryResult import org.wikipedia.dataclient.mwapi.MwQueryResult
import org.wikipedia.wikidata.Statement_partial
class WikidataClientTest { class WikidataClientTest {
@Mock @Mock
internal var wikidataInterface: WikidataInterface? = null internal var wikidataInterface: WikidataInterface? = null
@Mock
internal var gson: Gson? = null
@InjectMocks @InjectMocks
var wikidataClient: WikidataClient? = null var wikidataClient: WikidataClient? = null
@ -35,26 +41,18 @@ class WikidataClientTest {
.thenReturn(Observable.just(mwQueryResponse)) .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 @Test
fun addEditTag() { fun addEditTag() {
`when`(wikidataInterface!!.addEditTag(anyString(), anyString(), anyString(), anyString())) val response = mock(WbCreateClaimResponse::class.java)
.thenReturn(Observable.just(mock(AddEditTagResponse::class.java))) val pageInfo = mock(PageInfo::class.java)
wikidataClient!!.addEditTag(1L, "test", "test") 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 package fr.free.nrw.commons.wikidata
import android.content.Context import android.content.Context
import com.google.gson.Gson
import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.verifyZeroInteractions import com.nhaarman.mockitokotlin2.verifyZeroInteractions
import com.nhaarman.mockitokotlin2.whenever import com.nhaarman.mockitokotlin2.whenever
import fr.free.nrw.commons.kvstore.JsonKvStore import fr.free.nrw.commons.kvstore.JsonKvStore
import fr.free.nrw.commons.upload.UploadResult import fr.free.nrw.commons.upload.UploadResult
import fr.free.nrw.commons.upload.WikidataPlace import fr.free.nrw.commons.upload.WikidataPlace
import fr.free.nrw.commons.wikidata.model.AddEditTagResponse
import io.reactivex.Observable import io.reactivex.Observable
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
@ -15,7 +15,6 @@ import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.anyString import org.mockito.ArgumentMatchers.anyString
import org.mockito.InjectMocks import org.mockito.InjectMocks
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito.*
import org.mockito.MockitoAnnotations import org.mockito.MockitoAnnotations
class WikidataEditServiceTest { class WikidataEditServiceTest {
@ -31,6 +30,9 @@ class WikidataEditServiceTest {
@Mock @Mock
internal lateinit var wikibaseClient: WikiBaseClient internal lateinit var wikibaseClient: WikiBaseClient
@Mock
internal lateinit var gson: Gson
@InjectMocks @InjectMocks
lateinit var wikidataEditService: WikidataEditService lateinit var wikidataEditService: WikidataEditService
@ -44,7 +46,7 @@ class WikidataEditServiceTest {
fun noClaimsWhenLocationIsNotCorrect() { fun noClaimsWhenLocationIsNotCorrect() {
whenever(directKvStore.getBoolean("Picture_Has_Correct_Location", true)) whenever(directKvStore.getBoolean("Picture_Has_Correct_Location", true))
.thenReturn(false) .thenReturn(false)
wikidataEditService.createImageClaim(mock(), mock()) wikidataEditService.createClaim(mock(), "Test.jpg", hashMapOf())
verifyZeroInteractions(wikidataClient) verifyZeroInteractions(wikidataClient)
} }
@ -52,15 +54,16 @@ class WikidataEditServiceTest {
fun createImageClaim() { fun createImageClaim() {
whenever(directKvStore.getBoolean("Picture_Has_Correct_Location", true)) whenever(directKvStore.getBoolean("Picture_Has_Correct_Location", true))
.thenReturn(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)) 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>() val uploadResult = mock<UploadResult>()
whenever(uploadResult.filename).thenReturn("file") whenever(uploadResult.filename).thenReturn("file")
wikidataEditService.createImageClaim(wikidataPlace, uploadResult) wikidataEditService.createClaim(
verify(wikidataClient, times(1)).createImageClaim(wikidataPlace, """"file"""") 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(GlobeCoordinate_partial::class.java, GlobeCoordinate_partial.TYPE)
.registerSubtype(Time_partial::class.java, Time_partial.TYPE) .registerSubtype(Time_partial::class.java, Time_partial.TYPE)
.registerSubtype(Quantity_partial::class.java, Quantity_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": { // "value": {
@ -87,7 +87,7 @@ sealed class DataValue(val type: String) {
// "language": "ko" // "language": "ko"
// } // }
// } // }
class MonoLingualText_partial() : DataValue(TYPE) { class MonoLingualText(val value: WikiBaseMonolingualTextValue) : DataValue(TYPE) {
companion object { companion object {
const val TYPE = "monolingualtext" const val TYPE = "monolingualtext"
} }

View file

@ -21,5 +21,8 @@ import com.google.gson.annotations.SerializedName
data class Statement_partial( data class Statement_partial(
@SerializedName("mainsnak") val mainSnak: Snak_partial, @SerializedName("mainsnak") val mainSnak: Snak_partial,
val type: String, 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)