Merge branch 'main' into fix/upload-limit-#3101

This commit is contained in:
Nicolas Raoul 2025-10-19 22:59:05 +09:00 committed by GitHub
commit e17a668acc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 208 additions and 53 deletions

View file

@ -173,19 +173,13 @@ class BookmarkItemsDao @Inject constructor(
categoryNameList: List<String>, categoryNameList: List<String>,
categoryDescriptionList: List<String>, categoryDescriptionList: List<String>,
categoryThumbnailList: List<String> categoryThumbnailList: List<String>
): List<CategoryItem> { ): List<CategoryItem> = categoryNameList.mapIndexed { index, name ->
return buildList { CategoryItem(
for (i in categoryNameList.indices) { name = name,
add( description = categoryDescriptionList.getOrNull(index),
CategoryItem( thumbnail = categoryThumbnailList.getOrNull(index),
categoryNameList[i], isSelected = false
categoryDescriptionList[i], )
categoryThumbnailList[i],
false
)
)
}
}
} }
/** /**

View file

@ -7,8 +7,8 @@ import com.google.gson.annotations.SerializedName
*/ */
class CampaignConfig { class CampaignConfig {
@SerializedName("showOnlyLiveCampaigns") @SerializedName("showOnlyLiveCampaigns")
private val showOnlyLiveCampaigns = false var showOnlyLiveCampaigns = false
@SerializedName("sortBy") @SerializedName("sortBy")
private val sortBy: String? = null var sortBy: String? = null
} }

View file

@ -8,8 +8,8 @@ import fr.free.nrw.commons.campaigns.models.Campaign
*/ */
class CampaignResponseDTO { class CampaignResponseDTO {
@SerializedName("config") @SerializedName("config")
val campaignConfig: CampaignConfig? = null var campaignConfig: CampaignConfig? = null
@SerializedName("campaigns") @SerializedName("campaigns")
val campaigns: List<Campaign>? = null var campaigns: List<Campaign>? = null
} }

View file

