Improve credit line in image list (#6295)
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run

- When author is not uploader, show both.
- When failing to parse author from HTML, use structured data.
This commit is contained in:
Yusuke Matsubara 2025-04-23 23:23:09 +09:00 committed by GitHub
parent 30762971db
commit 329a68216e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 363 additions and 81 deletions

View file

@ -333,6 +333,7 @@ android {
buildConfigField "String", "TEST_USERNAME", "\"" + getTestUserName() + "\""
buildConfigField "String", "TEST_PASSWORD", "\"" + getTestPassword() + "\""
buildConfigField "String", "DEPICTS_PROPERTY", "\"P180\""
buildConfigField "String", "CREATOR_PROPERTY", "\"P170\""
dimension 'tier'
}
@ -370,6 +371,7 @@ android {
buildConfigField "String", "TEST_USERNAME", "\"" + getTestUserName() + "\""
buildConfigField "String", "TEST_PASSWORD", "\"" + getTestPassword() + "\""
buildConfigField "String", "DEPICTS_PROPERTY", "\"P245962\""
buildConfigField "String", "CREATOR_PROPERTY", "\"P253075\""
dimension 'tier'
}
}

View file

@ -53,6 +53,7 @@ class Media constructor(
*/
var author: String? = null,
var user: String? = null,
var creatorName: String? = null,
/**
* Gets the categories the file falls under.
* @return file categories as an ArrayList of Strings
@ -66,6 +67,7 @@ class Media constructor(
var captions: Map<String, String> = emptyMap(),
var descriptions: Map<String, String> = emptyMap(),
var depictionIds: List<String> = emptyList(),
var creatorIds: List<String> = emptyList(),
/**
* This field was added to find non-hidden categories
* Stores the mapping of category title to hidden attribute
@ -130,6 +132,7 @@ class Media constructor(
* returns user
* @return Author or User
*/
@Deprecated("Use user for uploader username. Use attributedAuthor() for attribution. Note that the uploader may not be the creator/author.")
fun getAuthorOrUser(): String? {
return if (!author.isNullOrEmpty()) {
author
@ -138,6 +141,19 @@ class Media constructor(
}
}
/**
* Returns author if it's not null or empty, otherwise
* returns creator name
* @return name of author or creator
*/
fun getAttributedAuthor(): String? {
return if (!author.isNullOrEmpty()) {
author
} else{
creatorName
}
}
/**
* Gets media display title
* @return Media title

View file

@ -1,7 +1,7 @@
package fr.free.nrw.commons
import androidx.core.text.HtmlCompat
import fr.free.nrw.commons.media.IdAndCaptions
import fr.free.nrw.commons.media.IdAndLabels
import fr.free.nrw.commons.media.MediaClient
import fr.free.nrw.commons.media.PAGE_ID_PREFIX
import io.reactivex.Single
@ -23,13 +23,23 @@ class MediaDataExtractor
private val mediaClient: MediaClient,
) {
fun fetchDepictionIdsAndLabels(media: Media) =
mediaClient
mediaClient
.getEntities(media.depictionIds)
.map {
it
.entities()
.mapValues { entry -> entry.value.labels().mapValues { it.value.value() } }
}.map { it.map { (key, value) -> IdAndCaptions(key, value) } }
}.map { it.map { (key, value) -> IdAndLabels(key, value) } }
.onErrorReturn { emptyList() }
fun fetchCreatorIdsAndLabels(media: Media) =
mediaClient
.getEntities(media.creatorIds)
.map {
it
.entities()
.mapValues { entry -> entry.value.labels().mapValues { it.value.value() } }
}.map { it.map { (key, value) -> IdAndLabels(key, value) } }
.onErrorReturn { emptyList() }
fun checkDeletionRequestExists(media: Media) = mediaClient.checkPageExistsUsingTitle("Commons:Deletion_requests/" + media.filename)

View file

@ -8,23 +8,29 @@ import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.RecyclerView
import com.facebook.imagepipeline.request.ImageRequest
import com.facebook.imagepipeline.request.ImageRequestBuilder
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.utils.MediaAttributionUtil
import fr.free.nrw.commons.MediaDataExtractor
import fr.free.nrw.commons.R
import fr.free.nrw.commons.databinding.LayoutContributionBinding
import fr.free.nrw.commons.media.MediaClient
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.schedulers.Schedulers
import timber.log.Timber
import java.io.File
class ContributionViewHolder internal constructor(
private val parent: View, private val callback: ContributionsListAdapter.Callback,
private val mediaClient: MediaClient
parent: View,
private val callback: ContributionsListAdapter.Callback,
private val compositeDisposable: CompositeDisposable,
private val mediaClient: MediaClient,
private val mediaDataExtractor: MediaDataExtractor
) : RecyclerView.ViewHolder(parent) {
var binding: LayoutContributionBinding = LayoutContributionBinding.bind(parent)
private var position = 0
private var contribution: Contribution? = null
private val compositeDisposable = CompositeDisposable()
private var isWikipediaButtonDisplayed = false
private val pausingPopUp: AlertDialog
var imageRequest: ImageRequest? = null
@ -54,7 +60,7 @@ an upload might take a dozen seconds. */
this.contribution = contribution
this.position = position
binding.contributionTitle.text = contribution.media.mostRelevantCaption
binding.authorView.text = contribution.media.getAuthorOrUser()
setAuthorText(contribution.media)
//Removes flicker of loading image.
binding.contributionImage.hierarchy.fadeDuration = 0
@ -93,6 +99,30 @@ an upload might take a dozen seconds. */
checkIfMediaExistsOnWikipediaPage(contribution)
}
fun updateAttribution() {
if (contribution != null) {
val media = contribution!!.media
if (!media.getAttributedAuthor().isNullOrEmpty()) {
return
}
compositeDisposable.addAll(
mediaDataExtractor.fetchCreatorIdsAndLabels(media)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ idAndLabels ->
media.creatorName = MediaAttributionUtil.getCreatorName(idAndLabels)
setAuthorText(media)
},
{ t: Throwable? -> Timber.e(t) })
)
}
}
private fun setAuthorText(media: Media) {
binding.authorView.text = MediaAttributionUtil.getTagLine(media, itemView.context)
}
/**
* Checks if a media exists on the corresponding Wikipedia article Currently the check is made
* for the device's current language Wikipedia

View file

@ -4,21 +4,26 @@ import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.paging.PagedListAdapter
import androidx.recyclerview.widget.DiffUtil
import fr.free.nrw.commons.MediaDataExtractor
import fr.free.nrw.commons.R
import fr.free.nrw.commons.media.MediaClient
import io.reactivex.disposables.CompositeDisposable
/**
* Represents The View Adapter for the List of Contributions
*/
class ContributionsListAdapter internal constructor(
private val callback: Callback,
private val mediaClient: MediaClient
private val mediaClient: MediaClient,
private val mediaDataExtractor: MediaDataExtractor,
private val compositeDisposable: CompositeDisposable
) : PagedListAdapter<Contribution, ContributionViewHolder>(DIFF_CALLBACK) {
/**
* Initializes the view holder with contribution data
*/
override fun onBindViewHolder(holder: ContributionViewHolder, position: Int) {
holder.init(position, getItem(position))
holder.updateAttribution()
}
fun getContributionForPosition(position: Int): Contribution? {
@ -36,7 +41,7 @@ class ContributionsListAdapter internal constructor(
val viewHolder = ContributionViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.layout_contribution, parent, false),
callback, mediaClient
callback, compositeDisposable, mediaClient, mediaDataExtractor
)
return viewHolder
}

