diff --git a/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java b/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java index 58f574595..1c1fa925b 100644 --- a/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java +++ b/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java @@ -4,6 +4,7 @@ import static fr.free.nrw.commons.notification.NotificationHelper.NOTIFICATION_D import android.annotation.SuppressLint; import android.content.Context; +import static fr.free.nrw.commons.utils.LangCodeUtils.getLocalizedResources; import android.content.Intent; import android.net.Uri; import androidx.appcompat.app.AlertDialog; @@ -78,7 +79,7 @@ public class DeleteHelper { String fileDeleteString = "{{delete|reason=" + reason + "|subpage=" + media.getFilename() + "|day=" + calendar.get(Calendar.DAY_OF_MONTH) + - "|month=" + calendar.getDisplayName(Calendar.MONTH, Calendar.LONG, Locale.getDefault()) + + "|month=" + calendar.getDisplayName(Calendar.MONTH, Calendar.LONG, Locale.ENGLISH) + "|year=" + calendar.get(Calendar.YEAR) + "}}"; @@ -156,16 +157,23 @@ public class DeleteHelper { ArrayList mUserReason = new ArrayList<>(); String[] reasonList = {"Reason 1", "Reason 2", "Reason 3"}; - + // Messages posted on-wiki should not be in the app user's locale, but rather in Commons' lingua franca English. + String[] reasonListEnglish = {"Eng1", "Eng2", "Eng3"}; if (problem == ReviewController.DeleteReason.SPAM) { reasonList[0] = context.getString(R.string.delete_helper_ask_spam_selfie); reasonList[1] = context.getString(R.string.delete_helper_ask_spam_blurry); reasonList[2] = context.getString(R.string.delete_helper_ask_spam_nonsense); + reasonListEnglish[0] = getLocalizedResources(context, Locale.ENGLISH).getString(R.string.delete_helper_ask_spam_selfie); + reasonListEnglish[1] = getLocalizedResources(context, Locale.ENGLISH).getString(R.string.delete_helper_ask_spam_blurry); + reasonListEnglish[2] = getLocalizedResources(context, Locale.ENGLISH).getString(R.string.delete_helper_ask_spam_nonsense); } else if (problem == ReviewController.DeleteReason.COPYRIGHT_VIOLATION) { reasonList[0] = context.getString(R.string.delete_helper_ask_reason_copyright_press_photo); reasonList[1] = context.getString(R.string.delete_helper_ask_reason_copyright_internet_photo); reasonList[2] = context.getString(R.string.delete_helper_ask_reason_copyright_logo); + reasonListEnglish[0] = getLocalizedResources(context, Locale.ENGLISH).getString(R.string.delete_helper_ask_reason_copyright_press_photo); + reasonListEnglish[1] = getLocalizedResources(context, Locale.ENGLISH).getString(R.string.delete_helper_ask_reason_copyright_internet_photo); + reasonListEnglish[2] = getLocalizedResources(context, Locale.ENGLISH).getString(R.string.delete_helper_ask_reason_copyright_logo); } alert.setMultiChoiceItems(reasonList, checkedItems, (dialogInterface, position, isChecked) -> { @@ -178,9 +186,10 @@ public class DeleteHelper { alert.setPositiveButton(context.getString(R.string.ok), (dialogInterface, i) -> { - String reason = context.getString(R.string.delete_helper_ask_alert_set_positive_button_reason) + " "; + String reason = getLocalizedResources(context, Locale.ENGLISH).getString(R.string.delete_helper_ask_alert_set_positive_button_reason) + " "; + for (int j = 0; j < mUserReason.size(); j++) { - reason = reason + reasonList[mUserReason.get(j)]; + reason = reason + reasonListEnglish[mUserReason.get(j)]; if (j != mUserReason.size() - 1) { reason = reason + ", "; } diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java index 72b2dd485..fc0b49df3 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java @@ -10,8 +10,10 @@ import static fr.free.nrw.commons.description.EditDescriptionConstants.LIST_OF_D import static fr.free.nrw.commons.description.EditDescriptionConstants.UPDATED_WIKITEXT; import static fr.free.nrw.commons.description.EditDescriptionConstants.WIKITEXT; import static fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.LAST_LOCATION; - +import android.content.res.Resources; +import static fr.free.nrw.commons.utils.LangCodeUtils.getLocalizedResources; import android.annotation.SuppressLint; +import java.lang.reflect.Field; import android.app.AlertDialog; import android.content.Context; import android.content.Intent; @@ -264,6 +266,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements //Had to make this class variable, to implement various onClicks, which access the media, also I fell why make separate variables when one can serve the purpose private Media media; private ArrayList reasonList; + private ArrayList reasonListEnglishMappings; /** * Height stores the height of the frame layout as soon as it is initialised and updates itself on @@ -324,6 +327,14 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements reasonList.add(getString(R.string.deletion_reason_no_longer_want_public)); reasonList.add(getString(R.string.deletion_reason_bad_for_my_privacy)); + // Add corresponding mappings in english locale so that we can upload it in deletion request + reasonListEnglishMappings = new ArrayList<>(); + reasonListEnglishMappings.add(getLocalizedResources(getContext(), Locale.ENGLISH).getString(R.string.deletion_reason_uploaded_by_mistake)); + reasonListEnglishMappings.add(getLocalizedResources(getContext(), Locale.ENGLISH).getString(R.string.deletion_reason_publicly_visible)); + reasonListEnglishMappings.add(getLocalizedResources(getContext(), Locale.ENGLISH).getString(R.string.deletion_reason_not_interesting)); + reasonListEnglishMappings.add(getLocalizedResources(getContext(), Locale.ENGLISH).getString(R.string.deletion_reason_no_longer_want_public)); + reasonListEnglishMappings.add(getLocalizedResources(getContext(), Locale.ENGLISH).getString(R.string.deletion_reason_bad_for_my_privacy)); + final View view = inflater.inflate(R.layout.fragment_media_detail, container, false); ButterKnife.bind(this,view); @@ -1201,9 +1212,10 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements private void onDeleteClicked(Spinner spinner) { applicationKvStore.putBoolean(String.format(NOMINATING_FOR_DELETION_MEDIA, media.getImageUrl()), true); enableProgressBar(); - String reason = spinner.getSelectedItem().toString(); + String reason = reasonListEnglishMappings.get(spinner.getSelectedItemPosition()); + String finalReason = reason; Single resultSingle = reasonBuilder.getReason(media, reason) - .flatMap(reasonString -> deleteHelper.makeDeletion(getContext(), media, reason)); + .flatMap(reasonString -> deleteHelper.makeDeletion(getContext(), media, finalReason)); resultSingle .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) diff --git a/app/src/main/java/fr/free/nrw/commons/utils/LangCodeUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/LangCodeUtils.java index 102a950cb..73bd5c02b 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/LangCodeUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/LangCodeUtils.java @@ -1,4 +1,8 @@ package fr.free.nrw.commons.utils; +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import java.util.Locale; /** * Utilities class for miscellaneous strings @@ -20,4 +24,16 @@ public class LangCodeUtils { return code; } } + + /** + * Returns configuration for locale of + * our choice regardless of user's device settings + */ + public static Resources getLocalizedResources(Context context, Locale desiredLocale) { + Configuration conf = context.getResources().getConfiguration(); + conf = new Configuration(conf); + conf.setLocale(desiredLocale); + Context localizedContext = context.createConfigurationContext(conf); + return localizedContext.getResources(); + } } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/delete/DeleteHelperTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/delete/DeleteHelperTest.kt index 9e7e97473..6bfe6ad47 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/delete/DeleteHelperTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/delete/DeleteHelperTest.kt @@ -5,22 +5,40 @@ import com.nhaarman.mockitokotlin2.eq import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.whenever +import fr.free.nrw.commons.FakeContextWrapper import fr.free.nrw.commons.Media +import fr.free.nrw.commons.TestCommonsApplication import fr.free.nrw.commons.actions.PageEditClient +import fr.free.nrw.commons.contributions.ContributionsListFragment +import fr.free.nrw.commons.review.ReviewController import io.reactivex.Observable +import io.reactivex.Single import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runner.Runner import org.mockito.ArgumentMatchers import org.mockito.Mock +import org.mockito.Mockito import org.mockito.MockitoAnnotations +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode /** * Tests for delete helper */ +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [21], application = TestCommonsApplication::class) +@LooperMode(LooperMode.Mode.PAUSED) class DeleteHelperTest { + @Mock + private lateinit var callback: ReviewController.ReviewCallback + @Mock internal lateinit var pageEditClient: PageEditClient @@ -58,7 +76,6 @@ class DeleteHelperTest { val creatorName = "Creator" whenever(media.author).thenReturn("$creatorName") whenever(media.filename).thenReturn("Test file.jpg") - val makeDeletion = deleteHelper.makeDeletion(context, media, "Test reason")?.blockingGet() assertNotNull(makeDeletion) assertTrue(makeDeletion!!) @@ -113,6 +130,18 @@ class DeleteHelperTest { deleteHelper.makeDeletion(context, media, "Test reason")?.blockingGet() } + @Test + fun askReasonAndExecuteSpamTest() { + val mContext = RuntimeEnvironment.getApplication().applicationContext + deleteHelper.askReasonAndExecute(media, mContext, "My Question", ReviewController.DeleteReason.SPAM, callback) + } + + @Test + fun askReasonAndExecuteCopyrightViolationTest() { + val mContext = RuntimeEnvironment.getApplication().applicationContext + deleteHelper.askReasonAndExecute(media, mContext, "My Question", ReviewController.DeleteReason.COPYRIGHT_VIOLATION, callback); + } + @Test(expected = RuntimeException::class) fun makeDeletionForEmptyCreatorName() { whenever(pageEditClient.prependEdit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString())) diff --git a/app/src/test/kotlin/fr/free/nrw/commons/media/MediaDetailFragmentUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/media/MediaDetailFragmentUnitTests.kt index 87de19ccd..42f0a1835 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/media/MediaDetailFragmentUnitTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/media/MediaDetailFragmentUnitTests.kt @@ -6,6 +6,7 @@ import android.content.Context import android.content.Intent import android.content.res.Configuration import android.os.Bundle +import android.os.Looper import android.view.LayoutInflater import android.view.View import android.view.ViewTreeObserver @@ -18,12 +19,9 @@ import com.facebook.drawee.backends.pipeline.Fresco import com.facebook.drawee.generic.GenericDraweeHierarchy import com.facebook.drawee.view.SimpleDraweeView import com.facebook.soloader.SoLoader +import fr.free.nrw.commons.* import fr.free.nrw.commons.LocationPicker.LocationPickerActivity -import fr.free.nrw.commons.Media import org.robolectric.Shadows.shadowOf -import fr.free.nrw.commons.R -import fr.free.nrw.commons.TestAppAdapter -import fr.free.nrw.commons.TestCommonsApplication import fr.free.nrw.commons.category.CategoryEditSearchRecyclerViewAdapter import fr.free.nrw.commons.explore.SearchActivity import fr.free.nrw.commons.kvstore.JsonKvStore @@ -32,17 +30,24 @@ import fr.free.nrw.commons.location.LocationServiceManager import fr.free.nrw.commons.ui.widget.HtmlTextView import org.junit.Assert import org.junit.Before +import fr.free.nrw.commons.TestCommonsApplication +import fr.free.nrw.commons.R +import fr.free.nrw.commons.TestAppAdapter +import fr.free.nrw.commons.Media +import fr.free.nrw.commons.contributions.ContributionViewHolder +import fr.free.nrw.commons.delete.DeleteHelper +import fr.free.nrw.commons.delete.ReasonBuilder +import fr.free.nrw.commons.utils.ImageUtils +import io.reactivex.Single import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.Mockito -import org.mockito.Mockito.`when` -import org.mockito.Mockito.mock -import org.mockito.MockitoAnnotations +import org.mockito.* +import org.mockito.Mockito.* import org.powermock.reflect.Whitebox import org.robolectric.Robolectric import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment +import org.robolectric.Shadows import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import org.robolectric.shadows.ShadowIntent @@ -50,6 +55,7 @@ import org.wikipedia.AppAdapter import java.lang.reflect.Field import java.lang.reflect.Method import java.util.* +import kotlin.collections.ArrayList import kotlin.collections.HashMap @RunWith(RobolectricTestRunner::class) @@ -57,7 +63,6 @@ import kotlin.collections.HashMap @LooperMode(LooperMode.Mode.PAUSED) class MediaDetailFragmentUnitTests { - private val REQUEST_CODE = 1001 private val LAST_LOCATION = "last_location_while_uploading" private val REQUEST_CODE_EDIT_DESCRIPTION = 1002 @@ -67,6 +72,30 @@ class MediaDetailFragmentUnitTests { private lateinit var view: View private lateinit var context: Context + private val NOMINATING_FOR_DELETION_MEDIA = "Nominating for deletion %s" + + + @Mock + private lateinit var deleteHelper: DeleteHelper + + @Mock + private lateinit var reasonBuilder: ReasonBuilder + + @Mock + private lateinit var progressBarDeletion: ProgressBar + + @Mock + private lateinit var delete: Button + + + private var isDeleted = true + + @Mock + private var reasonList: ArrayList? = null + + @Mock + private var reasonListEnglishMappings: ArrayList? = null + @Mock private lateinit var locationManager: LocationServiceManager @@ -156,9 +185,16 @@ class MediaDetailFragmentUnitTests { Whitebox.setInternalState(fragment, "scrollView", scrollView) categoryRecyclerView = view.findViewById(R.id.rv_categories) + progressBarDeletion = view.findViewById(R.id.progressBarDeletion) + delete = view.findViewById(R.id.nominateDeletion) Whitebox.setInternalState(fragment, "categoryRecyclerView", categoryRecyclerView) Whitebox.setInternalState(fragment, "media", media) + Whitebox.setInternalState(fragment, "isDeleted", isDeleted) + Whitebox.setInternalState(fragment, "reasonList", reasonList) + Whitebox.setInternalState(fragment, "reasonListEnglishMappings", reasonListEnglishMappings) + Whitebox.setInternalState(fragment, "reasonBuilder", reasonBuilder) + Whitebox.setInternalState(fragment, "deleteHelper", deleteHelper) Whitebox.setInternalState(fragment, "progressBar", progressBar) Whitebox.setInternalState(fragment, "progressBarEditDescription", progressBar) Whitebox.setInternalState(fragment, "captionsListView", listView) @@ -175,6 +211,7 @@ class MediaDetailFragmentUnitTests { Whitebox.setInternalState(fragment, "mediaCaption", textView) Whitebox.setInternalState(fragment, "captionLayout", linearLayout) Whitebox.setInternalState(fragment, "depictsLayout", linearLayout) + Whitebox.setInternalState(fragment, "delete", delete) Whitebox.setInternalState(fragment, "depictionContainer", linearLayout) Whitebox.setInternalState(fragment, "toDoLayout", linearLayout) Whitebox.setInternalState(fragment, "dummyCategoryEditContainer", linearLayout) @@ -183,6 +220,7 @@ class MediaDetailFragmentUnitTests { Whitebox.setInternalState(fragment, "editDescription", button) Whitebox.setInternalState(fragment, "categoryContainer", linearLayout) Whitebox.setInternalState(fragment, "categorySearchView", searchView) + Whitebox.setInternalState(fragment, "progressBarDeletion", progressBarDeletion) Whitebox.setInternalState(fragment, "mediaDiscussion", textView) Whitebox.setInternalState(fragment, "locationManager", locationManager) Whitebox.setInternalState( @@ -590,6 +628,28 @@ class MediaDetailFragmentUnitTests { method.invoke(fragment, "") } + @Test + @Throws(Exception::class) + fun testOnDeleteClickedNull() { + Shadows.shadowOf(Looper.getMainLooper()).idle() + val spinner = mock(Spinner::class.java) + `when`(media.imageUrl).thenReturn("test@example.com") + `when`(spinner.selectedItemPosition).thenReturn(0) + `when`(reasonListEnglishMappings?.get(spinner.selectedItemPosition)).thenReturn("TESTING") + `when`(applicationKvStore.getBoolean(String.format(MediaDetailFragment.NOMINATING_FOR_DELETION_MEDIA,media.imageUrl + ))).thenReturn(true) + doReturn(Single.just(true)).`when`(deleteHelper).makeDeletion(ArgumentMatchers.any(),ArgumentMatchers.any(), ArgumentMatchers.any()) + + doReturn(Single.just("")).`when`(reasonBuilder).getReason(ArgumentMatchers.any(), ArgumentMatchers.any()) + + val method: Method = MediaDetailFragment::class.java.getDeclaredMethod( + "onDeleteClicked", + Spinner::class.java + ) + method.isAccessible = true + method.invoke(fragment, spinner) + } + @Test @Throws(Exception::class) fun testForMedia() {