#3780 Create media using a combination of Entities & MwQueryResult - construct media with an entity - move fields from media down to contribution - move dynamic fields outside of media - remove unused constructors - remove all unnecessary fetching of captions/descriptions - bump database version

This commit is contained in:
Sean Mac Gillicuddy 2020-05-29 14:30:10 +01:00
parent d85d11441d
commit e96b7f6f81
45 changed files with 927 additions and 1486 deletions

View file

@ -1,6 +1,5 @@
package fr.free.nrw.commons;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
@ -8,44 +7,24 @@ import androidx.annotation.Nullable;
import androidx.room.Entity;
import androidx.room.PrimaryKey;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.media.Depictions;
import fr.free.nrw.commons.utils.CommonsDateUtil;
import fr.free.nrw.commons.utils.MediaDataExtractorUtil;
import java.text.ParseException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import org.apache.commons.lang3.StringUtils;
import org.wikipedia.dataclient.mwapi.MwQueryPage;
import org.wikipedia.gallery.ExtMetadata;
import org.wikipedia.gallery.ImageInfo;
import org.jetbrains.annotations.NotNull;
import org.wikipedia.page.PageTitle;
@Entity
public class Media implements Parcelable {
public static final Media EMPTY = new Media("");
// Primary metadata fields
@Nullable
private Uri localUri;
private String thumbUrl;
private String imageUrl;
private String filename;
private String thumbnailTitle;
/*
* Captions are a feature part of Structured data. They are meant to store short, multilingual descriptions about files
* This is a replacement of the previously used titles for images (titles were not multilingual)
* Also now captions replace the previous convention of using title for filename
*/
private String caption;
private String description; // monolingual description on input...
private String discussion;
private long dataLength;
private Date dateCreated;
private String fallbackDescription; // monolingual description on input...
@Nullable private Date dateUploaded;
private String license;
private String licenseUrl;
@ -57,13 +36,14 @@ public class Media implements Parcelable {
@NonNull
private String pageId;
private List<String> categories; // as loaded at runtime?
/**
* Depicts is a feature part of Structured data. Multiple Depictions can be added for an image just like categories.
* However unlike categories depictions is multi-lingual
*/
private Depictions depictions;
private boolean requestedDeletion;
@Nullable private LatLng coordinates;
@NotNull
private Map<String, String> captions = Collections.emptyMap();
@NotNull
private Map<String, String> descriptions = Collections.emptyMap();
@NotNull
private List<String> depictionIds = Collections.emptyList();
/**
* Provides local constructor
@ -72,6 +52,96 @@ public class Media implements Parcelable {
pageId = UUID.randomUUID().toString();
}
/**
* Provide Media constructor
* @param imageUrl Media image URL
* @param filename Media filename
* @param fallbackDescription Media description
* @param dateUploaded Media date uploaded
* @param creator Media creator
*/
public Media(final String imageUrl, final String filename,
final String fallbackDescription,
final Date dateUploaded,
final String creator) {
this();
thumbUrl = imageUrl;
this.imageUrl = imageUrl;
this.filename = filename;
this.fallbackDescription = fallbackDescription;
this.dateUploaded = dateUploaded;
this.creator = creator;
}
/**
* Constructor with all parameters
*/
public Media(final String thumbUrl,
final String imageUrl,
final String filename,
final String fallbackDescription,
@Nullable final Date dateUploaded,
final String license,
final String licenseUrl,
final String creator,
@NonNull final String pageId,
final List<String> categories,
@Nullable final LatLng coordinates,
@NotNull final Map<String, String> captions,
@NotNull final Map<String, String> descriptions,
@NotNull final List<String> depictionIds) {
this.thumbUrl = thumbUrl;
this.imageUrl = imageUrl;
this.filename = filename;
this.fallbackDescription = fallbackDescription;
this.dateUploaded = dateUploaded;
this.license = license;
this.licenseUrl = licenseUrl;
this.creator = creator;
this.pageId = pageId;
this.categories = categories;
this.coordinates = coordinates;
this.captions = captions;
this.descriptions = descriptions;
this.depictionIds = depictionIds;
}
public Media(Media media) {
this(media.getThumbUrl(), media.getImageUrl(), media.getFilename(),
media.getFallbackDescription(), media.getDateUploaded(), media.getLicense(),
media.getLicenseUrl(), media.getCreator(), media.getPageId(), media.getCategories(),
media.getCoordinates(), media.getCaptions(),media.getDescriptions(),media.getDepictionIds());
}
public Media(final String filename,
Map<String, String> captions, final String fallbackDescription,
final String creator, final List<String> categories) {
this(null, filename, fallbackDescription, new Date(), creator);
this.categories = categories;
this.captions=captions;
}
protected Media(final Parcel in) {
this(in.readString(), in.readString(), in.readString(),
in.readString(), readDateUploaded(in), in.readString(),
in.readString(), in.readString(), in.readString(), readList(in),
in.readParcelable(LatLng.class.getClassLoader()),
((Map<String, String>) in.readSerializable()),
((Map<String, String>) in.readSerializable()),
readList(in));
}
private static List<String> readList(Parcel in) {
final ArrayList<String> list = new ArrayList<>();
in.readStringList(list);
return list;
}
private static Date readDateUploaded(Parcel in) {
final long tmpDateUploaded = in.readLong();
return tmpDateUploaded == -1 ? null : new Date(tmpDateUploaded);
}
public static final Creator<Media> CREATOR = new Creator<Media>() {
@Override
public Media createFromParcel(final Parcel source) {
@ -84,192 +154,6 @@ public class Media implements Parcelable {
}
};
/**
* Provides a minimal constructor
*
* @param filename Media filename
*/
public Media(final String filename) {
this();
this.filename = filename;
}
/**
* Provide Media constructor
* @param localUri Media URI
* @param imageUrl Media image URL
* @param filename Media filename
* @param description Media description
* @param dataLength Media date length
* @param dateCreated Media creation date
* @param dateUploaded Media date uploaded
* @param creator Media creator
*/
public Media(final Uri localUri, final String imageUrl, final String filename,
final String description,
final long dataLength, final Date dateCreated, final Date dateUploaded,
final String creator) {
this();
this.localUri = localUri;
thumbUrl = imageUrl;
this.imageUrl = imageUrl;
this.filename = filename;
this.description = description;
this.dataLength = dataLength;
this.dateCreated = dateCreated;
this.dateUploaded = dateUploaded;
this.creator = creator;
}
/**
* Constructor with all parameters
*/
public Media(final String pageId,
final Uri localUri,
final String thumbUrl,
final String imageUrl,
final String filename,
final String description,
final String discussion,
final long dataLength,
final Date dateCreated,
final Date dateUploaded,
final String license,
final String licenseUrl,
final String creator,
final List<String> categories,
final boolean requestedDeletion,
final LatLng coordinates) {
this.pageId = pageId;
this.localUri = localUri;
this.thumbUrl = thumbUrl;
this.imageUrl = imageUrl;
this.filename = filename;
this.description = description;
this.discussion = discussion;
this.dataLength = dataLength;
this.dateCreated = dateCreated;
this.dateUploaded = dateUploaded;
this.license = license;
this.licenseUrl = licenseUrl;
this.creator = creator;
this.categories = categories;
this.requestedDeletion = requestedDeletion;
this.coordinates = coordinates;
}
public Media(final Uri localUri, final String filename,
final String description, final String creator, final List<String> categories) {
this(localUri,null, filename,
description, -1, null, new Date(), creator);
this.categories = categories;
}
public Media(final String title, final Date date, final String user) {
this(null, null, title, "", -1, date, date, user);
}
protected Media(final Parcel in) {
localUri = in.readParcelable(Uri.class.getClassLoader());
thumbUrl = in.readString();
imageUrl = in.readString();
filename = in.readString();
thumbnailTitle = in.readString();
caption = in.readString();
description = in.readString();
discussion = in.readString();
dataLength = in.readLong();
final long tmpDateCreated = in.readLong();
dateCreated = tmpDateCreated == -1 ? null : new Date(tmpDateCreated);
final long tmpDateUploaded = in.readLong();
dateUploaded = tmpDateUploaded == -1 ? null : new Date(tmpDateUploaded);
license = in.readString();
licenseUrl = in.readString();
creator = in.readString();
pageId = in.readString();
final ArrayList<String> list = new ArrayList<>();
in.readStringList(list);
categories = list;
in.readParcelable(Depictions.class.getClassLoader());
requestedDeletion = in.readByte() != 0;
coordinates = in.readParcelable(LatLng.class.getClassLoader());
}
/**
* 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
*/
@NonNull
public static Media from(final MwQueryPage page) {
final ImageInfo imageInfo = page.imageInfo();
if (imageInfo == null) {
return new Media(); // null is not allowed
}
final ExtMetadata metadata = imageInfo.getMetadata();
if (metadata == null) {
final Media media = new Media(null, imageInfo.getOriginalUrl(),
page.title(), "", 0, null, null, null);
if (!StringUtils.isBlank(imageInfo.getThumbUrl())) {
media.setThumbUrl(imageInfo.getThumbUrl());
}
return media;
}
final Media media = new Media(null,
imageInfo.getOriginalUrl(),
page.title(),
"",
0,
safeParseDate(metadata.dateTime()),
safeParseDate(metadata.dateTime()),
getArtist(metadata)
);
if (!StringUtils.isBlank(imageInfo.getThumbUrl())) {
media.setThumbUrl(imageInfo.getThumbUrl());
}
media.setPageId(String.valueOf(page.pageId()));
String language = Locale.getDefault().getLanguage();
if (StringUtils.isBlank(language)) {
language = "default";
}
media.setDescription(metadata.imageDescription());
media.setCategories(MediaDataExtractorUtil.extractCategoriesFromList(metadata.getCategories()));
final String latitude = metadata.getGpsLatitude();
final String longitude = metadata.getGpsLongitude();
if (!StringUtils.isBlank(latitude) && !StringUtils.isBlank(longitude)) {
final LatLng latLng = new LatLng(Double.parseDouble(latitude),
Double.parseDouble(longitude), 0);
media.setCoordinates(latLng);
}
media.setLicenseInformation(metadata.licenseShortName(), metadata.licenseUrl());
return media;
}
/**
* This method extracts the Commons Username from the artist HTML information
* @param metadata
* @return
*/
private static String getArtist(final ExtMetadata metadata) {
try {
final String artistHtml = metadata.artist();
return artistHtml.substring(artistHtml.indexOf("title=\""), artistHtml.indexOf("\">"))
.replace("title=\"User:", "");
} catch (final Exception ex) {
return "";
}
}
@Nullable
public String getThumbUrl() {
return thumbUrl;
@ -283,23 +167,6 @@ public class Media implements Parcelable {
return filename != null ? getPageTitle().getDisplayTextWithoutNamespace().replaceFirst("[.][^.]+$", "") : "";
}
@Nullable
private static Date safeParseDate(final String dateStr) {
try {
return CommonsDateUtil.getMediaSimpleDateFormat().parse(dateStr);
} catch (final ParseException e) {
return null;
}
}
/**
* @return title to be shown on image thumbnail
* If caption is available for the image then it returns caption else filename
*/
public String getThumbnailTitle() {
return thumbnailTitle != null? thumbnailTitle : getDisplayTitle();
}
/**
* Gets file page title
* @return New media page title
@ -308,13 +175,6 @@ public class Media implements Parcelable {
return Utils.getPageTitle(getFilename());
}
/**
* Gets local URI
* @return Media local URI
*/
public Uri getLocalUri() {
return localUri;
}
/**
* Gets image URL
@ -348,38 +208,12 @@ public class Media implements Parcelable {
this.pageId = pageId;
}
/**
* Gets the file discussion as a string.
* @return file discussion as a string
*/
public String getDiscussion() {
return discussion;
}
/**
* Gets the file description.
* @return file description as a string
*/
public String getDescription() {
return description;
}
/**
* Captions are a feature part of Structured data. They are meant to store short, multilingual descriptions about files
* This is a replacement of the previously used titles for images (titles were not multilingual)
* Also now captions replace the previous convention of using title for filename
*
* @return caption
*/
public String getCaption() {
return caption;
}
/**
* @return depictions associated with the current media
*/
public Depictions getDepiction() {
return depictions;
public String getFallbackDescription() {
return fallbackDescription;
}
/**
@ -390,36 +224,12 @@ public class Media implements Parcelable {
this.filename = filename;
}
/**
* Gets the dataLength of the file.
* @return file dataLength as a long
*/
public long getDataLength() {
return dataLength;
}
/**
* Sets the discussion of the file.
* @param discussion
*/
public void setDiscussion(final String discussion) {
this.discussion = discussion;
}
/**
* Gets the creation date of the file.
* @return creation date as a Date
*/
public Date getDateCreated() {
return dateCreated;
}
/**
* Sets the file description.
* @param description the new description of the file
* @param fallbackDescription the new description of the file
*/
public void setDescription(final String description) {
this.description = description;
public void setFallbackDescription(final String fallbackDescription) {
this.fallbackDescription = fallbackDescription;
}
/**
@ -440,14 +250,6 @@ public class Media implements Parcelable {
return creator;
}
/**
* Sets the dataLength of the file.
* @param dataLength as a long
*/
public void setDataLength(final long dataLength) {
this.dataLength = dataLength;
}
/**
* Gets the license name of the file.
* @return license as a String
@ -456,13 +258,6 @@ public class Media implements Parcelable {
return license;
}
/**
* Set Caption(if available) as the thumbnail title of the image
*/
public void setThumbnailTitle(final String title) {
thumbnailTitle = title;
}
public String getLicenseUrl() {
return licenseUrl;
}
@ -492,24 +287,10 @@ public class Media implements Parcelable {
* Gets the categories the file falls under.
* @return file categories as an ArrayList of Strings
*/
@SuppressWarnings("unchecked")
public List<String> getCategories() {
return categories;
}
/**
* Sets the license name of the file.
* @param license license name as a String
*/
public void setLicenseInformation(final String license, String licenseUrl) {
this.license = license;
if (!licenseUrl.startsWith("http://") && !licenseUrl.startsWith("https://")) {
licenseUrl = "https://" + licenseUrl;
}
this.licenseUrl = licenseUrl;
}
/**
* Sets the coordinates of where the file was created.
* @param coordinates file coordinates as a LatLng
@ -529,21 +310,6 @@ public class Media implements Parcelable {
this.categories = categories;
}
/**
* Get the value of requested deletion
* @return boolean requestedDeletion
*/
public boolean isRequestedDeletion(){
return requestedDeletion;
}
/**
* Set requested deletion to true
* @param requestedDeletion
*/
public void setRequestedDeletion(final boolean requestedDeletion) {
this.requestedDeletion = requestedDeletion;
}
/**
* Sets the license name of the file.
@ -554,22 +320,6 @@ public class Media implements Parcelable {
this.license = license;
}
/**
* Captions are a feature part of Structured data. They are meant to store short, multilingual descriptions about files
* This is a replacement of the previously used titles for images (titles were not multilingual)
* Also now captions replace the previous convention of using title for filename
*
* This function sets captions
* @param caption
*/
public void setCaption(final String caption) {
this.caption = caption;
}
public void setLocalUri(@Nullable final Uri localUri) {
this.localUri = localUri;
}
public void setImageUrl(final String imageUrl) {
this.imageUrl = imageUrl;
}
@ -582,28 +332,11 @@ public class Media implements Parcelable {
this.licenseUrl = licenseUrl;
}
public Depictions getDepictions() {
return depictions;
}
@Override
public int describeContents() {
return 0;
}
/* Sets depictions for the current media obtained fro Wikibase API*/
public void setDepictions(final Depictions depictions) {
this.depictions = depictions;
}
/**
* Sets the creation date of the file.
* @param date creation date as a Date
*/
public void setDateCreated(final Date date) {
dateCreated = date;
}
/**
* Creates a way to transfer information between two or more
* activities.
@ -612,69 +345,76 @@ public class Media implements Parcelable {
*/
@Override
public void writeToParcel(final Parcel dest, final int flags) {
dest.writeParcelable(localUri, flags);
dest.writeString(thumbUrl);
dest.writeString(imageUrl);
dest.writeString(filename);
dest.writeString(thumbnailTitle);
dest.writeString(caption);
dest.writeString(description);
dest.writeString(discussion);
dest.writeLong(dataLength);
dest.writeLong(dateCreated != null ? dateCreated.getTime() : -1);
dest.writeString(fallbackDescription);
dest.writeLong(dateUploaded != null ? dateUploaded.getTime() : -1);
dest.writeString(license);
dest.writeString(licenseUrl);
dest.writeString(creator);
dest.writeString(pageId);
dest.writeStringList(categories);
dest.writeParcelable(depictions, flags);
dest.writeByte(requestedDeletion ? (byte) 1 : (byte) 0);
dest.writeParcelable(coordinates, flags);
dest.writeSerializable((Serializable) captions);
dest.writeSerializable((Serializable) descriptions);
dest.writeList(depictionIds);
}
public Map<String, String> getCaptions() {
return captions;
}
public void setCaptions(Map<String, String> captions) {
this.captions = captions;
}
public Map<String, String> getDescriptions() {
return descriptions;
}
public void setDescriptions(Map<String, String> descriptions) {
this.descriptions = descriptions;
}
public List<String> getDepictionIds() {
return depictionIds;
}
public void setDepictionIds(final List<String> depictionIds) {
this.depictionIds = depictionIds;
}
/**
* Equals implementation that matches all parameters for equality check
*/
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Media)) {
if (o == null || getClass() != o.getClass()) {
return false;
}
final Media media = (Media) o;
return getDataLength() == media.getDataLength() &&
isRequestedDeletion() == media.isRequestedDeletion() &&
Objects.equals(getLocalUri(), media.getLocalUri()) &&
Objects.equals(getThumbUrl(), media.getThumbUrl()) &&
Objects.equals(getImageUrl(), media.getImageUrl()) &&
Objects.equals(getFilename(), media.getFilename()) &&
Objects.equals(getThumbnailTitle(), media.getThumbnailTitle()) &&
Objects.equals(getCaption(), media.getCaption()) &&
Objects.equals(getDescription(), media.getDescription()) &&
Objects.equals(getDiscussion(), media.getDiscussion()) &&
Objects.equals(getDateCreated(), media.getDateCreated()) &&
Objects.equals(getDateUploaded(), media.getDateUploaded()) &&
Objects.equals(getLicense(), media.getLicense()) &&
Objects.equals(getLicenseUrl(), media.getLicenseUrl()) &&
Objects.equals(getCreator(), media.getCreator()) &&
getPageId().equals(media.getPageId()) &&
Objects.equals(getCategories(), media.getCategories()) &&
Objects.equals(getDepictions(), media.getDepictions()) &&
Objects.equals(getCoordinates(), media.getCoordinates());
return Objects.equals(thumbUrl, media.thumbUrl) &&
Objects.equals(imageUrl, media.imageUrl) &&
Objects.equals(filename, media.filename) &&
Objects.equals(fallbackDescription, media.fallbackDescription) &&
Objects.equals(dateUploaded, media.dateUploaded) &&
Objects.equals(license, media.license) &&
Objects.equals(licenseUrl, media.licenseUrl) &&
Objects.equals(creator, media.creator) &&
pageId.equals(media.pageId) &&
Objects.equals(categories, media.categories) &&
Objects.equals(coordinates, media.coordinates) &&
captions.equals(media.captions) &&
descriptions.equals(media.descriptions) &&
depictionIds.equals(media.depictionIds);
}
/**
* Hashcode implementation that uses all parameters for calculating hash
*/
@Override
public int hashCode() {
return Objects
.hash(getLocalUri(), getThumbUrl(), getImageUrl(), getFilename(), getThumbnailTitle(),
getCaption(), getDescription(), getDiscussion(), getDataLength(), getDateCreated(),
getDateUploaded(), getLicense(), getLicenseUrl(), getCreator(), getPageId(),
getCategories(), getDepictions(), isRequestedDeletion(), getCoordinates());
.hash(thumbUrl, imageUrl, filename, fallbackDescription, dateUploaded, license,
licenseUrl,
creator, pageId, categories, coordinates, captions, descriptions, depictionIds);
}
}

View file

@ -1,101 +0,0 @@
package fr.free.nrw.commons;
import static fr.free.nrw.commons.depictions.Media.DepictedImagesFragment.PAGE_ID_PREFIX;
import androidx.core.text.HtmlCompat;
import fr.free.nrw.commons.media.Depictions;
import fr.free.nrw.commons.media.MediaClient;
import io.reactivex.Single;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.jetbrains.annotations.NotNull;
import timber.log.Timber;
/**
* Fetch additional media data from the network that we don't store locally.
*
* This includes things like category lists and multilingual descriptions,
* which are not intrinsic to the media and may change due to editing.
*/
@Singleton
public class MediaDataExtractor {
private final MediaClient mediaClient;
@Inject
public MediaDataExtractor(final MediaClient mediaClient) {
this.mediaClient = mediaClient;
}
/**
* Simplified method to extract all details required to show media details.
* It fetches media object, deletion status, talk page and captions for the filename
* @param filename for which the details are to be fetched
* @return full Media object with all details including deletion status and talk page
*/
public Single<Media> fetchMediaDetails(final String filename, final String pageId) {
return Single.zip(getMediaFromFileName(filename),
mediaClient.checkPageExistsUsingTitle("Commons:Deletion_requests/" + filename),
getDiscussion(filename),
pageId != null ? getCaption(PAGE_ID_PREFIX + pageId)
: Single.just(MediaClient.NO_CAPTION),
getDepictions(filename),
this::combineToMedia);
}
@NotNull
private Media combineToMedia(final Media media, final Boolean deletionStatus, final String discussion,
final String caption, final Depictions depictions) {
media.setDiscussion(discussion);
media.setCaption(caption);
media.setDepictions(depictions);
if (deletionStatus) {
media.setRequestedDeletion(true);
}
return media;
}
/**
* Obtains captions using filename
* @param wikibaseIdentifier
*
* @return caption for the image in user's locale
* Ex: "a nice painting" (english locale) and "No Caption" in case the caption is not available for the image
*/
private Single<String> getCaption(final String wikibaseIdentifier) {
return mediaClient.getCaptionByWikibaseIdentifier(wikibaseIdentifier);
}
/**
* Fetch depictions from the MediaWiki API
* @param filename the filename we will return the caption for
* @return Depictions
*/
private Single<Depictions> getDepictions(final String filename) {
return mediaClient.getDepictions(filename)
.doOnError(throwable -> Timber.e(throwable, "error while fetching depictions"));
}
/**
* Method can be used to fetch media for a given filename
* @param filename Eg. File:Test.jpg
* @return return data rich Media object
*/
public Single<Media> getMediaFromFileName(final String filename) {
return mediaClient.getMedia(filename);
}
/**
* Fetch talk page from the MediaWiki API
* @param filename
* @return
*/
private Single<String> getDiscussion(final String filename) {
return mediaClient.getPageHtml(filename.replace("File", "File talk"))
.map(discussion -> HtmlCompat.fromHtml(discussion, HtmlCompat.FROM_HTML_MODE_LEGACY).toString())
.onErrorReturn(throwable -> {
Timber.e(throwable, "Error occurred while fetching discussion");
return "";
});
}
}

View file

@ -0,0 +1,51 @@
package fr.free.nrw.commons
import androidx.core.text.HtmlCompat
import fr.free.nrw.commons.depictions.Media.DepictedImagesFragment.PAGE_ID_PREFIX
import fr.free.nrw.commons.media.IdAndCaptions
import fr.free.nrw.commons.media.MediaClient
import io.reactivex.Single
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
/**
* Fetch additional media data from the network that we don't store locally.
*
*
* This includes things like category lists and multilingual descriptions, which are not intrinsic
* to the media and may change due to editing.
*/
@Singleton
class MediaDataExtractor @Inject constructor(private val mediaClient: MediaClient) {
fun fetchDepictionIdsAndLabels(media: Media) =
mediaClient.getEntities(media.depictionIds)
.map {
it.entities()
.mapValues { entry -> entry.value.labels().mapValues { it.value.value() } }
}
.map { it.map { (key, value) -> IdAndCaptions(key, value) } }
.onErrorReturn { emptyList() }
fun checkDeletionRequestExists(media: Media) =
mediaClient.checkPageExistsUsingTitle("Commons:Deletion_requests/" + media.filename)
fun fetchDiscussion(media: Media) =
mediaClient.getPageHtml(media.filename.replace("File", "File talk"))
.map { HtmlCompat.fromHtml(it, HtmlCompat.FROM_HTML_MODE_LEGACY).toString() }
.onErrorReturn {
Timber.d("Error occurred while fetching discussion")
""
}
fun refresh(media: Media): Single<Media> {
return Single.ambArray(
mediaClient.getMediaById(PAGE_ID_PREFIX + media.pageId)
.onErrorResumeNext { Single.never() },
mediaClient.getMedia(media.filename)
.onErrorResumeNext { Single.never() }
)
}
}

View file

@ -1,13 +1,5 @@
package fr.free.nrw.commons.bookmarks.pictures;
import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.bookmarks.Bookmark;
import fr.free.nrw.commons.media.MediaClient;
@ -15,6 +7,10 @@ import io.reactivex.Observable;
import io.reactivex.ObservableSource;
import io.reactivex.Single;
import io.reactivex.functions.Function;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
@Singleton
public class BookmarkPicturesController {
@ -40,16 +36,13 @@ public class BookmarkPicturesController {
currentBookmarks = bookmarks;
return Observable.fromIterable(bookmarks)
.flatMap((Function<Bookmark, ObservableSource<Media>>) this::getMediaFromBookmark)
.filter(media -> media != null && !StringUtils.isBlank(media.getFilename()))
.toList();
}
private Observable<Media> getMediaFromBookmark(Bookmark bookmark) {
Media dummyMedia = new Media("");
return mediaClient.getMedia(bookmark.getMediaName())
.map(media -> media == null ? dummyMedia : media)
.onErrorReturn(throwable -> dummyMedia)
.toObservable();
.toObservable()
.onErrorResumeNext(Observable.empty());
}
/**

View file

@ -2,7 +2,6 @@ package fr.free.nrw.commons.category;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static fr.free.nrw.commons.depictions.Media.DepictedImagesFragment.PAGE_ID_PREFIX;
import android.annotation.SuppressLint;
import android.os.Bundle;
@ -253,37 +252,6 @@ public class CategoryImagesListFragment extends DaggerFragment {
progressBar.setVisibility(GONE);
isLoading = false;
statusTextView.setVisibility(GONE);
for (Media m : collection) {
final String pageId = m.getPageId();
if (pageId != null) {
replaceTitlesWithCaptions(PAGE_ID_PREFIX + pageId, mediaSize++);
}
}
}
/**
* fetch captions for the image using filename and replace title of on the image thumbnail(if captions are available)
* else show filename
*/
public void replaceTitlesWithCaptions(String wikibaseIdentifier, int i) {
compositeDisposable.add(mediaClient.getCaptionByWikibaseIdentifier(wikibaseIdentifier)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(subscriber -> {
handleLabelforImage(subscriber, i);
}));
}
/**
* If caption is available for the image, then modify grid adapter
* to show captions
*/
private void handleLabelforImage(String s, int position) {
if (!s.trim().equals(getString(R.string.detail_caption_empty))) {
gridAdapter.getItem(position).setThumbnailTitle(s);
gridAdapter.notifyDataSetChanged();
}
}
/**

View file

@ -88,12 +88,19 @@ public class GridViewAdapter extends ArrayAdapter {
SimpleDraweeView imageView = convertView.findViewById(R.id.categoryImageView);
TextView fileName = convertView.findViewById(R.id.categoryImageTitle);
TextView author = convertView.findViewById(R.id.categoryImageAuthor);
fileName.setText(item.getThumbnailTitle());
fileName.setText(getTitle(item));
setAuthorView(item, author);
imageView.setImageURI(item.getThumbUrl());
return convertView;
}
private String getTitle(Media item) {
for (String caption : item.getCaptions().values()) {
return caption;
}
return item.getDisplayTitle();
}
/**
* @return the Media item at the given position
*/

View file

@ -1,17 +1,18 @@
package fr.free.nrw.commons.contributions;
import android.net.Uri;
import android.os.Parcel;
import androidx.annotation.Nullable;
import androidx.room.Entity;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.upload.UploadMediaDetail;
import fr.free.nrw.commons.upload.UploadItem;
import fr.free.nrw.commons.upload.UploadMediaDetail;
import fr.free.nrw.commons.upload.WikidataPlace;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@Entity(tableName = "contribution")
@ -34,24 +35,23 @@ public class Contribution extends Media {
*/
private List<DepictedItem> depictedItems = new ArrayList<>();
private String mimeType;
/**
* This hasmap stores the list of multilingual captions, where key of the HashMap is the language
* and value is the caption in the corresponding language Ex: key = "en", value: "<caption in
* short in English>" key = "de" , value: "<caption in german>"
*/
private Map<String, String> captions = new HashMap<>();
@Nullable
private Uri localUri;
private long dataLength;
private Date dateCreated;
public Contribution() {
}
public Contribution(final UploadItem item, final SessionManager sessionManager,
final List<DepictedItem> depictedItems, final List<String> categories) {
super(item.getMediaUri(),
super(
item.getFileName(),
UploadMediaDetail.formatList(item.getUploadMediaDetails()),
UploadMediaDetail.formatCaptions(item.getUploadMediaDetails()),
UploadMediaDetail.formatDescriptions(item.getUploadMediaDetails()),
sessionManager.getAuthorName(),
categories);
captions = UploadMediaDetail.formatCaptions(item.getUploadMediaDetails());
localUri = item.getMediaUri();
decimalCoords = item.getGpsCoords().getDecimalCoords();
dateCreatedSource = "";
this.depictedItems = depictedItems;
@ -117,24 +117,6 @@ public class Contribution extends Media {
this.mimeType = mimeType;
}
/**
* Captions are a feature part of Structured data. They are meant to store short, multilingual
* descriptions about files This is a replacement of the previously used titles for images (titles
* were not multilingual) Also now captions replace the previous convention of using title for
* filename
* <p>
* key of the HashMap is the language and value is the caption in the corresponding language
* <p>
* returns list of captions stored in hashmap
*/
public Map<String, String> getCaptions() {
return captions;
}
public void setCaptions(Map<String, String> captions) {
this.captions = captions;
}
@Override
public int describeContents() {
return 0;
@ -147,7 +129,6 @@ public class Contribution extends Media {
dest.writeLong(transferred);
dest.writeString(decimalCoords);
dest.writeString(dateCreatedSource);
dest.writeSerializable((HashMap) captions);
}
/**
@ -156,13 +137,7 @@ public class Contribution extends Media {
* @param state
*/
public Contribution(Media media, int state) {
super(media.getPageId(),
media.getLocalUri(), media.getThumbUrl(), media.getImageUrl(), media.getFilename(),
media.getDescription(),
media.getDiscussion(),
media.getDataLength(), media.getDateCreated(), media.getDateUploaded(),
media.getLicense(), media.getLicenseUrl(), media.getCreator(), media.getCategories(),
media.isRequestedDeletion(), media.getCoordinates());
super(media);
this.state = state;
}
@ -172,7 +147,6 @@ public class Contribution extends Media {
transferred = in.readLong();
decimalCoords = in.readString();
dateCreatedSource = in.readString();
captions = (HashMap<String, String>) in.readSerializable();
}
public static final Creator<Contribution> CREATOR = new Creator<Contribution>() {
@ -187,34 +161,60 @@ public class Contribution extends Media {
}
};
/**
* Equals implementation of Contributions that compares all parameters for checking equality
*/
@Nullable
public Uri getLocalUri() {
return localUri;
}
public void setLocalUri(@Nullable Uri localUri) {
this.localUri = localUri;
}
public long getDataLength() {
return dataLength;
}
public void setDataLength(long dataLength) {
this.dataLength = dataLength;
}
public Date getDateCreated() {
return dateCreated;
}
public void setDateCreated(Date dateCreated) {
this.dateCreated = dateCreated;
}
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Contribution)) {
if (o == null || getClass() != o.getClass()) {
return false;
}
if (!super.equals(o)) {
return false;
}
final Contribution that = (Contribution) o;
return getState() == that.getState() && getTransferred() == that.getTransferred() && Objects
.equals(getDecimalCoords(), that.getDecimalCoords()) && Objects
.equals(getDateCreatedSource(), that.getDateCreatedSource()) && Objects
.equals(getWikidataPlace(), that.getWikidataPlace()) && Objects
.equals(getDepictedItems(), that.getDepictedItems()) && Objects
.equals(getMimeType(), that.getMimeType()) && Objects
.equals(getCaptions(), that.getCaptions());
return state == that.state &&
transferred == that.transferred &&
dataLength == that.dataLength &&
Objects.equals(decimalCoords, that.decimalCoords) &&
Objects.equals(dateCreatedSource, that.dateCreatedSource) &&
Objects.equals(wikidataPlace, that.wikidataPlace) &&
Objects.equals(depictedItems, that.depictedItems) &&
Objects.equals(mimeType, that.mimeType) &&
Objects.equals(localUri, that.localUri) &&
Objects.equals(dateCreated, that.dateCreated);
}
/**
* Hash code implementation of contributions that considers all parameters for calculating hash.
*/
@Override
public int hashCode() {
return Objects
.hash(getState(), getTransferred(), getDecimalCoords(), getDateCreatedSource(),
getWikidataPlace(), getDepictedItems(), getMimeType(), getCaptions());
.hash(super.hashCode(), state, transferred, decimalCoords, dateCreatedSource,
wikidataPlace,
depictedItems, mimeType, localUri, dataLength, dateCreated);
}
}

View file

@ -8,7 +8,6 @@ import fr.free.nrw.commons.media.MediaClient
import io.reactivex.Scheduler
import io.reactivex.disposables.CompositeDisposable
import timber.log.Timber
import java.util.*
import javax.inject.Inject
import javax.inject.Named
@ -52,20 +51,18 @@ class ContributionBoundaryCallback @Inject constructor(
* Fetches contributions using the MediaWiki API
*/
fun fetchContributions() {
if (mediaClient.doesMediaListForUserHaveMorePages(sessionManager.userName).not()) {
if (mediaClient.doesMediaListForUserHaveMorePages(sessionManager.userName!!).not()) {
return
}
compositeDisposable.add(
mediaClient.getMediaListForUser(sessionManager.userName)
mediaClient.getMediaListForUser(sessionManager.userName!!)
.map { mediaList: List<Media?> ->
mediaList.map {
Contribution(it, Contribution.STATE_COMPLETED)
}
}
.subscribeOn(ioThreadScheduler)
.subscribe(
::saveContributionsToDB
) { error: Throwable ->
.subscribe(::saveContributionsToDB) { error: Throwable ->
Timber.e(
"Failed to fetch contributions: %s",
error.message

View file

@ -1,7 +1,5 @@
package fr.free.nrw.commons.contributions;
import static fr.free.nrw.commons.depictions.Media.DepictedImagesFragment.PAGE_ID_PREFIX;
import android.net.Uri;
import android.text.TextUtils;
import android.view.View;
@ -19,10 +17,7 @@ import com.facebook.imagepipeline.request.ImageRequestBuilder;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.contributions.ContributionsListAdapter.Callback;
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;
public class ContributionViewHolder extends RecyclerView.ViewHolder {
@ -109,25 +104,15 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
if ((contribution.getState() != Contribution.STATE_COMPLETED)) {
titleView.setText(contribution.getDisplayTitle());
} else {
final String pageId = contribution.getPageId();
if (pageId != null) {
Timber.d("Fetching caption for %s", contribution.getFilename());
final String wikibaseMediaId = PAGE_ID_PREFIX
+ pageId; // Create Wikibase media id from the page id. Example media id: M80618155 for https://commons.wikimedia.org/wiki/File:Tantanmen.jpeg with has the pageid 80618155
compositeDisposable.add(mediaClient.getCaptionByWikibaseIdentifier(wikibaseMediaId)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(subscriber -> {
if (!subscriber.trim().equals(MediaClient.NO_CAPTION)) {
titleView.setText(subscriber);
} else {
titleView.setText(contribution.getDisplayTitle());
}
}));
} else {
titleView.setText(contribution.getDisplayTitle());
titleView.setText(getTitle(contribution));
}
}
private String getTitle(Contribution contribution) {
for (String value : contribution.getCaptions().values()) {
return value;
}
return contribution.getDisplayTitle();
}
/**

View file

@ -1,9 +1,6 @@
package fr.free.nrw.commons.contributions;
import java.util.List;
import fr.free.nrw.commons.BasePresenter;
import fr.free.nrw.commons.Media;
/**
* The contract for Contributions View & Presenter
@ -21,8 +18,5 @@ public class ContributionsContract {
void deleteUpload(Contribution contribution);
void updateContribution(Contribution contribution);
void fetchMediaDetails(Contribution contribution);
}
}

View file

@ -45,9 +45,8 @@ public class ContributionsListAdapter extends
* Initializes the view holder with contribution data
*/
@Override
public void onBindViewHolder(@NonNull final ContributionViewHolder holder, final int position) {
final Contribution contribution = getItem(position);
holder.init(position, contribution);
public void onBindViewHolder(@NonNull ContributionViewHolder holder, int position) {
holder.init(position, getItem(position));
}
Contribution getContributionForPosition(final int position) {

View file

@ -1,30 +1,10 @@
package fr.free.nrw.commons.contributions;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.MediaDataExtractor;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.MediaDataExtractor;
import fr.free.nrw.commons.contributions.ContributionsContract.UserActionListener;
import fr.free.nrw.commons.di.CommonsApplicationModule;
import fr.free.nrw.commons.mwapi.UserClient;
import fr.free.nrw.commons.utils.NetworkUtils;
import io.reactivex.Scheduler;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import timber.log.Timber;
import javax.inject.Inject;
import javax.inject.Named;
@ -77,24 +57,4 @@ public class ContributionsPresenter implements UserActionListener {
.subscribeOn(ioThreadScheduler)
.subscribe());
}
@Override
public void updateContribution(Contribution contribution) {
compositeDisposable.add(repository
.updateContribution(contribution)
.subscribeOn(ioThreadScheduler)
.subscribe());
}
@Override
public void fetchMediaDetails(Contribution contribution) {
compositeDisposable.add(mediaDataExtractor
.getMediaFromFileName(contribution.getFilename())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(media -> {
contribution.setThumbUrl(media.getThumbUrl());
updateContribution(contribution);
}));
}
}

View file

@ -10,7 +10,7 @@ import fr.free.nrw.commons.contributions.ContributionDao
* The database for accessing the respective DAOs
*
*/
@Database(entities = [Contribution::class], version = 2, exportSchema = false)
@Database(entities = [Contribution::class], version = 3, exportSchema = false)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun contributionDao(): ContributionDao

View file

@ -7,7 +7,6 @@ import com.google.gson.reflect.TypeToken;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.media.Depictions;
import fr.free.nrw.commons.upload.WikidataPlace;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import java.util.Date;
@ -93,16 +92,6 @@ public class Converters {
return readObjectWithTypeToken(depictedItems, new TypeToken<List<DepictedItem>>() {});
}
@TypeConverter
public static String depictionsToString(Depictions depictedItems) {
return writeObjectToString(depictedItems);
}
@TypeConverter
public static Depictions stringToDepictions(String depictedItems) {
return readObjectFromString(depictedItems, Depictions.class);
}
private static String writeObjectToString(Object object) {
return object == null ? null : getGson().toJson(object);
}

View file

@ -88,7 +88,7 @@ public class GridViewAdapter extends ArrayAdapter {
SimpleDraweeView imageView = convertView.findViewById(R.id.depict_image_view);
TextView fileName = convertView.findViewById(R.id.depict_image_title);
TextView author = convertView.findViewById(R.id.depict_image_author);
fileName.setText(item.getThumbnailTitle());
fileName.setText(item.getDisplayTitle());
setAuthorView(item, author);
imageView.setImageURI(item.getThumbUrl());
return convertView;

View file

@ -31,10 +31,6 @@ public interface DepictedImagesContract {
*/
void setAdapter(List<Media> mediaList);
/**
* Seat caption to the image at the given position
*/
void handleLabelforImage(String caption, int position);
/**
* Display snackbar
@ -94,12 +90,6 @@ public interface DepictedImagesContract {
*/
void fetchMoreImages(String entityId);
/**
* fetch captions for the image using filename and replace title of on the image thumbnail(if captions are available)
* else show filename
*/
void replaceTitlesWithCaptions(String title, int position);
/**
* add items to query list
*/

View file

@ -50,7 +50,6 @@ public class DepictedImagesFragment extends DaggerFragment implements DepictedIm
private String entityId = null;
private boolean isLastPage;
private boolean isLoading = true;
private int mediaSize = 0;
@Nullable
@Override
@ -146,17 +145,6 @@ public class DepictedImagesFragment extends DaggerFragment implements DepictedIm
});
}
/**
* Seat caption to the image at the given position
*/
@Override
public void handleLabelforImage(String caption, int position) {
if (!caption.trim().equals(getString(R.string.detail_caption_empty))) {
gridAdapter.getItem(position).setThumbnailTitle(caption);
gridAdapter.notifyDataSetChanged();
}
}
/**
* Display snackbar
*/
@ -257,11 +245,5 @@ public class DepictedImagesFragment extends DaggerFragment implements DepictedIm
progressBar.setVisibility(GONE);
isLoading = false;
statusTextView.setVisibility(GONE);
for (Media media : collection) {
final String pageId = media.getPageId();
if (pageId != null) {
presenter.replaceTitlesWithCaptions(PAGE_ID_PREFIX + pageId, mediaSize++);
}
}
}
}

View file

@ -136,20 +136,6 @@ public class DepictedImagesPresenter implements DepictedImagesContract.UserActio
}
}
/**
* fetch captions for the image using filename and replace title of on the image thumbnail(if captions are available)
* else show filename
*/
@Override
public void replaceTitlesWithCaptions(String wikibaseIdentifier, int position) {
compositeDisposable.add(mediaClient.getCaptionByWikibaseIdentifier(wikibaseIdentifier)
.subscribeOn(ioScheduler)
.observeOn(mainThreadScheduler)
.subscribe(caption -> {
view.handleLabelforImage(caption, position);
}));
}
/**
* add items to query list

View file

@ -9,18 +9,12 @@ import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.FrameLayout;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.viewpager.widget.ViewPager;
import com.google.android.material.tabs.TabLayout;
import java.util.ArrayList;
import java.util.List;
import butterknife.BindView;
import butterknife.ButterKnife;
import com.google.android.material.tabs.TabLayout;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.category.CategoryImagesCallback;
@ -29,6 +23,8 @@ import fr.free.nrw.commons.explore.SearchActivity;
import fr.free.nrw.commons.explore.ViewPagerAdapter;
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.theme.NavigationBaseActivity;
import java.util.ArrayList;
import java.util.List;
/**
* This activity displays featured images and images uploaded via mobile

View file

@ -46,7 +46,7 @@ class DepictsClient @Inject constructor(
/**
* @return list of images for a particular depict entity
*/
fun fetchImagesForDepictedItem(query: String, sroffset: Int): Observable<List<Media>> {
fun fetchImagesForDepictedItem(query: String, sroffset: Int): Single<List<Media>> {
return mediaInterface.fetchImagesForDepictedItem(
"haswbstatement:" + BuildConfig.DEPICTS_PROPERTY + "=" + query,
sroffset.toString()
@ -56,12 +56,9 @@ class DepictsClient @Inject constructor(
.search
.map {
Media(
null,
getUrl(it.title),
it.title,
"",
0,
safeParseDate(it.timestamp),
safeParseDate(it.timestamp),
""
)

View file

@ -1,9 +1,93 @@
package fr.free.nrw.commons.explore.media
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.location.LatLng
import fr.free.nrw.commons.upload.structure.depictions.get
import fr.free.nrw.commons.utils.CommonsDateUtil
import fr.free.nrw.commons.utils.MediaDataExtractorUtil
import fr.free.nrw.commons.wikidata.WikidataProperties
import org.apache.commons.lang3.StringUtils
import org.wikipedia.dataclient.mwapi.MwQueryPage
import org.wikipedia.gallery.ExtMetadata
import org.wikipedia.wikidata.DataValue
import org.wikipedia.wikidata.Entities
import java.text.ParseException
import java.util.*
import javax.inject.Inject
class MediaConverter @Inject constructor() {
fun convert(mwQueryPage: MwQueryPage): Media = Media.from(mwQueryPage)
fun convert(page: MwQueryPage, entity: Entities.Entity): Media {
val imageInfo = page.imageInfo()
requireNotNull(imageInfo) { "No image info" }
val metadata = imageInfo.metadata
requireNotNull(metadata) { "No metadata" }
return Media(
imageInfo.thumbUrl.takeIf { it.isNotBlank() } ?: imageInfo.originalUrl,
imageInfo.originalUrl,
page.title(),
metadata.imageDescription(),
safeParseDate(metadata.dateTime()),
metadata.licenseShortName(),
metadata.prefixedLicenseUrl,
getArtist(metadata),
page.pageId().toString(),
MediaDataExtractorUtil.extractCategoriesFromList(metadata.categories),
metadata.latLng,
entity.labels().mapValues { it.value.value() },
entity.descriptions().mapValues { it.value.value() },
entity.depictionIds()
)
}
/**
* 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? {
return try {
CommonsDateUtil.getMediaSimpleDateFormat().parse(dateStr)
} catch (e: ParseException) {
null
}
}
/**
* This method extracts the Commons Username from the artist HTML information
* @param metadata
* @return
*/
private fun getArtist(metadata: ExtMetadata): String? {
return try {
val artistHtml = metadata.artist()
artistHtml.substring(artistHtml.indexOf("title=\""), artistHtml.indexOf("\">"))
.replace("title=\"User:", "")
} catch (ex: java.lang.Exception) {
""
}
}
}
private fun Entities.Entity.depictionIds() =
this[WikidataProperties.DEPICTS]?.mapNotNull { (it.mainSnak.dataValue as? DataValue.EntityId)?.value?.id }
?: emptyList()
private val ExtMetadata.prefixedLicenseUrl: String
get() = licenseUrl().let {
if (!it.startsWith("http://") && !it.startsWith("https://"))
"https://$it"
else
it
}
private val ExtMetadata.latLng: LatLng?
get() = if (!StringUtils.isBlank(gpsLatitude) && !StringUtils.isBlank(gpsLongitude))
LatLng(gpsLatitude.toDouble(), gpsLongitude.toDouble(), 0.0f)
else
null

View file

@ -1,34 +1,17 @@
package fr.free.nrw.commons.explore.media
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.depictions.Media.DepictedImagesFragment.PAGE_ID_PREFIX
import fr.free.nrw.commons.explore.LiveDataConverter
import fr.free.nrw.commons.explore.PageableDataSource
import fr.free.nrw.commons.explore.depictions.LoadFunction
import fr.free.nrw.commons.media.MediaClient
import fr.free.nrw.commons.media.MediaClient.NO_CAPTION
import javax.inject.Inject
class PageableMediaDataSource @Inject constructor(
liveDataConverter: LiveDataConverter,
private val mediaConverter: MediaConverter,
private val mediaClient: MediaClient
) : PageableDataSource<Media>(liveDataConverter) {
override val loadFunction: LoadFunction<Media> = { loadSize: Int, startPosition: Int ->
mediaClient.getMediaListFromSearch(query, loadSize, startPosition)
.map { it.query()?.pages()?.map(mediaConverter::convert) ?: emptyList() }
.map { it.zip(getCaptions(it)) }
.map { it.map { (media, caption) -> media.also { it.caption = caption } } }
.blockingGet()
mediaClient.getMediaListFromSearch(query, loadSize, startPosition).blockingGet()
}
private fun getCaptions(it: List<Media>) =
mediaClient.getEntities(it.joinToString("|") { PAGE_ID_PREFIX + it.pageId })
.map {
it.entities().values.map { entity ->
entity.labels().values.firstOrNull()?.value() ?: NO_CAPTION
}
}
.blockingGet()
}

View file

@ -35,7 +35,7 @@ class SearchImagesViewHolder(containerView: View, val onImageClicked: (Int) -> U
override fun bind(item: Pair<Media, Int>) {
val media = item.first
categoryImageView.setOnClickListener { onImageClicked(item.second) }
categoryImageTitle.text = media.thumbnailTitle
categoryImageTitle.text = media.displayTitle
categoryImageView.setImageURI(media.thumbUrl)
if (media.creator?.isNotEmpty() == true) {
categoryImageAuthor.visibility = View.VISIBLE

View file

@ -1,30 +0,0 @@
package fr.free.nrw.commons.media
import android.os.Parcelable
import androidx.annotation.WorkerThread
import fr.free.nrw.commons.wikidata.WikidataProperties.DEPICTS
import kotlinx.android.parcel.Parcelize
import org.wikipedia.wikidata.DataValue.EntityId
import org.wikipedia.wikidata.Entities
import java.util.*
@Parcelize
data class Depictions(val depictions: List<IdAndLabel>) : Parcelable {
companion object {
@JvmStatic
@WorkerThread
fun from(entities: Entities, mediaClient: MediaClient) =
Depictions(
entities.first?.statements
?.getOrElse(DEPICTS.propertyName, { emptyList() })
?.map { statement ->
(statement.mainSnak.dataValue as EntityId).value.id
}
?.map { id -> IdAndLabel(id, fetchLabel(mediaClient, id)) }
?: emptyList()
)
private fun fetchLabel(mediaClient: MediaClient, id: String) =
mediaClient.getLabelForDepiction(id, Locale.getDefault().language).blockingGet()
}
}

View file

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

View file

@ -1,14 +0,0 @@
package fr.free.nrw.commons.media
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
import org.wikipedia.wikidata.Entities
@Parcelize
data class IdAndLabel(val entityId: String, val entityLabel: String) : Parcelable {
constructor(entityId: String, entities: MutableMap<String, Entities.Entity>) : this(
entityId,
entities.values.first().labels().values.first().value()
)
}

View file

@ -1,266 +0,0 @@
package fr.free.nrw.commons.media;
import androidx.annotation.NonNull;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.utils.CommonsDateUtil;
import io.reactivex.Observable;
import io.reactivex.Single;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
import org.wikipedia.wikidata.Entities;
import org.wikipedia.wikidata.Entities.Entity;
import org.wikipedia.wikidata.Entities.Label;
import timber.log.Timber;
/**
* Media Client to handle custom calls to Commons MediaWiki APIs
*/
@Singleton
public class MediaClient {
private final MediaInterface mediaInterface;
private final MediaDetailInterface mediaDetailInterface;
//OkHttpJsonApiClient used JsonKvStore for this. I don't know why.
private Map<String, Map<String, String>> continuationStore;
private Map<String, Boolean> continuationExists;
public static final String NO_CAPTION = "No caption";
private static final String NO_DEPICTION = "No depiction";
@Inject
public MediaClient(MediaInterface mediaInterface, MediaDetailInterface mediaDetailInterface) {
this.mediaInterface = mediaInterface;
this.mediaDetailInterface = mediaDetailInterface;
this.continuationStore = new HashMap<>();
this.continuationExists = new HashMap<>();
}
/**
* Checks if a page exists on Commons
* The same method can be used to check for file or talk page
*
* @param title File:Test.jpg or Commons:Deletion_requests/File:Test1.jpeg
*/
public Single<Boolean> checkPageExistsUsingTitle(String title) {
return mediaInterface.checkPageExistsUsingTitle(title)
.map(mwQueryResponse -> mwQueryResponse
.query().firstPage().pageId() > 0)
.singleOrError();
}
/**
* Take the fileSha and returns whether a file with a matching SHA exists or not
*
* @param fileSha SHA of the file to be checked
*/
public Single<Boolean> checkFileExistsUsingSha(String fileSha) {
return mediaInterface.checkFileExistsUsingSha(fileSha)
.map(mwQueryResponse -> mwQueryResponse
.query().allImages().size() > 0)
.singleOrError();
}
/**
* This method takes the category as input and returns a list of Media objects filtered using image generator query
* It uses the generator query API to get the images searched using a query, 10 at a time.
*
* @param category the search category. Must start with "Category:"
* @return
*/
public Single<List<Media>> getMediaListFromCategory(String category) {
return responseToMediaList(
continuationStore.containsKey("category_" + category) ?
mediaInterface.getMediaListFromCategory(category, 10, continuationStore.get("category_" + category)) : //if true
mediaInterface.getMediaListFromCategory(category, 10, Collections.emptyMap()),
"category_" + category); //if false
}
/**
* This method takes the userName as input and returns a list of Media objects filtered using
* allimages query It uses the allimages query API to get the images contributed by the userName,
* 10 at a time.
*
* @param userName the username
* @return
*/
public Single<List<Media>> getMediaListForUser(String userName) {
Map<String, String> continuation =
continuationStore.containsKey("user_" + userName)
? continuationStore.get("user_" + userName)
: Collections.emptyMap();
return responseToMediaList(mediaInterface
.getMediaListForUser(userName, 10, continuation), "user_" + userName);
}
/**
* Check if media for user has reached the end of the list.
* @param userName
* @return
*/
public boolean doesMediaListForUserHaveMorePages(String userName) {
final String key = "user_" + userName;
if(continuationExists.containsKey(key)) {
return continuationExists.get(key);
}
return true;
}
/**
* This method takes a keyword as input and returns a list of Media objects filtered using image generator query
* It uses the generator query API to get the images searched using a query, 10 at a time.
*
* @param keyword the search keyword
* @param limit
* @param offset
* @return
*/
public Single<MwQueryResponse> getMediaListFromSearch(String keyword, int limit, int offset) {
return mediaInterface.getMediaListFromSearch(keyword, limit, offset);
}
private Single<List<Media>> responseToMediaList(Observable<MwQueryResponse> response, String key) {
return response.flatMap(mwQueryResponse -> {
if (null == mwQueryResponse
|| null == mwQueryResponse.query()
|| null == mwQueryResponse.query().pages()) {
return Observable.empty();
}
if(mwQueryResponse.continuation() != null) {
continuationStore.put(key, mwQueryResponse.continuation());
continuationExists.put(key, true);
} else {
continuationExists.put(key, false);
}
return Observable.fromIterable(mwQueryResponse.query().pages());
})
.map(Media::from)
.collect(ArrayList<Media>::new, List::add);
}
/**
* Fetches Media object from the imageInfo API
*
* @param titles the tiles to be searched for. Can be filename or template name
* @return
*/
public Single<Media> getMedia(String titles) {
return mediaInterface.getMedia(titles)
.flatMap(mwQueryResponse -> {
if (null == mwQueryResponse
|| null == mwQueryResponse.query()
|| null == mwQueryResponse.query().firstPage()) {
return Observable.empty();
}
return Observable.just(mwQueryResponse.query().firstPage());
})
.map(Media::from)
.single(Media.EMPTY);
}
/**
* The method returns the picture of the day
*
* @return Media object corresponding to the picture of the day
*/
@NonNull
public Single<Media> getPictureOfTheDay() {
String date = CommonsDateUtil.getIso8601DateFormatShort().format(new Date());
Timber.d("Current date is %s", date);
String template = "Template:Potd/" + date;
return mediaInterface.getMediaWithGenerator(template)
.flatMap(mwQueryResponse -> {
if (null == mwQueryResponse
|| null == mwQueryResponse.query()
|| null == mwQueryResponse.query().firstPage()) {
return Observable.empty();
}
return Observable.just(mwQueryResponse.query().firstPage());
})
.map(Media::from)
.single(Media.EMPTY);
}
@NonNull
public Single<String> getPageHtml(String title){
return mediaInterface.getPageHtml(title)
.filter(MwParseResponse::success)
.map(MwParseResponse::parse)
.map(MwParseResult::text)
.first("");
}
/**
* @return caption for image using wikibaseIdentifier
*/
public Single<String> getCaptionByWikibaseIdentifier(String wikibaseIdentifier) {
return mediaDetailInterface.getEntityForImage(Locale.getDefault().getLanguage(), wikibaseIdentifier)
.map(mediaDetailResponse -> {
if (isSuccess(mediaDetailResponse)) {
for (Entity wikibaseItem : mediaDetailResponse.entities().values()) {
for (Label label : wikibaseItem.labels().values()) {
return label.value();
}
}
}
return NO_CAPTION;
})
.singleOrError();
}
private boolean isSuccess(Entities response) {
return response != null && response.getSuccess() == 1 && response.entities() != null;
}
/**
* Fetches Structured data from API
*
* @param filename
* @return a map containing caption and depictions (empty string in the map if no caption/depictions)
*/
public Single<Depictions> getDepictions(String filename) {
return mediaDetailInterface.fetchEntitiesByFileName(Locale.getDefault().getLanguage(), filename)
.map(entities -> Depictions.from(entities, this))
.singleOrError();
}
/**
* Gets labels for Depictions using Entity Id from MediaWikiAPI
*
* @param entityId EntityId (Ex: Q81566) of the depict entity
* @return label
*/
public Single<String> getLabelForDepiction(String entityId, String language) {
return mediaDetailInterface.getEntity(entityId)
.map(entities -> {
if (isSuccess(entities)) {
for (Entity entity : entities.entities().values()) {
final Map<String, Label> languageToLabelMap = entity.labels();
if (languageToLabelMap.containsKey(language)) {
return languageToLabelMap.get(language).value();
}
for (Label label : languageToLabelMap.values()) {
return label.value();
}
}
}
throw new RuntimeException("failed getEntities");
});
}
public Single<Entities> getEntities(String entityId) {
return mediaDetailInterface.getEntity(entityId);
}
}

View file

@ -0,0 +1,172 @@
package fr.free.nrw.commons.media
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.depictions.Media.DepictedImagesFragment.PAGE_ID_PREFIX
import fr.free.nrw.commons.explore.media.MediaConverter
import fr.free.nrw.commons.utils.CommonsDateUtil
import io.reactivex.Single
import org.wikipedia.dataclient.mwapi.MwQueryPage
import org.wikipedia.dataclient.mwapi.MwQueryResponse
import org.wikipedia.wikidata.Entities
import java.util.*
import javax.inject.Inject
import javax.inject.Singleton
/**
* Media Client to handle custom calls to Commons MediaWiki APIs
*/
@Singleton
class MediaClient @Inject constructor(
private val mediaInterface: MediaInterface,
private val mediaDetailInterface: MediaDetailInterface,
private val mediaConverter: MediaConverter
) {
fun getMediaById(id: String) =
responseToMediaList(mediaInterface.getMediaById(id)).map { it.first() }
//OkHttpJsonApiClient used JsonKvStore for this. I don't know why.
private val continuationStore: MutableMap<String, Map<String, String>?> = mutableMapOf()
private val continuationExists: MutableMap<String, Boolean> = mutableMapOf()
/**
* Checks if a page exists on Commons
* The same method can be used to check for file or talk page
*
* @param title File:Test.jpg or Commons:Deletion_requests/File:Test1.jpeg
*/
fun checkPageExistsUsingTitle(title: String?): Single<Boolean> {
return mediaInterface.checkPageExistsUsingTitle(title)
.map { it.query()!!.firstPage()!!.pageId() > 0 }
}
/**
* Take the fileSha and returns whether a file with a matching SHA exists or not
*
* @param fileSha SHA of the file to be checked
*/
fun checkFileExistsUsingSha(fileSha: String?): Single<Boolean> {
return mediaInterface.checkFileExistsUsingSha(fileSha)
.map { it.query()!!.allImages().size > 0 }
}
/**
* This method takes the category as input and returns a list of Media objects filtered using image generator query
* It uses the generator query API to get the images searched using a query, 10 at a time.
*
* @param category the search category. Must start with "Category:"
* @return
*/
fun getMediaListFromCategory(category: String): Single<List<Media>> {
return responseToMediaList(
mediaInterface.getMediaListFromCategory(
category,
10,
continuationStore["category_$category"] ?: emptyMap()
),
"category_$category"
)
}
/**
* This method takes the userName as input and returns a list of Media objects filtered using
* allimages query It uses the allimages query API to get the images contributed by the userName,
* 10 at a time.
*
* @param userName the username
* @return
*/
fun getMediaListForUser(userName: String): Single<List<Media>> {
return responseToMediaList(
mediaInterface.getMediaListForUser(
userName,
10,
continuationStore["user_$userName"] ?: Collections.emptyMap()
),
"user_$userName"
)
}
/**
* This method takes a keyword as input and returns a list of Media objects filtered using image generator query
* It uses the generator query API to get the images searched using a query, 10 at a time.
*
* @param keyword the search keyword
* @param limit
* @param offset
* @return
*/
fun getMediaListFromSearch(keyword: String?, limit: Int, offset: Int) =
responseToMediaList(mediaInterface.getMediaListFromSearch(keyword, limit, offset))
private fun responseToMediaList(
response: Single<MwQueryResponse>,
key: String? = null
): Single<List<Media>> {
return response.map {
if (key != null) {
continuationExists[key] =
it.continuation()?.let { continuation ->
continuationStore[key] = continuation
true
} ?: false
}
it.query()?.pages() ?: emptyList()
}.flatMap(::mediaFromPageAndEntity)
}
private fun mediaFromPageAndEntity(pages: List<MwQueryPage>): Single<List<Media>> {
return getEntities(pages.map { "$PAGE_ID_PREFIX${it.pageId()}" })
.map {
pages.zip(it.entities().values)
.map { (page, entity) -> mediaConverter.convert(page, entity) }
}
}
/**
* Fetches Media object from the imageInfo API
*
* @param titles the tiles to be searched for. Can be filename or template name
* @return
*/
fun getMedia(titles: String?): Single<Media> {
return responseToMediaList(mediaInterface.getMedia(titles))
.map { it.first() }
}
/**
* The method returns the picture of the day
*
* @return Media object corresponding to the picture of the day
*/
fun getPictureOfTheDay(): Single<Media> {
val date = CommonsDateUtil.getIso8601DateFormatShort().format(Date())
return responseToMediaList(mediaInterface.getMediaWithGenerator("Template:Potd/$date")).map { it.first() }
}
fun getPageHtml(title: String?): Single<String> {
return mediaInterface.getPageHtml(title)
.map { obj: MwParseResponse -> obj.parse()?.text() ?: "" }
}
fun getEntities(entityIds: List<String>): Single<Entities> {
return if (entityIds.isEmpty())
Single.error(Exception("empty list passed for ids"))
else
mediaDetailInterface.getEntity(entityIds.joinToString("|"))
}
/**
* Check if media for user has reached the end of the list.
* @param userName
* @return
*/
fun doesMediaListForUserHaveMorePages(userName: String): Boolean {
val key = "user_$userName"
return if (continuationExists.containsKey(key)) continuationExists[key]!! else true
}
}

View file

@ -43,7 +43,6 @@ import fr.free.nrw.commons.MediaDataExtractor;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.auth.AccountUtil;
import fr.free.nrw.commons.category.CategoryClient;
import fr.free.nrw.commons.category.CategoryDetailsActivity;
import fr.free.nrw.commons.contributions.ContributionsFragment;
import fr.free.nrw.commons.delete.DeleteHelper;
@ -54,11 +53,12 @@ import fr.free.nrw.commons.ui.widget.HtmlTextView;
import fr.free.nrw.commons.utils.ViewUtilWrapper;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import javax.inject.Inject;
import org.apache.commons.lang3.StringUtils;
import org.wikipedia.util.DateUtil;
@ -70,7 +70,6 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
private boolean isCategoryImage;
private MediaDetailPagerFragment.MediaDetailProvider detailProvider;
private int index;
private Locale locale;
private boolean isDeleted = false;
@ -141,7 +140,6 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
@BindView(R.id.mediaDetailScrollView)
ScrollView scrollView;
private ArrayList<String> categoryNames;
/**
* Depicts is a feature part of Structured data. Multiple Depictions can be added for an image just like categories.
* However unlike categories depictions is multi-lingual
@ -150,10 +148,6 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
private ImageInfo imageInfoCache;
private int oldWidthOfImageView;
private int newWidthOfImageView;
private Depictions depictions;
private boolean categoriesLoaded = false;
private boolean categoriesPresent = false;
private boolean depictionLoaded = false;
private boolean heightVerifyingBoolean = true; // helps in maintaining aspect ratio
private ViewTreeObserver.OnGlobalLayoutListener layoutListener; // for layout stuff, only used once!
@ -203,9 +197,6 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
reasonList.add(getString(R.string.deletion_reason_no_longer_want_public));
reasonList.add(getString(R.string.deletion_reason_bad_for_my_privacy));
categoryNames = new ArrayList<>();
categoryNames.add(getString(R.string.detail_panel_cats_loading));
final View view = inflater.inflate(R.layout.fragment_media_detail, container, false);
ButterKnife.bind(this,view);
@ -217,7 +208,6 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
authorLayout.setVisibility(GONE);
}
locale = getResources().getConfiguration().locale;
return view;
}
@ -291,19 +281,55 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
}
private void displayMediaDetails() {
//Always load image from Internet to allow viewing the desc, license, and cats
setupImageView();
title.setText(media.getDisplayTitle());
desc.setHtmlText(media.getDescription());
license.setText(media.getLicense());
Disposable disposable = mediaDataExtractor.fetchMediaDetails(media.getFilename(), media.getPageId())
setTextFields(media);
compositeDisposable.addAll(
mediaDataExtractor.fetchDepictionIdsAndLabels(media)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::setTextFields);
compositeDisposable.add(disposable);
.subscribe(this::onDepictionsLoaded, Timber::e),
mediaDataExtractor.checkDeletionRequestExists(media)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::onDeletionPageExists, Timber::e),
mediaDataExtractor.fetchDiscussion(media)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::onDiscussionLoaded, Timber::e),
mediaDataExtractor.refresh(media)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::onMediaRefreshed, Timber::e)
);
}
private void onMediaRefreshed(Media media) {
setTextFields(media);
compositeDisposable.addAll(
mediaDataExtractor.fetchDepictionIdsAndLabels(media)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::onDepictionsLoaded, Timber::e)
);
}
private void onDiscussionLoaded(String discussion) {
mediaDiscussion.setText(prettyDiscussion(discussion.trim()));
}
private void onDeletionPageExists(Boolean deletionPageExists) {
if (deletionPageExists){
delete.setVisibility(GONE);
nominatedForDeletion.setVisibility(VISIBLE);
} else if (!isCategoryImage) {
delete.setVisibility(VISIBLE);
nominatedForDeletion.setVisibility(GONE);
}
}
private void onDepictionsLoaded(List<IdAndCaptions> idAndCaptions){
depictsLayout.setVisibility(idAndCaptions.isEmpty() ? GONE : VISIBLE);
buildDepictionList(idAndCaptions);
}
/**
* The imageSpacer is Basically a transparent overlay for the SimpleDraweeView
* which holds the image to be displayed( moreover this image is out of
@ -370,58 +396,45 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
}
private void setTextFields(Media media) {
this.media = media;
setupImageView();
title.setText(media.getDisplayTitle());
desc.setHtmlText(prettyDescription(media));
license.setText(prettyLicense(media));
coordinates.setText(prettyCoordinates(media));
uploadedDate.setText(prettyUploadedDate(media));
mediaDiscussion.setText(prettyDiscussion(media));
if (prettyCaption(media).equals(getContext().getString(R.string.detail_caption_empty))) {
captionLayout.setVisibility(GONE);
} else mediaCaption.setText(prettyCaption(media));
} else {
mediaCaption.setText(prettyCaption(media));
}
categoryNames.clear();
categoryNames.addAll(media.getCategories());
depictions=media.getDepiction();
depictionLoaded = true;
categoriesLoaded = true;
categoriesPresent = (categoryNames.size() > 0);
if (!categoriesPresent) {
final List<String> categories = media.getCategories();
if (categories.isEmpty()) {
// Stick in a filler element.
categoryNames.add(getString(R.string.detail_panel_cats_none));
categories.add(getString(R.string.detail_panel_cats_none));
}
rebuildCatList();
rebuildCatList(categories);
if(depictions != null) {
rebuildDepictionList();
}
else depictsLayout.setVisibility(GONE);
if (media.getCreator() == null || media.getCreator().equals("")) {
authorLayout.setVisibility(GONE);
} else {
author.setText(media.getCreator());
}
checkDeletion(media);
}
/**
* Populates media details fragment with depiction list
* @param idAndCaptions
*/
private void rebuildDepictionList() {
private void buildDepictionList(List<IdAndCaptions> idAndCaptions) {
depictionContainer.removeAllViews();
for (IdAndLabel depiction : depictions.getDepictions()) {
depictionContainer.addView(
buildDepictLabel(
depiction.getEntityLabel(),
depiction.getEntityId(),
for (IdAndCaptions idAndCaption : idAndCaptions) {
depictionContainer.addView(buildDepictLabel(
idAndCaption.getCaptions().values().iterator().next(),
idAndCaption.getId(),
depictionContainer
));
}
@ -446,7 +459,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
@OnClick(R.id.copyWikicode)
public void onCopyWikicodeClicked(){
String data = "[[" + media.getFilename() + "|thumb|" + media.getDescription() + "]]";
String data = "[[" + media.getFilename() + "|thumb|" + media.getFallbackDescription() + "]]";
Utils.copy("wikiCode",data,getContext());
Timber.d("Generated wikidata copy code: %s", data);
@ -573,42 +586,37 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
}
}
private void rebuildCatList() {
private void rebuildCatList(List<String> categories) {
categoryContainer.removeAllViews();
// @fixme add the category items
for (String category : categories) {
categoryContainer.addView(buildCatLabel(sanitise(category), categoryContainer));
}
}
//As per issue #1826(see https://github.com/commons-app/apps-android-commons/issues/1826), some categories come suffixed with strings prefixed with |. As per the discussion
//that was meant for alphabetical sorting of the categories and can be safely removed.
for (int i = 0; i < categoryNames.size(); i++) {
String categoryName = categoryNames.get(i);
//Removed everything after '|'
int indexOfPipe = categoryName.indexOf('|');
private String sanitise(String category) {
int indexOfPipe = category.indexOf('|');
if (indexOfPipe != -1) {
categoryName = categoryName.substring(0, indexOfPipe);
//Set the updated category to the list as well
categoryNames.set(i, categoryName);
}
View catLabel = buildCatLabel(categoryName, categoryContainer);
categoryContainer.addView(catLabel);
//Removed everything after '|'
return category.substring(0, indexOfPipe);
}
return category;
}
/**
* Add view to depictions obtained also tapping on depictions should open the url
*/
private View buildDepictLabel(String depictionName, String entityId, LinearLayout depictionContainer) {
final View item = LayoutInflater.from(getContext()).inflate(R.layout.detail_category_item, depictionContainer, false);
final View item = LayoutInflater.from(getContext()).inflate(R.layout.detail_category_item, depictionContainer,false);
final TextView textView = item.findViewById(R.id.mediaDetailCategoryItemText);
textView.setText(depictionName);
if (depictionLoaded) {
item.setOnClickListener(view -> {
Intent intent = new Intent(getContext(), WikidataItemDetailsActivity.class);
intent.putExtra("wikidataItemName", depictionName);
intent.putExtra("entityId", entityId);
getContext().startActivity(intent);
});
}
return item;
}
@ -617,7 +625,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
final TextView textView = item.findViewById(R.id.mediaDetailCategoryItemText);
textView.setText(catName);
if (categoriesLoaded && categoriesPresent) {
if(!getString(R.string.detail_panel_cats_none).equals(catName)) {
textView.setOnClickListener(view -> {
// Open Category Details page
String selectedCategoryTitle = CATEGORY_PREFIX + catName;
@ -636,30 +644,36 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
* @return caption as string
*/
private String prettyCaption(Media media) {
String caption = media.getCaption().trim();
for (String caption : media.getCaptions().values()) {
if (caption.equals("")) {
return getString(R.string.detail_caption_empty);
} else {
return caption;
}
}
return getString(R.string.detail_caption_empty);
}
private String prettyDescription(Media media) {
// @todo use UI language when multilingual descs are available
String desc = media.getDescription();
if (desc.equals("")) {
return getString(R.string.detail_description_empty);
} else {
return desc;
final String description = chooseDescription(media);
return description.isEmpty() ? getString(R.string.detail_description_empty)
: description;
}
private String chooseDescription(Media media) {
final Map<String, String> descriptions = media.getDescriptions();
final String multilingualDesc = descriptions.get(Locale.getDefault().getLanguage());
if (multilingualDesc != null) {
return multilingualDesc;
}
private String prettyDiscussion(Media media) {
String disc = media.getDiscussion().trim();
if (disc.equals("")) {
return getString(R.string.detail_discussion_empty);
} else {
return disc;
for (String description : descriptions.values()) {
return description;
}
return media.getFallbackDescription();
}
private String prettyDiscussion(String discussion) {
return discussion.isEmpty() ? getString(R.string.detail_discussion_empty) : discussion;
}
private String prettyLicense(Media media) {
@ -691,14 +705,4 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
return media.getCoordinates().getPrettyCoordinateString();
}
private void checkDeletion(Media media){
if (media.isRequestedDeletion()){
delete.setVisibility(GONE);
nominatedForDeletion.setVisibility(VISIBLE);
} else if (!isCategoryImage) {
delete.setVisibility(VISIBLE);
nominatedForDeletion.setVisibility(GONE);
}
}
}

View file

@ -1,7 +1,6 @@
package fr.free.nrw.commons.media;
import fr.free.nrw.commons.depictions.models.DepictionResponse;
import io.reactivex.Observable;
import io.reactivex.Single;
import java.util.Map;
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
@ -24,7 +23,7 @@ public interface MediaInterface {
* @return
*/
@GET("w/api.php?action=query&format=json&formatversion=2")
Observable<MwQueryResponse> checkPageExistsUsingTitle(@Query("titles") String title);
Single<MwQueryResponse> checkPageExistsUsingTitle(@Query("titles") String title);
/**
* Check if file exists
@ -33,7 +32,7 @@ public interface MediaInterface {
* @return
*/
@GET("w/api.php?action=query&format=json&formatversion=2&list=allimages")
Observable<MwQueryResponse> checkFileExistsUsingSha(@Query("aisha1") String aisha1);
Single<MwQueryResponse> checkFileExistsUsingSha(@Query("aisha1") String aisha1);
/**
* This method retrieves a list of Media objects filtered using image generator query
@ -46,7 +45,8 @@ public interface MediaInterface {
@GET("w/api.php?action=query&format=json&formatversion=2" + //Basic parameters
"&generator=categorymembers&gcmtype=file&gcmsort=timestamp&gcmdir=desc" + //Category parameters
MEDIA_PARAMS)
Observable<MwQueryResponse> getMediaListFromCategory(@Query("gcmtitle") String category, @Query("gcmlimit") int itemLimit, @QueryMap Map<String, String> continuation);
Single<MwQueryResponse> getMediaListFromCategory(@Query("gcmtitle") String category, @Query("gcmlimit") int itemLimit, @QueryMap Map<String, String> continuation);
/**
* This method retrieves a list of Media objects for a given user name
@ -58,7 +58,7 @@ public interface MediaInterface {
*/
@GET("w/api.php?action=query&format=json&formatversion=2" + //Basic parameters
"&generator=allimages&gaisort=timestamp&gaidir=older" + MEDIA_PARAMS)
Observable<MwQueryResponse> getMediaListForUser(@Query("gaiuser") String username,
Single<MwQueryResponse> getMediaListForUser(@Query("gaiuser") String username,
@Query("gailimit") int itemLimit, @QueryMap(encoded = true) Map<String, String> continuation);
/**
@ -82,7 +82,17 @@ public interface MediaInterface {
*/
@GET("w/api.php?action=query&format=json&formatversion=2" +
MEDIA_PARAMS)
Observable<MwQueryResponse> getMedia(@Query("titles") String title);
Single<MwQueryResponse> getMedia(@Query("titles") String title);
/**
* Fetches Media object from the imageInfo API
*
* @param pageIds the ids to be searched for
* @return
*/
@GET("w/api.php?action=query&format=json&formatversion=2" +
MEDIA_PARAMS)
Single<MwQueryResponse> getMediaById(@Query("pageids") String pageIds);
/**
* Fetches Media object from the imageInfo API
@ -93,21 +103,27 @@ public interface MediaInterface {
*/
@GET("w/api.php?action=query&format=json&formatversion=2&generator=images" +
MEDIA_PARAMS)
Observable<MwQueryResponse> getMediaWithGenerator(@Query("titles") String title);
Single<MwQueryResponse> getMediaWithGenerator(@Query("titles") String title);
@GET("w/api.php?format=json&action=parse&prop=text")
Observable<MwParseResponse> getPageHtml(@Query("page") String title);
Single<MwParseResponse> getPageHtml(@Query("page") String title);
/**
* Fetches caption using file name
*
* @param filename name of the file to be used for fetching captions
* */
@GET("w/api.php?action=wbgetentities&props=labels&format=json&languagefallback=1")
Single<MwQueryResponse> fetchCaptionByFilename(@Query("language") String language, @Query("titles") String filename);
/**
* Fetches list of images from a depiction entity
*
* @param query depictionEntityId
* @param sroffset number od depictions already fetched, this is useful in implementing
* pagination
* @param sroffset number od depictions already fetched, this is useful in implementing pagination
*/
@GET("w/api.php?action=query&list=search&format=json&srnamespace=6")
Observable<DepictionResponse> fetchImagesForDepictedItem(@Query("srsearch") String query,
@Query("sroffset") String sroffset);
Single<DepictionResponse> fetchImagesForDepictedItem(@Query("srsearch") String query, @Query("sroffset") String sroffset);
}

View file

@ -14,18 +14,13 @@ import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.appcompat.widget.Toolbar;
import androidx.drawerlayout.widget.DrawerLayout;
import butterknife.BindView;
import butterknife.ButterKnife;
import com.facebook.drawee.view.SimpleDraweeView;
import com.google.android.material.navigation.NavigationView;
import com.viewpagerindicator.CirclePageIndicator;
import javax.inject.Inject;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.delete.DeleteHelper;
@ -35,6 +30,7 @@ import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
import javax.inject.Inject;
public class ReviewActivity extends NavigationBaseActivity {
@ -144,10 +140,8 @@ public class ReviewActivity extends NavigationBaseActivity {
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(media -> {
if (media != null) {
reviewPagerAdapter.disableButtons();
updateImage(media);
}
}));
return true;
}

View file

@ -1,22 +1,19 @@
package fr.free.nrw.commons.review;
import org.apache.commons.lang3.StringUtils;
import org.wikipedia.dataclient.mwapi.MwQueryPage;
import org.wikipedia.dataclient.mwapi.RecentChange;
import org.wikipedia.util.DateUtil;
import java.util.Collections;
import java.util.Date;
import java.util.Random;
import javax.inject.Inject;
import javax.inject.Singleton;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.media.MediaClient;
import io.reactivex.Observable;
import io.reactivex.Single;
import java.util.Collections;
import java.util.Date;
import java.util.Random;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.apache.commons.lang3.StringUtils;
import org.wikipedia.dataclient.mwapi.MwQueryPage;
import org.wikipedia.dataclient.mwapi.RecentChange;
import org.wikipedia.util.DateUtil;
@Singleton
public class ReviewHelper {
@ -69,7 +66,6 @@ public class ReviewHelper {
public Single<Media> getRandomMedia() {
return getRecentChanges()
.flatMapSingle(change -> getRandomMediaFromRecentChange(change))
.onExceptionResumeNext(Observable.just(new Media("")))
.filter(media -> !StringUtils.isBlank(media.getFilename()))
.firstOrError();
}
@ -86,7 +82,7 @@ public class ReviewHelper {
.flatMap(change -> mediaClient.checkPageExistsUsingTitle("Commons:Deletion_requests/" + change.getTitle()))
.flatMap(isDeleted -> {
if (isDeleted) {
return Single.just(new Media(""));
return Single.error(new Exception(recentChange.getTitle() + " is deleted"));
}
return mediaClient.getMedia(recentChange.getTitle());
});

View file

@ -33,7 +33,7 @@ class PageContentsCreator {
buffer
.append("== {{int:filedesc}} ==\n")
.append("{{Information\n")
.append("|description=").append(contribution.getDescription()).append("\n")
.append("|description=").append(contribution.getFallbackDescription()).append("\n")
.append("|source=").append("{{own}}\n")
.append("|author=[[User:").append(contribution.getCreator()).append("|")
.append(contribution.getCreator()).append("]]\n");

View file

@ -13,7 +13,6 @@ import android.net.Uri;
import android.os.IBinder;
import android.provider.MediaStore;
import android.text.TextUtils;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.contributions.Contribution;
@ -110,8 +109,8 @@ public class UploadController {
contribution.setCreator(sessionManager.getAuthorName());
}
if (contribution.getDescription() == null) {
contribution.setDescription("");
if (contribution.getFallbackDescription() == null) {
contribution.setFallbackDescription("");
}
final String license = store.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_3);
@ -164,7 +163,7 @@ public class UploadController {
return mimeType;
}
private long resolveDataLength(final ContentResolver contentResolver, final Media contribution) {
private long resolveDataLength(final ContentResolver contentResolver, final Contribution contribution) {
try {
if (contribution.getDataLength() <= 0) {
Timber.d("UploadController/doInBackground, contribution.getLocalUri():%s", contribution.getLocalUri());
@ -182,7 +181,7 @@ public class UploadController {
return contribution.getDataLength();
}
private Date resolveDateTakenOrNow(final ContentResolver contentResolver, final Media contribution) {
private Date resolveDateTakenOrNow(final ContentResolver contentResolver, final Contribution contribution) {
Timber.d("local uri %s", contribution.getLocalUri());
try(final Cursor cursor = dateTakenCursor(contentResolver, contribution)) {
if (cursor != null && cursor.getCount() != 0 && cursor.getColumnCount() != 0) {
@ -196,7 +195,7 @@ public class UploadController {
}
}
private Cursor dateTakenCursor(final ContentResolver contentResolver, final Media contribution) {
private Cursor dateTakenCursor(final ContentResolver contentResolver, final Contribution contribution) {
return contentResolver.query(contribution.getLocalUri(),
new String[]{MediaStore.Images.ImageColumns.DATE_TAKEN}, null, null, null);
}

View file

@ -57,7 +57,7 @@ data class UploadMediaDetail constructor(
* @return a string with the pattern of {{en|1=descriptionText}}
*/
@JvmStatic
fun formatList(descriptions: List<UploadMediaDetail>) =
fun formatDescriptions(descriptions: List<UploadMediaDetail>) =
descriptions.filter { it.descriptionText.isNotEmpty() }
.joinToString { "{{${it.languageCode}|1=${it.descriptionText}}}" }
}

View file

@ -141,9 +141,6 @@ public class UploadModel {
{
final Contribution contribution = new Contribution(
item, sessionManager, newListOf(selectedDepictions), newListOf(selectedCategories));
Timber.d("Created timestamp while building contribution is %s, %s",
item.getCreatedTimestamp(),
new Date(item.getCreatedTimestamp()));
if (item.getCreatedTimestamp() != -1L) {
contribution.setDateCreated(new Date(item.getCreatedTimestamp()));
contribution.setDateCreatedSource(item.getCreatedTimestampSource());

View file

@ -5,7 +5,7 @@ import fr.free.nrw.commons.nearby.Place
import kotlinx.android.parcel.Parcelize
@Parcelize
internal data class WikidataPlace(override val id: String, override val name: String, val imageValue: String?) :
data class WikidataPlace(override val id: String, override val name: String, val imageValue: String?) :
WikidataItem,Parcelable {
constructor(place: Place) : this(
place.wikiDataEntityId!!,

View file

@ -1,5 +1,8 @@
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.category.CategoryItem
import fr.free.nrw.commons.location.LatLng
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import java.util.*
fun depictedItem(
name: String = "label",
@ -21,3 +24,35 @@ fun depictedItem(
fun categoryItem(name: String = "name", selected: Boolean = false) =
CategoryItem(name, selected)
fun media(
thumbUrl: String? = "thumbUrl",
imageUrl: String? = "imageUrl",
filename: String? = "filename",
fallbackDescription: String? = "fallbackDescription",
dateUploaded: Date? = Date(),
license: String? = "license",
licenseUrl: String? = "licenseUrl",
creator: String? = "creator",
pageId: String = "pageId",
categories: List<String>? = listOf("categories"),
coordinates: LatLng? = LatLng(0.0, 0.0, 0.0f),
captions: Map<String?, String?> = mapOf("en" to "caption"),
descriptions: Map<String?, String?> = mapOf("en" to "description"),
depictionIds: List<String> = listOf("depictionId")
) = Media(
thumbUrl,
imageUrl,
filename,
fallbackDescription,
dateUploaded,
license,
licenseUrl,
creator,
pageId,
categories,
coordinates,
captions,
descriptions,
depictionIds
)

View file

@ -1,5 +1,6 @@
package fr.free.nrw.commons
import media
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@ -11,13 +12,15 @@ import org.robolectric.annotation.Config
class MediaTest {
@Test
fun displayTitleShouldStripExtension() {
val m = Media("File:Example.jpg")
val m = media(filename = "File:Example.jpg")
assertEquals("Example", m.displayTitle)
}
@Test
fun displayTitleShouldUseSpaceForUnderscore() {
val m = Media("File:Example 1_2.jpg")
val m = media(filename = "File:Example 1_2.jpg")
assertEquals("Example 1 2", m.displayTitle)
}
}

View file

@ -1,108 +0,0 @@
package fr.free.nrw.commons.bookmarks.pictures;
import android.net.Uri;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.ArrayList;
import java.util.List;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.bookmarks.Bookmark;
import fr.free.nrw.commons.media.MediaClient;
import io.reactivex.Single;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;
/**
* Tests for bookmark pictures controller
*/
public class BookmarkPicturesControllerTest {
@Mock
MediaClient mediaClient;
@Mock
BookmarkPicturesDao bookmarkDao;
@InjectMocks
BookmarkPicturesController bookmarkPicturesController;
/**
* Init mocks
*/
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
Media mockMedia = getMockMedia();
when(bookmarkDao.getAllBookmarks())
.thenReturn(getMockBookmarkList());
when(mediaClient.getMedia(anyString()))
.thenReturn(Single.just(mockMedia));
}
/**
* Get mock bookmark list
* @return
*/
private List<Bookmark> getMockBookmarkList() {
ArrayList<Bookmark> list = new ArrayList<>();
list.add(new Bookmark("File:Test1.jpg", "Maskaravivek", Uri.EMPTY));
list.add(new Bookmark("File:Test2.jpg", "Maskaravivek", Uri.EMPTY));
return list;
}
/**
* Test case where all bookmark pictures are fetched and media is found against it
*/
@Test
public void loadBookmarkedPictures() {
List<Media> bookmarkedPictures = bookmarkPicturesController.loadBookmarkedPictures().blockingGet();
assertEquals(2, bookmarkedPictures.size());
}
/**
* Test case where all bookmark pictures are fetched and only one media is found
*/
@Test
public void loadBookmarkedPicturesForNullMedia() {
when(mediaClient.getMedia("File:Test1.jpg"))
.thenReturn(Single.error(new NullPointerException("Error occurred")));
when(mediaClient.getMedia("File:Test2.jpg"))
.thenReturn(Single.just(getMockMedia()));
List<Media> bookmarkedPictures = bookmarkPicturesController.loadBookmarkedPictures().blockingGet();
assertEquals(1, bookmarkedPictures.size());
}
private Media getMockMedia() {
return new Media("File:Test.jpg");
}
/**
* Test case where current bookmarks don't match the bookmarks in DB
*/
@Test
public void needRefreshBookmarkedPictures() {
boolean needRefreshBookmarkedPictures = bookmarkPicturesController.needRefreshBookmarkedPictures();
assertTrue(needRefreshBookmarkedPictures);
}
/**
* Test case where the DB is up to date with the bookmarks loaded in the list
*/
@Test
public void doNotNeedRefreshBookmarkedPictures() {
List<Media> bookmarkedPictures = bookmarkPicturesController.loadBookmarkedPictures().blockingGet();
assertEquals(2, bookmarkedPictures.size());
boolean needRefreshBookmarkedPictures = bookmarkPicturesController.needRefreshBookmarkedPictures();
assertFalse(needRefreshBookmarkedPictures);
}
}

View file

@ -0,0 +1,110 @@
package fr.free.nrw.commons.bookmarks.pictures
import android.net.Uri
import com.nhaarman.mockitokotlin2.whenever
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.bookmarks.Bookmark
import fr.free.nrw.commons.media.MediaClient
import io.reactivex.Single
import media
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.mockito.ArgumentMatchers
import org.mockito.InjectMocks
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import java.util.*
/**
* Tests for bookmark pictures controller
*/
class BookmarkPicturesControllerTest {
@Mock
var mediaClient: MediaClient? = null
@Mock
var bookmarkDao: BookmarkPicturesDao? = null
@InjectMocks
var bookmarkPicturesController: BookmarkPicturesController? = null
/**
* Init mocks
*/
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
val mockMedia = mockMedia
whenever(bookmarkDao!!.allBookmarks)
.thenReturn(mockBookmarkList)
whenever(
mediaClient!!.getMedia(
ArgumentMatchers.anyString()
)
)
.thenReturn(Single.just(mockMedia))
}
/**
* Get mock bookmark list
* @return
*/
private val mockBookmarkList: List<Bookmark>
private get() {
val list = ArrayList<Bookmark>()
list.add(Bookmark("File:Test1.jpg", "Maskaravivek", Uri.EMPTY))
list.add(Bookmark("File:Test2.jpg", "Maskaravivek", Uri.EMPTY))
return list
}
/**
* Test case where all bookmark pictures are fetched and media is found against it
*/
@Test
fun loadBookmarkedPictures() {
val bookmarkedPictures =
bookmarkPicturesController!!.loadBookmarkedPictures().blockingGet()
Assert.assertEquals(2, bookmarkedPictures.size.toLong())
}
/**
* Test case where all bookmark pictures are fetched and only one media is found
*/
@Test
fun loadBookmarkedPicturesForNullMedia() {
whenever(mediaClient!!.getMedia("File:Test1.jpg"))
.thenReturn(Single.error(NullPointerException("Error occurred")))
whenever(mediaClient!!.getMedia("File:Test2.jpg"))
.thenReturn(Single.just(mockMedia))
val bookmarkedPictures =
bookmarkPicturesController!!.loadBookmarkedPictures().blockingGet()
Assert.assertEquals(1, bookmarkedPictures.size.toLong())
}
private val mockMedia: Media
private get() = media(filename="File:Test.jpg")
/**
* Test case where current bookmarks don't match the bookmarks in DB
*/
@Test
fun needRefreshBookmarkedPictures() {
val needRefreshBookmarkedPictures =
bookmarkPicturesController!!.needRefreshBookmarkedPictures()
Assert.assertTrue(needRefreshBookmarkedPictures)
}
/**
* Test case where the DB is up to date with the bookmarks loaded in the list
*/
@Test
fun doNotNeedRefreshBookmarkedPictures() {
val bookmarkedPictures =
bookmarkPicturesController!!.loadBookmarkedPictures().blockingGet()
Assert.assertEquals(2, bookmarkedPictures.size.toLong())
val needRefreshBookmarkedPictures =
bookmarkPicturesController!!.needRefreshBookmarkedPictures()
Assert.assertFalse(needRefreshBookmarkedPictures)
}
}

View file

@ -8,12 +8,12 @@ import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
import fr.free.nrw.commons.utils.ViewUtilWrapper
import io.reactivex.Single
import media
import org.junit.Before
import org.junit.Test
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.InjectMocks
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.Mockito.*
import org.mockito.MockitoAnnotations
import java.util.*
@ -55,8 +55,7 @@ class ReasonBuilderTest {
`when`(okHttpJsonApiClient!!.getAchievements(anyString()))
.thenReturn(Single.just(mock(FeedbackResponse::class.java)))
val media = Media("test_file")
media.dateUploaded=Date()
val media = media(filename="test_file", dateUploaded = Date())
reasonBuilder!!.getReason(media, "test")
verify(sessionManager, times(0))!!.forceLogin(any(Context::class.java))

View file

@ -6,7 +6,6 @@ import fr.free.nrw.commons.depictions.Media.DepictedImagesPresenter
import fr.free.nrw.commons.explore.depictions.DepictsClient
import fr.free.nrw.commons.kvstore.JsonKvStore
import fr.free.nrw.commons.media.MediaClient
import io.reactivex.Observable
import io.reactivex.Single
import io.reactivex.schedulers.TestScheduler
import org.junit.Before
@ -40,7 +39,7 @@ class DepictedImagesPresenterTest {
@Mock
lateinit var mediaItem: Media
var testObservable: Observable<List<Media>>? = null
var testSingle: Single<List<Media>>? = null
@Before
@ -49,7 +48,7 @@ class DepictedImagesPresenterTest {
MockitoAnnotations.initMocks(this)
testScheduler = TestScheduler()
mediaList.add(mediaItem)
testObservable = Observable.just(mediaList)
testSingle = Single.just(mediaList)
depictedImagesPresenter = DepictedImagesPresenter(jsonKvStore, depictsClient, mediaClient, testScheduler, testScheduler)
depictedImagesPresenter.onAttachView(view)
}
@ -59,18 +58,9 @@ class DepictedImagesPresenterTest {
Mockito.`when`(
depictsClient.fetchImagesForDepictedItem(ArgumentMatchers.anyString(),
ArgumentMatchers.anyInt())
).thenReturn(testObservable)
).thenReturn(testSingle)
depictedImagesPresenter.initList("rabbit")
depictedImagesPresenter.handleSuccess(mediaList)
verify(view)?.handleSuccess(mediaList)
}
@Test
fun replaceTitlesWithCaptions() {
var stringObservable: Single<String>? = Single.just(String())
Mockito.`when`(mediaClient.getCaptionByWikibaseIdentifier(ArgumentMatchers.anyString()))?.thenReturn(stringObservable)
depictedImagesPresenter.replaceTitlesWithCaptions("File:rabbit.jpg", 0)
testScheduler.triggerActions()
verify(view)?.handleLabelforImage("", 0)
}
}

View file

@ -1,10 +1,7 @@
package fr.free.nrw.commons.explore.media
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.depictions.Media.DepictedImagesFragment.PAGE_ID_PREFIX
import fr.free.nrw.commons.media.MediaClient
import io.reactivex.Single
import org.hamcrest.MatcherAssert.assertThat
@ -13,10 +10,6 @@ import org.junit.Before
import org.junit.Test
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import org.wikipedia.dataclient.mwapi.MwQueryPage
import org.wikipedia.dataclient.mwapi.MwQueryResponse
import org.wikipedia.dataclient.mwapi.MwQueryResult
import org.wikipedia.wikidata.Entities
class PageableMediaDataSourceTest {
@Mock
@ -31,41 +24,10 @@ class PageableMediaDataSourceTest {
@Test
fun `loadFunction invokes mediaClient and has Label`() {
val (media, entity: Entities.Entity) = expectMediaAndEntity()
val label: Entities.Label = mock()
whenever(entity.labels()).thenReturn(mapOf(" " to label))
whenever(label.value()).thenReturn("label")
val pageableMediaDataSource = PageableMediaDataSource(mock(), mediaConverter, mediaClient)
pageableMediaDataSource.onQueryUpdated("test")
assertThat(pageableMediaDataSource.loadFunction(0,1), `is`(listOf(media)))
verify(media).caption = "label"
}
@Test
fun `loadFunction invokes mediaClient and does not have Label`() {
val (media, entity: Entities.Entity) = expectMediaAndEntity()
whenever(entity.labels()).thenReturn(mapOf())
val pageableMediaDataSource = PageableMediaDataSource(mock(), mediaConverter, mediaClient)
pageableMediaDataSource.onQueryUpdated("test")
assertThat(pageableMediaDataSource.loadFunction(0,1), `is`(listOf(media)))
verify(media).caption = MediaClient.NO_CAPTION
}
private fun expectMediaAndEntity(): Pair<Media, Entities.Entity> {
val queryResponse: MwQueryResponse = mock()
whenever(mediaClient.getMediaListFromSearch("test", 0, 1))
.thenReturn(Single.just(queryResponse))
val queryResult: MwQueryResult = mock()
whenever(queryResponse.query()).thenReturn(queryResult)
val queryPage: MwQueryPage = mock()
whenever(queryResult.pages()).thenReturn(listOf(queryPage))
val media = mock<Media>()
whenever(mediaConverter.convert(queryPage)).thenReturn(media)
whenever(media.pageId).thenReturn("1")
val entities: Entities = mock()
whenever(mediaClient.getEntities("${PAGE_ID_PREFIX}1")).thenReturn(Single.just(entities))
val entity: Entities.Entity = mock()
whenever(entities.entities()).thenReturn(mapOf("" to entity))
return Pair(media, entity)
.thenReturn(Single.just(emptyList()))
val pageableMediaDataSource = PageableMediaDataSource(mock(), mediaClient)
pageableMediaDataSource.onQueryUpdated("test")
assertThat(pageableMediaDataSource.loadFunction(0,1), `is`(emptyList()))
}
}

View file

@ -1,29 +1,33 @@
package fr.free.nrw.commons.media
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.whenever
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.explore.media.MediaConverter
import fr.free.nrw.commons.utils.CommonsDateUtil
import io.reactivex.Observable
import io.reactivex.Single
import junit.framework.Assert.*
import org.junit.Before
import org.junit.Test
import org.mockito.*
import org.mockito.Mockito.*
import org.wikipedia.dataclient.mwapi.ImageDetails
import org.wikipedia.dataclient.mwapi.MwQueryPage
import org.wikipedia.dataclient.mwapi.MwQueryResponse
import org.wikipedia.dataclient.mwapi.MwQueryResult
import org.wikipedia.gallery.ImageInfo
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.*
import org.wikipedia.wikidata.Entities
import java.util.*
import org.mockito.Captor
import org.mockito.Mockito.*
class MediaClientTest {
@Mock
internal var mediaInterface: MediaInterface? = null
@Mock
internal var mediaConverter: MediaConverter? = null
@Mock
internal var mediaDetailInterface: MediaDetailInterface? = null
@InjectMocks
var mediaClient: MediaClient? = null
@ -45,7 +49,7 @@ class MediaClientTest {
`when`(mockResponse.query()).thenReturn(mwQueryResult)
`when`(mediaInterface!!.checkPageExistsUsingTitle(ArgumentMatchers.anyString()))
.thenReturn(Observable.just(mockResponse))
.thenReturn(Single.just(mockResponse))
val checkPageExistsUsingTitle =
mediaClient!!.checkPageExistsUsingTitle("File:Test.jpg").blockingGet()
@ -63,7 +67,7 @@ class MediaClientTest {
`when`(mockResponse.query()).thenReturn(mwQueryResult)
`when`(mediaInterface!!.checkPageExistsUsingTitle(ArgumentMatchers.anyString()))
.thenReturn(Observable.just(mockResponse))
.thenReturn(Single.just(mockResponse))
val checkPageExistsUsingTitle =
mediaClient!!.checkPageExistsUsingTitle("File:Test.jpg").blockingGet()
@ -81,7 +85,7 @@ class MediaClientTest {
`when`(mockResponse.query()).thenReturn(mwQueryResult)
`when`(mediaInterface!!.checkFileExistsUsingSha(ArgumentMatchers.anyString()))
.thenReturn(Observable.just(mockResponse))
.thenReturn(Single.just(mockResponse))
val checkFileExistsUsingSha = mediaClient!!.checkFileExistsUsingSha("abcde").blockingGet()
assertTrue(checkFileExistsUsingSha)
@ -98,7 +102,7 @@ class MediaClientTest {
`when`(mockResponse.query()).thenReturn(mwQueryResult)
`when`(mediaInterface!!.checkFileExistsUsingSha(ArgumentMatchers.anyString()))
.thenReturn(Observable.just(mockResponse))
.thenReturn(Single.just(mockResponse))
val checkFileExistsUsingSha = mediaClient!!.checkFileExistsUsingSha("abcde").blockingGet()
assertFalse(checkFileExistsUsingSha)
@ -106,21 +110,12 @@ class MediaClientTest {
@Test
fun getMedia() {
val imageInfo = ImageInfo()
val mwQueryPage = mock(MwQueryPage::class.java)
`when`(mwQueryPage.title()).thenReturn("Test")
`when`(mwQueryPage.imageInfo()).thenReturn(imageInfo)
val mwQueryResult = mock(MwQueryResult::class.java)
`when`(mwQueryResult.firstPage()).thenReturn(mwQueryPage)
val mockResponse = mock(MwQueryResponse::class.java)
`when`(mockResponse.query()).thenReturn(mwQueryResult)
val (mockResponse, media: Media) = expectGetEntitiesAndMediaConversion()
`when`(mediaInterface!!.getMedia(ArgumentMatchers.anyString()))
.thenReturn(Observable.just(mockResponse))
.thenReturn(Single.just(mockResponse))
assertEquals("Test", mediaClient!!.getMedia("abcde").blockingGet().filename)
mediaClient!!.getMedia("abcde").test().assertValue(media)
}
@Test
@ -137,34 +132,34 @@ class MediaClientTest {
`when`(mockResponse.query()).thenReturn(mwQueryResult)
`when`(mediaInterface!!.getMedia(ArgumentMatchers.anyString()))
.thenReturn(Observable.just(mockResponse))
assertEquals(Media.EMPTY, mediaClient!!.getMedia("abcde").blockingGet())
.thenReturn(Single.just(mockResponse))
mediaClient!!.getMedia("abcde").test().assertErrorMessage("empty list passed for ids")
}
@Captor
private val filenameCaptor: ArgumentCaptor<String>? = null
@Test
fun getPictureOfTheDay() {
val template = "Template:Potd/" + CommonsDateUtil.getIso8601DateFormatShort().format(Date())
val imageInfo = ImageInfo()
val (mockResponse, media: Media) = expectGetEntitiesAndMediaConversion()
`when`(mediaInterface!!.getMediaWithGenerator(template))
.thenReturn(Single.just(mockResponse))
mediaClient!!.getPictureOfTheDay().test().assertValue(media)
}
val mwQueryPage = mock(MwQueryPage::class.java)
`when`(mwQueryPage.title()).thenReturn("Test")
`when`(mwQueryPage.imageInfo()).thenReturn(imageInfo)
val mwQueryResult = mock(MwQueryResult::class.java)
`when`(mwQueryResult.firstPage()).thenReturn(mwQueryPage)
private fun expectGetEntitiesAndMediaConversion(): Pair<MwQueryResponse, Media> {
val mockResponse = mock(MwQueryResponse::class.java)
`when`(mockResponse.query()).thenReturn(mwQueryResult)
`when`(mediaInterface!!.getMediaWithGenerator(filenameCaptor!!.capture()))
.thenReturn(Observable.just(mockResponse))
assertEquals("Test", mediaClient!!.getPictureOfTheDay().blockingGet().filename)
assertEquals(template, filenameCaptor.value);
val queryResult: MwQueryResult = mock()
whenever(mockResponse.query()).thenReturn(queryResult)
val queryPage: MwQueryPage = mock()
whenever(queryResult.pages()).thenReturn(listOf(queryPage))
whenever(queryPage.pageId()).thenReturn(0)
val entities: Entities = mock()
whenever(mediaDetailInterface!!.getEntity("M0")).thenReturn(Single.just(entities))
val entity: Entities.Entity = mock()
whenever(entities.entities()).thenReturn(mapOf("id" to entity))
val media: Media = mock()
whenever(mediaConverter!!.convert(queryPage, entity)).thenReturn(media)
return Pair(mockResponse, media)
}
@Captor
@ -173,17 +168,8 @@ class MediaClientTest {
@Test
fun getMediaListFromCategoryTwice() {
val mockContinuation = mapOf(Pair("gcmcontinue", "test"))
val imageInfo = ImageInfo()
val mwQueryPage = mock(MwQueryPage::class.java)
`when`(mwQueryPage.title()).thenReturn("Test")
`when`(mwQueryPage.imageInfo()).thenReturn(imageInfo)
val mwQueryResult = mock(MwQueryResult::class.java)
`when`(mwQueryResult.pages()).thenReturn(listOf(mwQueryPage))
val mockResponse = mock(MwQueryResponse::class.java)
`when`(mockResponse.query()).thenReturn(mwQueryResult)
val (mockResponse, media: Media) = expectGetEntitiesAndMediaConversion()
`when`(mockResponse.continuation()).thenReturn(mockContinuation)
`when`(
@ -192,31 +178,23 @@ class MediaClientTest {
continuationCaptor!!.capture()
)
)
.thenReturn(Observable.just(mockResponse))
.thenReturn(Single.just(mockResponse))
val media1 = mediaClient!!.getMediaListFromCategory("abcde").blockingGet().get(0)
val media2 = mediaClient!!.getMediaListFromCategory("abcde").blockingGet().get(0)
assertEquals(continuationCaptor.allValues[0], emptyMap<String, String>())
assertEquals(continuationCaptor.allValues[1], mockContinuation)
assertEquals(media1.filename, "Test")
assertEquals(media2.filename, "Test")
assertEquals(media1, media)
assertEquals(media2, media)
}
@Test
fun getMediaListForUser() {
val mockContinuation = mapOf("gcmcontinue" to "test")
val imageInfo = ImageInfo()
val mwQueryPage = mock(MwQueryPage::class.java)
whenever(mwQueryPage.title()).thenReturn("Test")
whenever(mwQueryPage.imageInfo()).thenReturn(imageInfo)
val mwQueryResult = mock(MwQueryResult::class.java)
whenever(mwQueryResult.pages()).thenReturn(listOf(mwQueryPage))
val mockResponse = mock(MwQueryResponse::class.java)
whenever(mockResponse.query()).thenReturn(mwQueryResult)
val (mockResponse, media: Media) = expectGetEntitiesAndMediaConversion()
whenever(mockResponse.continuation()).thenReturn(mockContinuation)
whenever(
@ -225,7 +203,7 @@ class MediaClientTest {
continuationCaptor!!.capture()
)
)
.thenReturn(Observable.just(mockResponse))
.thenReturn(Single.just(mockResponse))
val media1 = mediaClient!!.getMediaListForUser("Test").blockingGet().get(0)
val media2 = mediaClient!!.getMediaListForUser("Test").blockingGet().get(0)
@ -245,7 +223,7 @@ class MediaClientTest {
mockResponse.setParse(mwParseResult)
`when`(mediaInterface!!.getPageHtml(ArgumentMatchers.anyString()))
.thenReturn(Observable.just(mockResponse))
.thenReturn(Single.just(mockResponse))
assertEquals("Test", mediaClient!!.getPageHtml("abcde").blockingGet())
}
@ -256,7 +234,7 @@ class MediaClientTest {
mockResponse.setParse(null)
`when`(mediaInterface!!.getPageHtml(ArgumentMatchers.anyString()))
.thenReturn(Observable.just(mockResponse))
.thenReturn(Single.just(mockResponse))
assertEquals("", mediaClient!!.getPageHtml("abcde").blockingGet())
}