View file

@ -27,6 +27,7 @@ import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver
import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener
import androidx.recyclerview.widget.SimpleItemAnimator
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.MediaDataExtractor
import fr.free.nrw.commons.R
import fr.free.nrw.commons.Utils
import fr.free.nrw.commons.auth.SessionManager
@ -63,6 +64,10 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL
@Inject
var mediaClient: MediaClient? = null
@JvmField
@Inject
var mediaDataExtractor: MediaDataExtractor? = null
@JvmField
@Named(NetworkingModule.NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE)
@Inject
@ -231,7 +236,7 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL
}
private fun initAdapter() {
adapter = ContributionsListAdapter(this, mediaClient!!)
adapter = ContributionsListAdapter(this, mediaClient!!, mediaDataExtractor!!, compositeDisposable)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

View file

@ -18,6 +18,12 @@ import javax.inject.Inject
class MediaConverter
@Inject
constructor() {
/**
* Creating Media object from MWQueryPage.
*
* @param page response from the API
* @return Media object
*/
fun convert(
page: MwQueryPage,
entity: Entities.Entity,
@ -40,24 +46,17 @@ class MediaConverter
metadata.prefixedLicenseUrl,
getAuthor(metadata),
imageInfo.getUser(),
null,
MediaDataExtractorUtil.extractCategoriesFromList(metadata.categories()),
metadata.latLng,
entity.labels().mapValues { it.value.value() },
entity.descriptions().mapValues { it.value.value() },
entity.depictionIds(),
entity.creatorIds(),
myMap,
)
}
/**
* Creating Media object from MWQueryPage.
* Earlier only basic details were set for the media object but going forward,
* a full media object(with categories, descriptions, coordinates etc) can be constructed using this method
*
* @param page response from the API
* @return Media object
*/
private fun safeParseDate(dateStr: String): Date? =
try {
CommonsDateUtil.getMediaSimpleDateFormat().parse(dateStr)
@ -66,24 +65,32 @@ class MediaConverter
}
/**
* This method extracts the Commons Username from the artist HTML information
* This method extracts the Commons Username from the artist HTML information.
* When the HTML is in customized formatting, it may fail to parse and return null.
* @param metadata
* @return
*/
private fun getAuthor(metadata: ExtMetadata): String? {
return try {
val authorHtml = metadata.artist()
val anchorStartTagTerminalChars = "\">"
val anchorCloseTag = "</a>"
val authorHtml = metadata.artist()
val anchorStartTagTerminalString = "\">"
val anchorCloseTag = "</a>"
return authorHtml.substring(
authorHtml.indexOf(anchorStartTagTerminalChars) +
anchorStartTagTerminalChars
.length,
return if (!authorHtml.contains("<") && !authorHtml.contains(">") ) {
authorHtml.trim()
} else if (!authorHtml.contains(anchorStartTagTerminalString) || !authorHtml.endsWith(anchorCloseTag)) {
null
} else {
val authorText = authorHtml.substring(
authorHtml.indexOf(anchorStartTagTerminalString) +
anchorStartTagTerminalString.length,
authorHtml.indexOf(anchorCloseTag),
)
} catch (ex: java.lang.Exception) {
""
if (authorText.contains("<") || authorText.contains(">")) {
null
} else {
authorText
}
}
}
}
@ -92,6 +99,10 @@ private fun Entities.Entity.depictionIds() =
this[WikidataProperties.DEPICTS]?.mapNotNull { (it.mainSnak.dataValue as? DataValue.EntityId)?.value?.id }
?: emptyList()
private fun Entities.Entity.creatorIds() =
this[WikidataProperties.CREATOR]?.mapNotNull { (it.mainSnak.dataValue as? DataValue.EntityId)?.value?.id }
?: emptyList()
private val ExtMetadata.prefixedLicenseUrl: String
get() =
licenseUrl().let {

View file

@ -4,16 +4,18 @@ import android.content.Context
import android.os.Bundle
import android.view.View
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.MediaDataExtractor
import fr.free.nrw.commons.R
import fr.free.nrw.commons.category.CategoryImagesCallback
import fr.free.nrw.commons.explore.paging.BasePagingFragment
import fr.free.nrw.commons.media.MediaDetailPagerFragment.MediaDetailProvider
import javax.inject.Inject
abstract class PageableMediaFragment :
BasePagingFragment<Media>(),
MediaDetailProvider {
override val pagedListAdapter by lazy {
PagedMediaAdapter(categoryImagesCallback::onMediaClicked)
PagedMediaAdapter(categoryImagesCallback::onMediaClicked, mediaDataExtractor)
}
override val errorTextId: Int = R.string.error_loading_images
@ -22,6 +24,9 @@ abstract class PageableMediaFragment :
lateinit var categoryImagesCallback: CategoryImagesCallback
@Inject
lateinit var mediaDataExtractor: MediaDataExtractor
override fun onAttach(context: Context) {
super.onAttach(context)
if (parentFragment != null) {

View file

@ -5,13 +5,22 @@ import android.view.ViewGroup
import androidx.paging.PagedListAdapter
import androidx.recyclerview.widget.DiffUtil
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.MediaDataExtractor
import fr.free.nrw.commons.utils.MediaAttributionUtil
import fr.free.nrw.commons.R
import fr.free.nrw.commons.databinding.LayoutCategoryImagesBinding
import fr.free.nrw.commons.explore.paging.BaseViewHolder
import fr.free.nrw.commons.explore.paging.inflate
import fr.free.nrw.commons.media.IdAndLabels
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.schedulers.Schedulers
import timber.log.Timber
class PagedMediaAdapter(
private val onImageClicked: (Int) -> Unit,
private val mediaDataExtractor: MediaDataExtractor,
private val compositeDisposable: CompositeDisposable = CompositeDisposable()
) : PagedListAdapter<Media, SearchImagesViewHolder>(
object : DiffUtil.ItemCallback<Media>() {
override fun areItemsTheSame(
@ -25,6 +34,7 @@ class PagedMediaAdapter(
) = oldItem.pageId == newItem.pageId
},
) {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int,
@ -37,7 +47,24 @@ class PagedMediaAdapter(
holder: SearchImagesViewHolder,
position: Int,
) {
holder.bind(getItem(position)!! to position)
val media = getItem(position) ?: return
holder.bind(media to position)
if (!media.getAttributedAuthor().isNullOrEmpty()) {
return
}
compositeDisposable.addAll(
mediaDataExtractor.fetchCreatorIdsAndLabels(media)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ idAndLabels ->
media.creatorName = MediaAttributionUtil.getCreatorName(idAndLabels);
holder.setAuthorText(media)
},
{ t: Throwable? -> Timber.e(t) })
)
}
}
@ -52,7 +79,10 @@ class SearchImagesViewHolder(
binding.categoryImageView.setOnClickListener { onImageClicked(item.second) }
binding.categoryImageTitle.text = media.mostRelevantCaption
binding.categoryImageView.setImageURI(media.thumbUrl)
binding.categoryImageAuthor.text =
containerView.context.getString(R.string.image_uploaded_by, media.getAuthorOrUser())
setAuthorText(media)
}
fun setAuthorText(media: Media) {
binding.categoryImageAuthor.text = MediaAttributionUtil.getTagLine(media, containerView.context)
}
}

View file

@ -1,6 +0,0 @@
package fr.free.nrw.commons.media
data class IdAndCaptions(
val id: String,
val captions: Map<String, String>,
)

View file

@ -0,0 +1,18 @@
package fr.free.nrw.commons.media
data class IdAndLabels(
val id: String,
val labels: Map<String, String>,
) {
// if a label is available in user's locale, return it
// if not then check for english, else show any available.
fun getLocalizedLabel(locale: String): String? {
if (labels[locale] != null) {
return labels[locale]
}
if (labels["en"] != null) {
return labels["en"]
}
return labels.values.firstOrNull() ?: id
}
}

View file

@ -16,7 +16,6 @@ import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.view.ViewTreeObserver.OnGlobalLayoutListener
import android.widget.ArrayAdapter
import android.widget.Button
@ -622,10 +621,9 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ idAndCaptions: List<IdAndCaptions> -> onDepictionsLoaded(idAndCaptions) },
{ IdAndLabels: List<IdAndLabels> -> onDepictionsLoaded(IdAndLabels) },
{ t: Throwable? -> Timber.e(t) })
)
// compositeDisposable.add(disposable);
}
private fun onDiscussionLoaded(discussion: String) {
@ -655,10 +653,10 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
}
}
private fun onDepictionsLoaded(idAndCaptions: List<IdAndCaptions>) {
private fun onDepictionsLoaded(IdAndLabels: List<IdAndLabels>) {
binding.depictsLayout.visibility = View.VISIBLE
binding.depictionsEditButton.visibility = View.VISIBLE
buildDepictionList(idAndCaptions)
buildDepictionList(IdAndLabels)
}
/**
@ -863,26 +861,26 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
/**
* Populates media details fragment with depiction list
* @param idAndCaptions
* @param IdAndLabels
*/
private fun buildDepictionList(idAndCaptions: List<IdAndCaptions>) {
private fun buildDepictionList(IdAndLabels: List<IdAndLabels>) {
binding.mediaDetailDepictionContainer.removeAllViews()
// Create a mutable list from the original list
val mutableIdAndCaptions = idAndCaptions.toMutableList()
val mutableIdAndLabels = IdAndLabels.toMutableList()
if (mutableIdAndCaptions.isEmpty()) {
// Create a placeholder IdAndCaptions object and add it to the list
mutableIdAndCaptions.add(
IdAndCaptions(
if (mutableIdAndLabels.isEmpty()) {
// Create a placeholder IdAndLabels object and add it to the list
mutableIdAndLabels.add(
IdAndLabels(
id = media?.pageId ?: "", // Use an empty string if media?.pageId is null
captions = mapOf(Locale.getDefault().language to getString(R.string.detail_panel_cats_none)) // Create a Map with the language as the key and the message as the value
labels = mapOf(Locale.getDefault().language to getString(R.string.detail_panel_cats_none)) // Create a Map with the language as the key and the message as the value
)
)
}
val locale: String = Locale.getDefault().language
for (idAndCaption: IdAndCaptions in mutableIdAndCaptions) {
for (idAndCaption: IdAndLabels in mutableIdAndLabels) {
binding.mediaDetailDepictionContainer.addView(
buildDepictLabel(
getDepictionCaption(idAndCaption, locale),
@ -894,16 +892,16 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
}
private fun getDepictionCaption(idAndCaption: IdAndCaptions, locale: String): String? {
private fun getDepictionCaption(idAndCaption: IdAndLabels, locale: String): String? {
// Check if the Depiction Caption is available in user's locale
// if not then check for english, else show any available.
if (idAndCaption.captions[locale] != null) {
return idAndCaption.captions[locale]
if (idAndCaption.labels[locale] != null) {
return idAndCaption.labels[locale]
}
if (idAndCaption.captions["en"] != null) {
return idAndCaption.captions["en"]
if (idAndCaption.labels["en"] != null) {
return idAndCaption.labels["en"]
}
return idAndCaption.captions.values.iterator().next()
return idAndCaption.labels.values.iterator().next()
}
private fun onMediaDetailLicenceClicked() {

View file

@ -0,0 +1,39 @@
package fr.free.nrw.commons.utils
import android.content.Context
import android.icu.text.ListFormatter
import android.os.Build
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.R
import fr.free.nrw.commons.media.IdAndLabels
import java.util.Locale
object MediaAttributionUtil {
fun getTagLine(media: Media, context: Context): String {
val uploader = media.user
val author = media.getAttributedAuthor()
return if (author.isNullOrEmpty()) {
context.getString(R.string.image_uploaded_by, uploader)
} else if (author == uploader) {
context.getString(R.string.image_tag_line_created_and_uploaded_by, author)
} else {
context.getString(
R.string.image_tag_line_created_by_and_uploaded_by,
author,
uploader
)
}
}
fun getCreatorName(idAndLabels: List<IdAndLabels>): String? {
val locale = Locale.getDefault()
val names = idAndLabels.map{ x -> x.getLocalizedLabel(locale.language)}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val formatter = ListFormatter.getInstance(locale)
return formatter.format(names)
} else {
return names.joinToString(", ")
}
}
}

View file

@ -1,9 +1,8 @@
package fr.free.nrw.commons.utils
import android.os.Build
import android.text.Html
import android.text.Spanned
import android.text.SpannedString
import androidx.core.text.HtmlCompat
object StringUtil {
@ -26,12 +25,6 @@ object StringUtil {
.replace("&#8207;", "\u200F")
.replace("&amp;", "&")
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Html.fromHtml(processedSource, Html.FROM_HTML_MODE_LEGACY)
} else {
//noinspection deprecation
@Suppress("DEPRECATION")
Html.fromHtml(processedSource)
}
return HtmlCompat.fromHtml(processedSource, HtmlCompat.FROM_HTML_MODE_LEGACY)
}
}

View file

@ -7,6 +7,7 @@ enum class WikidataProperties(
) {
IMAGE("P18"),
DEPICTS(BuildConfig.DEPICTS_PROPERTY),
CREATOR(BuildConfig.CREATOR_PROPERTY),
COMMONS_CATEGORY("P373"),
INSTANCE_OF("P31"),
MEDIA_LEGENDS("P2096"),

View file

@ -1,7 +1,6 @@
package fr.free.nrw.commons.wikidata.model.gallery
import com.google.gson.annotations.SerializedName
import org.apache.commons.lang3.StringUtils
class ExtMetadata {
@SerializedName("DateTime") private val dateTime: Values? = null

View file

@ -873,4 +873,6 @@ Upload your first media by tapping on the add button.</string>
<string name="show_in_explore">Show in Explore</string>
<string name="show_in_nearby">Show in Nearby</string>
<string name="image_tag_line_created_and_uploaded_by">Created and uploaded by: %1$s</string>
<string name="image_tag_line_created_by_and_uploaded_by">Created by %1$s and uploaded by %2$s</string>
</resources>

View file

@ -50,6 +50,7 @@ fun media(
licenseUrl: String? = "licenseUrl",
author: String? = "creator",
user: String? = "user",
creatorName: String? = null,
pageId: String = "pageId",
categories: List<String>? = listOf("categories"),
coordinates: LatLng? = LatLng(0.0, 0.0, 0.0f),
@ -67,6 +68,7 @@ fun media(
licenseUrl,
author,
user,
creatorName,
categories,
coordinates,
captions,

View file

@ -8,6 +8,7 @@ import androidx.test.core.app.ApplicationProvider
import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.soloader.SoLoader
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.MediaDataExtractor
import fr.free.nrw.commons.R
import fr.free.nrw.commons.TestCommonsApplication
import fr.free.nrw.commons.TestUtility.setFinalStatic
@ -46,6 +47,9 @@ class ContributionViewHolderUnitTests {
@Mock
private lateinit var mediaClient: MediaClient
@Mock
private lateinit var mediaDataExtractor: MediaDataExtractor
@Mock
private lateinit var uri: Uri
@ -66,8 +70,9 @@ class ContributionViewHolderUnitTests {
SoLoader.setInTestMode()
Fresco.initialize(ApplicationProvider.getApplicationContext())
activity = Robolectric.buildActivity(ProfileActivity::class.java).create().get()
compositeDisposable = CompositeDisposable()
parent = LayoutInflater.from(activity).inflate(R.layout.layout_contribution, null)
contributionViewHolder = ContributionViewHolder(parent, callback, mediaClient)
contributionViewHolder = ContributionViewHolder(parent, callback, compositeDisposable, mediaClient, mediaDataExtractor)
bindind = LayoutContributionBinding.bind(parent)

View file

@ -10,6 +10,7 @@ import org.junit.Before
import org.junit.Test
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
import java.lang.IllegalArgumentException
@ -42,23 +43,61 @@ class MediaConverterTest {
@Test
fun testConvertIfThumbUrlBlank() {
Mockito.`when`(imageInfo.getMetadata()).thenReturn(metadata)
Mockito.`when`(imageInfo.getThumbUrl()).thenReturn("")
Mockito.`when`(imageInfo.getOriginalUrl()).thenReturn("originalUrl")
Mockito.`when`(imageInfo.getMetadata()?.licenseUrl()).thenReturn("licenseUrl")
Mockito.`when`(imageInfo.getMetadata()?.dateTime()).thenReturn("yyyy-MM-dd HH:mm:ss")
`when`(imageInfo.getMetadata()).thenReturn(metadata)
`when`(imageInfo.getThumbUrl()).thenReturn("")
`when`(imageInfo.getOriginalUrl()).thenReturn("originalUrl")
`when`(metadata.licenseUrl()).thenReturn("licenseUrl")
`when`(metadata.dateTime()).thenReturn("yyyy-MM-dd HH:mm:ss")
`when`(metadata.artist()).thenReturn("Foo Bar")
media = mediaConverter.convert(page, entity, imageInfo)
assertEquals(media.thumbUrl, media.imageUrl, "originalUrl")
}
@Test
fun testConvertIfThumbUrlNotBlank() {
Mockito.`when`(imageInfo.getMetadata()).thenReturn(metadata)
Mockito.`when`(imageInfo.getThumbUrl()).thenReturn("thumbUrl")
Mockito.`when`(imageInfo.getOriginalUrl()).thenReturn("originalUrl")
Mockito.`when`(imageInfo.getMetadata()?.licenseUrl()).thenReturn("licenseUrl")
Mockito.`when`(imageInfo.getMetadata()?.dateTime()).thenReturn("yyyy-MM-dd HH:mm:ss")
`when`(imageInfo.getMetadata()).thenReturn(metadata)
`when`(imageInfo.getThumbUrl()).thenReturn("thumbUrl")
`when`(imageInfo.getOriginalUrl()).thenReturn("originalUrl")
`when`(metadata.licenseUrl()).thenReturn("licenseUrl")
`when`(metadata.dateTime()).thenReturn("yyyy-MM-dd HH:mm:ss")
`when`(metadata.artist()).thenReturn("Foo Bar")
media = mediaConverter.convert(page, entity, imageInfo)
assertEquals(media.thumbUrl, "thumbUrl")
}
@Test
fun `test converting artist value (author) with html links`() {
`when`(imageInfo.getMetadata()).thenReturn(metadata)
`when`(imageInfo.getThumbUrl()).thenReturn("thumbUrl")
`when`(imageInfo.getOriginalUrl()).thenReturn("originalUrl")
`when`(metadata.licenseUrl()).thenReturn("licenseUrl")
`when`(metadata.dateTime()).thenReturn("yyyy-MM-dd HH:mm:ss")
`when`(metadata.artist()).thenReturn("<a href=\"//commons.wikimedia.org/wiki/User:Foo_Bar\" title=\"Foo Bar\">Foo Bar</a>")
// Artist values like above is very common, found in file pages created via UploadWizard
media = mediaConverter.convert(page, entity, imageInfo)
assertEquals("Foo Bar", media.author)
}
@Test
fun `test convert artist value (author) in plain text`() {
`when`(imageInfo.getMetadata()).thenReturn(metadata)
`when`(imageInfo.getThumbUrl()).thenReturn("thumbUrl")
`when`(imageInfo.getOriginalUrl()).thenReturn("originalUrl")
`when`(metadata.licenseUrl()).thenReturn("licenseUrl")
`when`(metadata.dateTime()).thenReturn("yyyy-MM-dd HH:mm:ss")
`when`(metadata.artist()).thenReturn("Foo Bar")
media = mediaConverter.convert(page, entity, imageInfo)
assertEquals("Foo Bar", media.author)
}
@Test
fun `test convert artist value (author) containing red link`() {
`when`(imageInfo.getMetadata()).thenReturn(metadata)
`when`(imageInfo.getThumbUrl()).thenReturn("thumbUrl")
`when`(imageInfo.getOriginalUrl()).thenReturn("originalUrl")
`when`(metadata.licenseUrl()).thenReturn("licenseUrl")
`when`(metadata.dateTime()).thenReturn("yyyy-MM-dd HH:mm:ss")
`when`(metadata.artist()).thenReturn("<a href=\"/w/index.php?title=User:Foo&action=edit&redlink=1\" class=\"new\" title=\"User:Foo (page does not exist)\">Foo</a>")
media = mediaConverter.convert(page, entity, imageInfo)
assertEquals("Foo", media.author)
}
}

View file

@ -0,0 +1,78 @@
package fr.free.nrw.commons.utils
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import com.nhaarman.mockitokotlin2.whenever
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.TestCommonsApplication
import fr.free.nrw.commons.media.IdAndLabels
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.mock
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [21], application = TestCommonsApplication::class, qualifiers="en-rUS")
class MediaAttributionUtilTest {
@Mock
private lateinit var appContext: Context
@Before
fun setup() {
appContext = ApplicationProvider.getApplicationContext()
}
@Test
fun getTagLineWithUploaderOnly() {
val media = mock(Media::class.java)
whenever(media.user).thenReturn("TestUploader")
whenever(media.author).thenReturn(null)
assertEquals("Uploaded by: TestUploader",
MediaAttributionUtil.getTagLine(media, appContext))
}
@Test
fun `get tag line from same author and uploader`() {
val media = mock(Media::class.java)
whenever(media.user).thenReturn("TestUser")
whenever(media.getAttributedAuthor()).thenReturn("TestUser")
assertEquals("Created and uploaded by: TestUser",
MediaAttributionUtil.getTagLine(media, appContext))
}
@Test
fun `get creator name from EN label`() {
assertEquals("FooBar",
MediaAttributionUtil.getCreatorName(listOf(IdAndLabels("Q1", mapOf("en" to "FooBar")))))
}
@Test
fun `get creator name from ES label`() {
assertEquals("FooBar",
MediaAttributionUtil.getCreatorName(listOf(IdAndLabels("Q2", mapOf("es" to "FooBar")))))
}
@Test
fun `get creator name from EN label and ignore ES label`() {
assertEquals("Bar",
MediaAttributionUtil.getCreatorName(listOf(
IdAndLabels("Q3", mapOf("en" to "Bar", "es" to "Foo")))))
}
@Test
fun `get creator name from two creators`() {
val name = MediaAttributionUtil.getCreatorName(listOf(
IdAndLabels("Q1", mapOf("en" to "Foo")),
IdAndLabels("Q1", mapOf("en" to "Bar"))
))
assertNotNull(name)
assertTrue(name!!.contains("Foo"))
assertTrue(name.contains("Bar"))
}
}