@ -324,7 +324,7 @@ after opening the app.
) )
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.blockingGet() .blockingGet()
Timber.d("Resuming " + stuckUploads.size + " uploads...") Timber.d("Resuming %d uploads...", stuckUploads.size)
if (!stuckUploads.isEmpty()) { if (!stuckUploads.isEmpty()) {
for (contribution in stuckUploads) { for (contribution in stuckUploads) {
contribution.state = Contribution.STATE_QUEUED contribution.state = Contribution.STATE_QUEUED

View file

@ -9,7 +9,6 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ProgressBar import android.widget.ProgressBar
import android.widget.Switch
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible import androidx.core.view.isVisible
@ -20,6 +19,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.switchmaterial.SwitchMaterial
import fr.free.nrw.commons.contributions.Contribution import fr.free.nrw.commons.contributions.Contribution
import fr.free.nrw.commons.contributions.ContributionDao import fr.free.nrw.commons.contributions.ContributionDao
import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao
@ -82,7 +82,7 @@ class ImageFragment :
*/ */
private var selectorRV: RecyclerView? = null private var selectorRV: RecyclerView? = null
private var loader: ProgressBar? = null private var loader: ProgressBar? = null
private var switch: Switch? = null private var switch: SwitchMaterial? = null
lateinit var filteredImages: ArrayList<Image> lateinit var filteredImages: ArrayList<Image>
/** /**

View file

@ -112,8 +112,8 @@ class WikidataItemDetailsActivity : BaseActivity(), MediaDetailProvider, Categor
viewPagerAdapter!!.setTabs( viewPagerAdapter!!.setTabs(
R.string.title_for_media to depictionImagesListFragment!!, R.string.title_for_media to depictionImagesListFragment!!,
R.string.title_for_subcategories to childDepictionsFragment, R.string.title_for_child_classes to childDepictionsFragment,
R.string.title_for_parent_categories to parentDepictionsFragment R.string.title_for_parent_classes to parentDepictionsFragment
) )
binding!!.viewPager.offscreenPageLimit = 2 binding!!.viewPager.offscreenPageLimit = 2
viewPagerAdapter!!.notifyDataSetChanged() viewPagerAdapter!!.notifyDataSetChanged()

View file

@ -27,6 +27,7 @@ import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.util.Locale import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
import timber.log.Timber
/** /**
* This activity will set two tabs, achievements and * This activity will set two tabs, achievements and
@ -122,7 +123,7 @@ class ProfileActivity : BaseActivity() {
val rootView = window.decorView.findViewById<View>(android.R.id.content) val rootView = window.decorView.findViewById<View>(android.R.id.content)
val screenShot = getScreenShot(rootView) val screenShot = getScreenShot(rootView)
if (screenShot == null) { if (screenShot == null) {
Log.e("ERROR", "ScreenShot is null") Timber.e("ScreenShot is null")
return false return false
} }
showAlert(screenShot) showAlert(screenShot)

View file

@ -1,6 +1,7 @@
package fr.free.nrw.commons.settings package fr.free.nrw.commons.settings
import android.Manifest.permission import android.Manifest.permission
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.app.Dialog import android.app.Dialog
import android.content.Context.MODE_PRIVATE import android.content.Context.MODE_PRIVATE
@ -303,6 +304,11 @@ class SettingsFragment : PreferenceFragmentCompat() {
) )
} }
// Remove the space for icons in the settings menu.
// This uses an internal API that shouldn't be used in app code,
// but it appears to be the most robust way to do this at the moment,
// disable the warning.
@SuppressLint("RestrictedApi")
override fun onCreateAdapter(preferenceScreen: PreferenceScreen): Adapter<PreferenceViewHolder> override fun onCreateAdapter(preferenceScreen: PreferenceScreen): Adapter<PreferenceViewHolder>
{ {
return object : PreferenceGroupAdapter(preferenceScreen) { return object : PreferenceGroupAdapter(preferenceScreen) {

View file

@ -37,7 +37,7 @@ object LocationUtils {
latLng = LatLng(latLngArray[1].trim().toDouble(), latLng = LatLng(latLngArray[1].trim().toDouble(),
latLngArray[0].trim().toDouble(), 1f) latLngArray[0].trim().toDouble(), 1f)
} catch (e: Exception) { } catch (e: Exception) {
Timber.e("Error while parsing user entered lat long: %s", e) Timber.e(e, "Error while parsing user entered lat long")
} }
return latLng return latLng

View file

@ -6,7 +6,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="?attr/mainBackground"> android:background="?attr/mainBackground">
<Switch <com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switchWidget" android:id="@+id/switchWidget"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View file

@ -2,7 +2,7 @@
<resources> <resources>
<!--accessibility UI description strings--> <!--accessibility UI description strings-->
<string name="commons_facebook">Commons Facebook Page</string> <string name="commons_facebook">Commons Facebook Page</string>
<string name="commons_github">Commons Github Source Code</string> <string name="commons_github">Commons GitHub Source Code</string>
<string name="commons_logo">Commons Logo</string> <string name="commons_logo">Commons Logo</string>
<string name="commons_website">Commons Website</string> <string name="commons_website">Commons Website</string>
<string name="exit_location_picker">Exit location picker</string> <string name="exit_location_picker">Exit location picker</string>
@ -493,12 +493,12 @@ Upload your first media by tapping on the add button.</string>
<string name="check_category_failure_message">Could not request category check for %1$s</string> <string name="check_category_failure_message">Could not request category check for %1$s</string>
<string name="check_category_toast">Requesting category check for %1$s</string> <string name="check_category_toast">Requesting category check for %1$s</string>
<string name="nominate_for_deletion_done">Done</string> <string name="nominate_for_deletion_done">Done</string>
<string name="send_thank_success_title">Sending Thanks: Success</string> <string name="send_thank_success_title">Sending thanks: Success</string>
<string name="send_thank_success_message">Sent thanks to %1$s</string> <string name="send_thank_success_message">Sent thanks to %1$s</string>
<string name="send_thank_failure_message">Failed to send thanks to %1$s</string> <string name="send_thank_failure_message">Failed to send thanks to %1$s</string>
<string name="send_thank_failure_title">Sending Thanks: Failure</string> <string name="send_thank_failure_title">Sending thanks: Failure</string>
<string name="send_thank_toast">Sending Thanks for %1$s</string> <string name="send_thank_toast">Sending thanks for %1$s</string>
<string name="review_copyright">Does this follow the rules of copyright?</string> <string name="review_copyright">Does this follow the rules of copyright?</string>
<string name="review_category">Is this correctly categorized?</string> <string name="review_category">Is this correctly categorized?</string>
<string name="review_spam">Is this in-scope?</string> <string name="review_spam">Is this in-scope?</string>

View file

@ -3,6 +3,10 @@ package fr.free.nrw.commons
import com.google.gson.Gson import com.google.gson.Gson
import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.times
import fr.free.nrw.commons.campaigns.CampaignResponseDTO
import fr.free.nrw.commons.campaigns.CampaignConfig
import fr.free.nrw.commons.campaigns.models.Campaign
import fr.free.nrw.commons.explore.depictions.DepictsClient import fr.free.nrw.commons.explore.depictions.DepictsClient
import fr.free.nrw.commons.location.LatLng import fr.free.nrw.commons.location.LatLng
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
@ -10,13 +14,22 @@ import fr.free.nrw.commons.nearby.model.NearbyQueryParams
import okhttp3.Call import okhttp3.Call
import okhttp3.HttpUrl import okhttp3.HttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import okhttp3.ResponseBody
import okhttp3.mockwebserver.MockResponse
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito import org.mockito.Mockito
import org.mockito.Mockito.times import org.mockito.Mockito.eq
import org.mockito.MockitoAnnotations import org.mockito.MockitoAnnotations
import java.io.BufferedReader
import java.io.InputStreamReader
import java.lang.Exception import java.lang.Exception
class OkHttpJsonApiClientTests { class OkHttpJsonApiClientTests {
@ -45,34 +58,43 @@ class OkHttpJsonApiClientTests {
@Mock @Mock
lateinit var response: Response lateinit var response: Response
@Mock
lateinit var responseBody: ResponseBody
private lateinit var mockWebServer: TestWebServer
@Before @Before
fun setUp() { fun setUp() {
MockitoAnnotations.openMocks(this) MockitoAnnotations.openMocks(this)
okHttpJsonApiClient = mockWebServer = TestWebServer()
OkHttpJsonApiClient( mockWebServer.setUp()
okhttpClient, okHttpJsonApiClient = OkHttpJsonApiClient(
depictsClient, okhttpClient,
wikiMediaToolforgeUrl, depictsClient,
sparqlQueryUrl, wikiMediaToolforgeUrl,
campaignsUrl, sparqlQueryUrl,
gson, mockWebServer.getUrl(), //use the mock server for the campaignsUrl
) gson
)
Mockito.`when`(okhttpClient.newCall(any())).thenReturn(call) Mockito.`when`(okhttpClient.newCall(any())).thenReturn(call)
Mockito.`when`(call.execute()).thenReturn(response) Mockito.`when`(call.execute()).thenReturn(response)
Mockito.`when`(response.isSuccessful).thenReturn(false)
Mockito.`when`(response.message).thenReturn("test")
Mockito.`when`(response.body).thenReturn(responseBody)
Mockito.`when`(responseBody.string()).thenReturn("{\"error\": \"test\"}")
} }
@Test @Test
fun testGetNearbyPlacesCustomQuery() { fun testGetNearbyPlacesCustomQuery() {
Mockito.`when`(response.message).thenReturn("test")
try { try {
okHttpJsonApiClient.getNearbyPlaces(latLng, "test", 10.0, "test") okHttpJsonApiClient.getNearbyPlaces(latLng, "test", 10.0, "test")
} catch (e: Exception) { } catch (e: Exception) {
assert(e.message.equals("test")) assertEquals("test", e.message)
} }
try { try {
okHttpJsonApiClient.getNearbyPlaces(NearbyQueryParams.Rectangular(latLng, latLng), "test", true, "test") okHttpJsonApiClient.getNearbyPlaces(NearbyQueryParams.Rectangular(latLng, latLng), "test", true, "test")
} catch (e: Exception) { } catch (e: Exception) {
assert(e.message.equals("test")) assertEquals("test", e.message)
} }
verify(okhttpClient, times(2)).newCall(any()) verify(okhttpClient, times(2)).newCall(any())
verify(call, times(2)).execute() verify(call, times(2)).execute()
@ -80,11 +102,10 @@ class OkHttpJsonApiClientTests {
@Test @Test
fun testGetNearbyPlaces() { fun testGetNearbyPlaces() {
Mockito.`when`(response.message).thenReturn("test")
try { try {
okHttpJsonApiClient.getNearbyPlaces(latLng, "test", 10.0, null) okHttpJsonApiClient.getNearbyPlaces(latLng, "test", 10.0, null)
} catch (e: Exception) { } catch (e: Exception) {
assert(e.message.equals("test")) assertEquals("test", e.message)
} }
try { try {
okHttpJsonApiClient.getNearbyPlaces( okHttpJsonApiClient.getNearbyPlaces(
@ -93,9 +114,8 @@ class OkHttpJsonApiClientTests {
true, true,
null null
) )
} catch (e: Exception) { } catch (e: Exception) {
assert(e.message.equals("test")) assertEquals("test", e.message)
} }
try { try {
okHttpJsonApiClient.getNearbyPlaces( okHttpJsonApiClient.getNearbyPlaces(
@ -105,7 +125,7 @@ class OkHttpJsonApiClientTests {
null null
) )
} catch (e: Exception) { } catch (e: Exception) {
assert(e.message.equals("test")) assertEquals("test", e.message)
} }
verify(okhttpClient, times(3)).newCall(any()) verify(okhttpClient, times(3)).newCall(any())
verify(call, times(3)).execute() verify(call, times(3)).execute()
@ -113,18 +133,121 @@ class OkHttpJsonApiClientTests {
@Test @Test
fun testGetNearbyItemCount() { fun testGetNearbyItemCount() {
Mockito.`when`(response.message).thenReturn("test")
try { try {
okHttpJsonApiClient.getNearbyItemCount(NearbyQueryParams.Radial(latLng, 10f)) okHttpJsonApiClient.getNearbyItemCount(NearbyQueryParams.Radial(latLng, 10f))
} catch (e: Exception) { } catch (e: Exception) {
assert(e.message.equals("test")) assertEquals("test", e.message)
} }
try { try {
okHttpJsonApiClient.getNearbyItemCount(NearbyQueryParams.Rectangular(latLng, latLng)) okHttpJsonApiClient.getNearbyItemCount(NearbyQueryParams.Rectangular(latLng, latLng))
} catch (e: Exception) { } catch (e: Exception) {
assert(e.message.equals("test")) assertEquals("test", e.message)
} }
verify(okhttpClient, times(2)).newCall(any()) verify(okhttpClient, times(2)).newCall(any())
verify(call, times(2)).execute() verify(call, times(2)).execute()
} }
}
@Test
fun testGetCampaignsWithData() {
//loads the json response from resources
val jsonResponse = loadJsonFromResource("campaigns_response_with_data.json")
//mocks the succesfull response chain
Mockito.`when`(response.isSuccessful).thenReturn(true)
Mockito.`when`(response.message).thenReturn("OK")
Mockito.`when`(response.body).thenReturn(responseBody)
Mockito.`when`(responseBody.string()).thenReturn(jsonResponse)
val campaignResponse = CampaignResponseDTO().apply {
campaignConfig = CampaignConfig().apply {
showOnlyLiveCampaigns = false
sortBy = "startDate"
}
campaigns = listOf(
Campaign().apply {
title = "Wiki Loves Monuments"
isWLMCampaign = true
},
Campaign().apply {
title = "Wiki Loves Nature"
isWLMCampaign = false
}
)
}
//any() for the string argument and eq() for the class argument.
Mockito.`when`(
gson.fromJson(
any<String>(),
eq(CampaignResponseDTO::class.java)
)
).thenReturn(campaignResponse)
//call the getCampaigns
val result: CampaignResponseDTO? = okHttpJsonApiClient.getCampaigns().blockingGet()
//verify the results
assertNotNull(result)
assertNotNull(result?.campaigns)
assertEquals(2, result?.campaigns!!.size)
assertEquals("Wiki Loves Monuments", result.campaigns!![0].title)
assertTrue(result.campaigns!![0].isWLMCampaign)
assertEquals("Wiki Loves Nature", result.campaigns!![1].title)
assertEquals(false, result.campaigns!![1].isWLMCampaign)
assertNotNull(result.campaignConfig)
assertFalse(result.campaignConfig!!.showOnlyLiveCampaigns)
assertEquals("startDate", result.campaignConfig!!.sortBy)
}
@Test
fun testGetCampaignsEmpty() {
//loads the empty json response
val jsonResponse = loadJsonFromResource("campaigns_response_empty.json")
//mocks the successful response chain
Mockito.`when`(response.isSuccessful).thenReturn(true)
Mockito.`when`(response.message).thenReturn("OK")
Mockito.`when`(response.body).thenReturn(responseBody)
Mockito.`when`(responseBody.string()).thenReturn(jsonResponse)
val campaignResponse = CampaignResponseDTO().apply {
campaignConfig = CampaignConfig().apply {
showOnlyLiveCampaigns = false
sortBy = "startDate"
}
campaigns = emptyList()
}
//use any() for the string argument and eq() for the class argument.
Mockito.`when`(
gson.fromJson(
any<String>(),
eq(CampaignResponseDTO::class.java)
)
).thenReturn(campaignResponse)
//calls getCampaigns
val result: CampaignResponseDTO? = okHttpJsonApiClient.getCampaigns().blockingGet()
//verify the results
assertNotNull(result)
assertNotNull(result?.campaigns)
assertTrue(result?.campaigns!!.isEmpty())
assertNotNull(result.campaignConfig)
assertFalse(result.campaignConfig!!.showOnlyLiveCampaigns)
assertEquals("startDate", result.campaignConfig!!.sortBy)
}
fun loadJsonFromResource(fileName: String): String {
val resourcePath = "raw/$fileName"
//uses the classloader to find the resource in the test environment
val inputStream = javaClass.classLoader?.getResourceAsStream(resourcePath)
if (inputStream != null) {
//reads the entire stream content
return BufferedReader(InputStreamReader(inputStream)).use { it.readText() }
}
//throws an exception with the correct expected path
throw IllegalArgumentException("Resource $fileName not found. Please ensure the file is located in app/src/test/resources/raw/")
}
}

View file

@ -0,0 +1,7 @@
{
"config": {
"showOnlyLiveCampaigns": false,
"sortBy": "startDate"
},
"campaigns": []
}

View file

@ -0,0 +1,24 @@
{
"config": {
"showOnlyLiveCampaigns": false,
"sortBy": "startDate"
},
"campaigns": [
{
"title": "Wiki Loves Monuments",
"description": "A campaign to photograph monuments",
"startDate": "2025-09-01",
"endDate": "2025-09-30",
"link": "https://commons.wikimedia.org/wiki/Campaign:Wiki_Loves_Monuments",
"isWLMCampaign": true
},
{
"title": "Wiki Loves Nature",
"description": "A campaign to photograph nature",
"startDate": "2025-06-01",
"endDate": "2025-06-30",
"link": "https://commons.wikimedia.org/wiki/Campaign:Wiki_Loves_Nature",
"isWLMCampaign": false
}
]
}