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>,
categoryDescriptionList: List<String>,
categoryThumbnailList: List<String>
): List<CategoryItem> {
return buildList {
for (i in categoryNameList.indices) {
add(
CategoryItem(
categoryNameList[i],
categoryDescriptionList[i],
categoryThumbnailList[i],
false
)
)
}
}
): List<CategoryItem> = categoryNameList.mapIndexed { index, name ->
CategoryItem(
name = name,
description = categoryDescriptionList.getOrNull(index),
thumbnail = categoryThumbnailList.getOrNull(index),
isSelected = false
)
}
/**

View file

@ -7,8 +7,8 @@ import com.google.gson.annotations.SerializedName
*/
class CampaignConfig {
@SerializedName("showOnlyLiveCampaigns")
private val showOnlyLiveCampaigns = false
var showOnlyLiveCampaigns = false
@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 {
@SerializedName("config")
val campaignConfig: CampaignConfig? = null
var campaignConfig: CampaignConfig? = null
@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())
.blockingGet()
Timber.d("Resuming " + stuckUploads.size + " uploads...")
Timber.d("Resuming %d uploads...", stuckUploads.size)
if (!stuckUploads.isEmpty()) {
for (contribution in stuckUploads) {
contribution.state = Contribution.STATE_QUEUED

View file

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

View file

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

View file

@ -27,6 +27,7 @@ import java.io.File
import java.io.FileOutputStream
import java.util.Locale
import javax.inject.Inject
import timber.log.Timber
/**
* 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 screenShot = getScreenShot(rootView)
if (screenShot == null) {
Log.e("ERROR", "ScreenShot is null")
Timber.e("ScreenShot is null")
return false
}
showAlert(screenShot)

View file

@ -1,6 +1,7 @@
package fr.free.nrw.commons.settings
import android.Manifest.permission
import android.annotation.SuppressLint
import android.app.Activity
import android.app.Dialog
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>
{
return object : PreferenceGroupAdapter(preferenceScreen) {

View file

@ -37,7 +37,7 @@ object LocationUtils {
latLng = LatLng(latLngArray[1].trim().toDouble(),
latLngArray[0].trim().toDouble(), 1f)
} 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

View file

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

View file

@ -2,7 +2,7 @@
<resources>
<!--accessibility UI description strings-->
<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_website">Commons Website</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_toast">Requesting category check for %1$s</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_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_category">Is this correctly categorized?</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.nhaarman.mockitokotlin2.any
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.location.LatLng
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
@ -10,13 +14,22 @@ import fr.free.nrw.commons.nearby.model.NearbyQueryParams
import okhttp3.Call
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
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.Test
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.Mockito.times
import org.mockito.Mockito.eq
import org.mockito.MockitoAnnotations
import java.io.BufferedReader
import java.io.InputStreamReader
import java.lang.Exception
class OkHttpJsonApiClientTests {
@ -45,34 +58,43 @@ class OkHttpJsonApiClientTests {
@Mock
lateinit var response: Response
@Mock
lateinit var responseBody: ResponseBody
private lateinit var mockWebServer: TestWebServer
@Before
fun setUp() {
MockitoAnnotations.openMocks(this)
okHttpJsonApiClient =
OkHttpJsonApiClient(
okhttpClient,
depictsClient,
wikiMediaToolforgeUrl,
sparqlQueryUrl,
campaignsUrl,
gson,
)
mockWebServer = TestWebServer()
mockWebServer.setUp()
okHttpJsonApiClient = OkHttpJsonApiClient(
okhttpClient,
depictsClient,
wikiMediaToolforgeUrl,
sparqlQueryUrl,
mockWebServer.getUrl(), //use the mock server for the campaignsUrl
gson
)
Mockito.`when`(okhttpClient.newCall(any())).thenReturn(call)
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
fun testGetNearbyPlacesCustomQuery() {
Mockito.`when`(response.message).thenReturn("test")
try {
okHttpJsonApiClient.getNearbyPlaces(latLng, "test", 10.0, "test")
} catch (e: Exception) {
assert(e.message.equals("test"))
assertEquals("test", e.message)
}
try {
okHttpJsonApiClient.getNearbyPlaces(NearbyQueryParams.Rectangular(latLng, latLng), "test", true, "test")
} catch (e: Exception) {
assert(e.message.equals("test"))
assertEquals("test", e.message)
}
verify(okhttpClient, times(2)).newCall(any())
verify(call, times(2)).execute()
@ -80,11 +102,10 @@ class OkHttpJsonApiClientTests {
@Test
fun testGetNearbyPlaces() {
Mockito.`when`(response.message).thenReturn("test")
try {
okHttpJsonApiClient.getNearbyPlaces(latLng, "test", 10.0, null)
} catch (e: Exception) {
assert(e.message.equals("test"))
assertEquals("test", e.message)
}
try {
okHttpJsonApiClient.getNearbyPlaces(
@ -93,9 +114,8 @@ class OkHttpJsonApiClientTests {
true,
null
)
} catch (e: Exception) {
assert(e.message.equals("test"))
assertEquals("test", e.message)
}
try {
okHttpJsonApiClient.getNearbyPlaces(
@ -105,7 +125,7 @@ class OkHttpJsonApiClientTests {
null
)
} catch (e: Exception) {
assert(e.message.equals("test"))
assertEquals("test", e.message)
}
verify(okhttpClient, times(3)).newCall(any())
verify(call, times(3)).execute()
@ -113,18 +133,121 @@ class OkHttpJsonApiClientTests {
@Test
fun testGetNearbyItemCount() {
Mockito.`when`(response.message).thenReturn("test")
try {
okHttpJsonApiClient.getNearbyItemCount(NearbyQueryParams.Radial(latLng, 10f))
} catch (e: Exception) {
assert(e.message.equals("test"))
assertEquals("test", e.message)
}
try {
okHttpJsonApiClient.getNearbyItemCount(NearbyQueryParams.Rectangular(latLng, latLng))
} catch (e: Exception) {
assert(e.message.equals("test"))
assertEquals("test", e.message)
}
verify(okhttpClient, times(2)).newCall(any())
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
}
]
}