Feature: Show where file is being used on Commons & Other wikis (#6006)

* add url to build config

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* add network call functions

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* return response asynchronously

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* inject page size in the request

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* rename from Commons..Response.kt to ..Response.kt

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* convert to .kt

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* ui setup working

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* fix merge conflict

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* cleanup

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* fix CI

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* use suspend function for network calls

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>

* doc

* doc

* doc

* doc

* doc

---------

Signed-off-by: parneet-guraya <gurayaparneet@gmail.com>
Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
This commit is contained in:
Parneet Singh 2024-12-09 19:37:44 +05:30 committed by GitHub
parent 04a07ed655
commit 85d9aef2f3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 2587 additions and 1499 deletions

View file

@ -0,0 +1,35 @@
package fr.free.nrw.commons.fileusages
import com.google.gson.annotations.SerializedName
/**
* Show where file is being used on Commons and oher wikis.
*/
data class FileUsagesResponse(
@SerializedName("continue") val continueResponse: CommonsContinue?,
@SerializedName("batchcomplete") val batchComplete: Boolean,
@SerializedName("query") val query: Query,
)
data class CommonsContinue(
@SerializedName("fucontinue") val fuContinue: String,
@SerializedName("continue") val continueKey: String
)
data class Query(
@SerializedName("pages") val pages: List<Page>
)
data class Page(
@SerializedName("pageid") val pageId: Int,
@SerializedName("ns") val nameSpace: Int,
@SerializedName("title") val title: String,
@SerializedName("fileusage") val fileUsage: List<FileUsage>
)
data class FileUsage(
@SerializedName("pageid") val pageId: Int,
@SerializedName("ns") val nameSpace: Int,
@SerializedName("title") val title: String,
@SerializedName("redirect") val redirect: Boolean
)

View file

@ -0,0 +1,18 @@
package fr.free.nrw.commons.fileusages
/**
* Show where file is being used on Commons and oher wikis.
*/
data class FileUsagesUiModel(
val title: String,
val link: String?
)
fun FileUsage.toUiModel(): FileUsagesUiModel {
return FileUsagesUiModel(title = title, link = "https://commons.wikimedia.org/wiki/$title")
}
fun GlobalFileUsage.toUiModel(): FileUsagesUiModel {
// link is associated with sub items under wiki group (which is not used ATM)
return FileUsagesUiModel(title = wiki, link = null)
}

View file

@ -0,0 +1,34 @@
package fr.free.nrw.commons.fileusages
import com.google.gson.annotations.SerializedName
/**
* Show where file is being used on Commons and oher wikis.
*/
data class GlobalFileUsagesResponse(
@SerializedName("continue") val continueResponse: GlobalContinue?,
@SerializedName("batchcomplete") val batchComplete: Boolean,
@SerializedName("query") val query: GlobalQuery,
)
data class GlobalContinue(
@SerializedName("gucontinue") val guContinue: String,
@SerializedName("continue") val continueKey: String
)
data class GlobalQuery(
@SerializedName("pages") val pages: List<GlobalPage>
)
data class GlobalPage(
@SerializedName("pageid") val pageId: Int,
@SerializedName("ns") val nameSpace: Int,
@SerializedName("title") val title: String,
@SerializedName("globalusage") val fileUsage: List<GlobalFileUsage>
)
data class GlobalFileUsage(
@SerializedName("title") val title: String,
@SerializedName("wiki") val wiki: String,
@SerializedName("url") val url: String
)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,116 @@
package fr.free.nrw.commons.media
import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import fr.free.nrw.commons.R
import fr.free.nrw.commons.fileusages.FileUsagesUiModel
import fr.free.nrw.commons.fileusages.toUiModel
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
/**
* Show where file is being used on Commons and oher wikis.
*/
class MediaDetailViewModel(
private val applicationContext: Context,
private val okHttpJsonApiClient: OkHttpJsonApiClient
) :
ViewModel() {
private val _commonsContainerState =
MutableStateFlow<FileUsagesContainerState>(FileUsagesContainerState.Initial)
val commonsContainerState = _commonsContainerState.asStateFlow()
private val _globalContainerState =
MutableStateFlow<FileUsagesContainerState>(FileUsagesContainerState.Initial)
val globalContainerState = _globalContainerState.asStateFlow()
fun loadFileUsagesCommons(fileName: String) {
viewModelScope.launch {
_commonsContainerState.update { FileUsagesContainerState.Loading }
try {
val result =
okHttpJsonApiClient.getFileUsagesOnCommons(fileName, 10)
val data = result?.query?.pages?.first()?.fileUsage?.map { it.toUiModel() }
_commonsContainerState.update { FileUsagesContainerState.Success(data = data) }
} catch (e: Exception) {
_commonsContainerState.update {
FileUsagesContainerState.Error(
errorMessage = applicationContext.getString(
R.string.error_while_loading
)
)
}
Timber.e(e, javaClass.simpleName)
}
}
}
fun loadGlobalFileUsages(fileName: String) {
viewModelScope.launch {
_globalContainerState.update { FileUsagesContainerState.Loading }
try {
val result = okHttpJsonApiClient.getGlobalFileUsages(fileName, 10)
val data = result?.query?.pages?.first()?.fileUsage?.map { it.toUiModel() }
_globalContainerState.update { FileUsagesContainerState.Success(data = data) }
} catch (e: Exception) {
_globalContainerState.update {
FileUsagesContainerState.Error(
errorMessage = applicationContext.getString(
R.string.error_while_loading
)
)
}
Timber.e(e, javaClass.simpleName)
}
}
}
sealed class FileUsagesContainerState {
object Initial : FileUsagesContainerState()
object Loading : FileUsagesContainerState()
data class Success(val data: List<FileUsagesUiModel>?) : FileUsagesContainerState()
data class Error(val errorMessage: String) : FileUsagesContainerState()
}
class MediaDetailViewModelProviderFactory
@Inject constructor(
private val okHttpJsonApiClient: OkHttpJsonApiClient,
private val applicationContext: Context
) :
ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MediaDetailViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return MediaDetailViewModel(applicationContext, okHttpJsonApiClient) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
}

View file

@ -2,8 +2,11 @@ package fr.free.nrw.commons.mwapi
import android.text.TextUtils
import com.google.gson.Gson
import fr.free.nrw.commons.BuildConfig
import fr.free.nrw.commons.campaigns.CampaignResponseDTO
import fr.free.nrw.commons.explore.depictions.DepictsClient
import fr.free.nrw.commons.fileusages.FileUsagesResponse
import fr.free.nrw.commons.fileusages.GlobalFileUsagesResponse
import fr.free.nrw.commons.location.LatLng
import fr.free.nrw.commons.nearby.Place
import fr.free.nrw.commons.nearby.model.ItemsClass
@ -20,6 +23,8 @@ import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour
import fr.free.nrw.commons.wikidata.model.GetWikidataEditCountResponse
import io.reactivex.Observable
import io.reactivex.Single
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient
@ -50,8 +55,10 @@ class OkHttpJsonApiClient @Inject constructor(
): Observable<LeaderboardResponse> {
val fetchLeaderboardUrlTemplate =
wikiMediaToolforgeUrl.toString() + LeaderboardConstants.LEADERBOARD_END_POINT
val url = String.format(Locale.ENGLISH,
fetchLeaderboardUrlTemplate, userName, duration, category, limit, offset)
val url = String.format(
Locale.ENGLISH,
fetchLeaderboardUrlTemplate, userName, duration, category, limit, offset
)
val urlBuilder: HttpUrl.Builder = url.toHttpUrlOrNull()!!.newBuilder()
.addQueryParameter("user", userName)
.addQueryParameter("duration", duration)
@ -80,6 +87,80 @@ class OkHttpJsonApiClient @Inject constructor(
})
}
/**
* Show where file is being used on Commons.
*/
suspend fun getFileUsagesOnCommons(
fileName: String?,
pageSize: Int
): FileUsagesResponse? {
return withContext(Dispatchers.IO) {
return@withContext try {
val urlBuilder = BuildConfig.FILE_USAGES_BASE_URL.toHttpUrlOrNull()!!.newBuilder()
urlBuilder.addQueryParameter("prop", "fileusage")
urlBuilder.addQueryParameter("titles", fileName)
urlBuilder.addQueryParameter("fulimit", pageSize.toString())
Timber.i("Url %s", urlBuilder.toString())
val request: Request = Request.Builder()
.url(urlBuilder.toString())
.build()
val response: Response = okHttpClient.newCall(request).execute()
if (response.body != null && response.isSuccessful) {
val json: String = response.body!!.string()
gson.fromJson<FileUsagesResponse>(
json,
FileUsagesResponse::class.java
)
} else null
} catch (e: Exception) {
Timber.e(e)
null
}
}
}
/**
* Show where file is being used on non-Commons wikis, typically the Wikipedias in various languages.
*/
suspend fun getGlobalFileUsages(
fileName: String?,
pageSize: Int
): GlobalFileUsagesResponse? {
return withContext(Dispatchers.IO) {
return@withContext try {
val urlBuilder = BuildConfig.FILE_USAGES_BASE_URL.toHttpUrlOrNull()!!.newBuilder()
urlBuilder.addQueryParameter("prop", "globalusage")
urlBuilder.addQueryParameter("titles", fileName)
urlBuilder.addQueryParameter("gulimit", pageSize.toString())
Timber.i("Url %s", urlBuilder.toString())
val request: Request = Request.Builder()
.url(urlBuilder.toString())
.build()
val response: Response = okHttpClient.newCall(request).execute()
if (response.body != null && response.isSuccessful) {
val json: String = response.body!!.string()
gson.fromJson<GlobalFileUsagesResponse>(
json,
GlobalFileUsagesResponse::class.java
)
} else null
} catch (e: Exception) {
Timber.e(e)
null
}
}
}
fun setAvatar(username: String?, avatar: String?): Single<UpdateAvatarResponse?> {
val urlTemplate = wikiMediaToolforgeUrl
.toString() + LeaderboardConstants.UPDATE_AVATAR_END_POINT