#3529 Captions/depictions are not saved to Commons (#3588)

* #3529 Captions/depictions are not saved to Commons - update flow to update appropriate data

* #3529 Captions/depictions are not saved to Commons - fix invoking of setlabel

* #3529 Captions/depictions are not saved to Commons - fix unit tests

* #3529 Captions/depictions are not saved to Commons - use constant for @Named

* #3529 Captions/depictions are not saved to Commons - remove captions interface

* #3529 Captions/depictions are not saved to Commons - delete unused Contribution fields - enforce Single Responsibility by using PageContentsCreator

* #3529 Captions/depictions are not saved to Commons - prefix id with M - remove language from url and only add from Field

* #3529 Captions/depictions are not saved to Commons - make edits of depictions and captions sequential

* #3529 Captions/depictions are not saved to Commons - remove unused model fields

* #3529 Captions/depictions are not saved to Commons - weaken type of categories - copy list on Contribution creation

* #3529 Captions/depictions are not saved to Commons - mark Media fields private - weaken types - remove partly implemented fields

* #3529 Captions/depictions are not saved to Commons - add semi colon

* #3529 Captions/depictions are not saved to Commons - fix test
This commit is contained in:
Seán Mac Gillicuddy 2020-04-09 16:38:40 +01:00 committed by GitHub
parent 3f6d26c296
commit 628a6056e0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 925 additions and 1271 deletions

View file

@ -11,9 +11,7 @@ import fr.free.nrw.commons.utils.CommonsDateUtil;
import fr.free.nrw.commons.utils.MediaDataExtractorUtil; import fr.free.nrw.commons.utils.MediaDataExtractorUtil;
import java.text.ParseException; import java.text.ParseException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
@ -30,62 +28,42 @@ public class Media implements Parcelable {
// Primary metadata fields // Primary metadata fields
@Nullable @Nullable
public Uri localUri; private Uri localUri;
public String thumbUrl; private String thumbUrl;
public String imageUrl; private String imageUrl;
public String filename; private String filename;
public String thumbnailTitle; private String thumbnailTitle;
/** /*
* Captions are a feature part of Structured data. They are meant to store short, multilingual descriptions about files * 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) * 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 * Also now captions replace the previous convention of using title for filename
*/ */
private String caption; private String caption;
public String description; // monolingual description on input... private String description; // monolingual description on input...
public String discussion; private String discussion;
long dataLength; private long dataLength;
public Date dateCreated; private Date dateCreated;
@Nullable public Date dateUploaded; @Nullable private Date dateUploaded;
public int width; private String license;
public int height; private String licenseUrl;
public String license; private String creator;
public String licenseUrl;
public String creator;
/** /**
* Wikibase Identifier associated with media files * Wikibase Identifier associated with media files
*/ */
public String pageId; private String pageId;
public ArrayList<String> categories; // as loaded at runtime? 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. * 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 * However unlike categories depictions is multi-lingual
*/ */
public ArrayList<Map<String, String>> depictionList; private List<Map<String, String>> depictionList= new ArrayList<>();
/** private boolean requestedDeletion;
* The above hashmap is fetched from API and to diplay in Explore @Nullable private LatLng coordinates;
* However this list of depictions is for storing and retrieving depictions from local storage or cache
*/
public ArrayList<String> depictions;
public boolean requestedDeletion;
public HashMap<String, String> descriptions; // multilingual descriptions as loaded
/**
* 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>"
*/
public Map<String, String> captions;
public HashMap<String, String> tags = new HashMap<>();
@Nullable public LatLng coordinates;
/** /**
* Provides local constructor * Provides local constructor
*/ */
protected Media() { public Media() {
this.categories = new ArrayList<>();
this.depictions = new ArrayList<>();
this.descriptions = new HashMap<>();
this.captions = new HashMap<>();
} }
/** /**
@ -94,7 +72,6 @@ public class Media implements Parcelable {
* @param filename Media filename * @param filename Media filename
*/ */
public Media(String filename) { public Media(String filename) {
this();
this.filename = filename; this.filename = filename;
} }
@ -103,29 +80,35 @@ public class Media implements Parcelable {
* @param localUri Media URI * @param localUri Media URI
* @param imageUrl Media image URL * @param imageUrl Media image URL
* @param filename Media filename * @param filename Media filename
* @param captions Media captions
* @param description Media description * @param description Media description
* @param dataLength Media date length * @param dataLength Media date length
* @param dateCreated Media creation date * @param dateCreated Media creation date
* @param dateUploaded Media date uploaded * @param dateUploaded Media date uploaded
* @param creator Media creator * @param creator Media creator
*/ */
public Media(Uri localUri, String imageUrl, String filename, Map<String, String> captions, String description, public Media(Uri localUri, String imageUrl, String filename,
long dataLength, Date dateCreated, Date dateUploaded, String creator) { String description,
this(); long dataLength, Date dateCreated, Date dateUploaded, String creator) {
this.localUri = localUri; this.localUri = localUri;
this.thumbUrl = imageUrl; this.thumbUrl = imageUrl;
this.imageUrl = imageUrl; this.imageUrl = imageUrl;
this.filename = filename; this.filename = filename;
this.captions = captions;
this.description = description; this.description = description;
this.dataLength = dataLength; this.dataLength = dataLength;
this.dateCreated = dateCreated; this.dateCreated = dateCreated;
this.dateUploaded = dateUploaded; this.dateUploaded = dateUploaded;
this.creator = creator; this.creator = creator;
this.categories = new ArrayList<>(); }
this.depictions = new ArrayList<>();
this.descriptions = new HashMap<>(); public Media(Uri localUri, String filename,
String description, String creator, List<String> categories) {
this(localUri,null, filename,
description, -1, null, new Date(), creator);
this.categories = categories;
}
public Media(String title, Date date, String user) {
this(null, null, title, "", -1, date, date, user);
} }
/** /**
@ -145,7 +128,7 @@ public class Media implements Parcelable {
ExtMetadata metadata = imageInfo.getMetadata(); ExtMetadata metadata = imageInfo.getMetadata();
if (metadata == null) { if (metadata == null) {
Media media = new Media(null, imageInfo.getOriginalUrl(), Media media = new Media(null, imageInfo.getOriginalUrl(),
page.title(), new HashMap<>() , "", 0, null, null, null); page.title(), "", 0, null, null, null);
if (!StringUtils.isBlank(imageInfo.getThumbUrl())) { if (!StringUtils.isBlank(imageInfo.getThumbUrl())) {
media.setThumbUrl(imageInfo.getThumbUrl()); media.setThumbUrl(imageInfo.getThumbUrl());
} }
@ -155,8 +138,7 @@ public class Media implements Parcelable {
Media media = new Media(null, Media media = new Media(null,
imageInfo.getOriginalUrl(), imageInfo.getOriginalUrl(),
page.title(), page.title(),
new HashMap<>(), "",
"",
0, 0,
safeParseDate(metadata.dateTime()), safeParseDate(metadata.dateTime()),
safeParseDate(metadata.dateTime()), safeParseDate(metadata.dateTime()),
@ -174,7 +156,7 @@ public class Media implements Parcelable {
language = "default"; language = "default";
} }
media.setDescriptions(Collections.singletonMap(language, metadata.imageDescription())); media.setDescription(metadata.imageDescription());
media.setCategories(MediaDataExtractorUtil.extractCategoriesFromList(metadata.getCategories())); media.setCategories(MediaDataExtractorUtil.extractCategoriesFromList(metadata.getCategories()));
String latitude = metadata.getGpsLatitude(); String latitude = metadata.getGpsLatitude();
String longitude = metadata.getGpsLongitude(); String longitude = metadata.getGpsLongitude();
@ -212,31 +194,14 @@ public class Media implements Parcelable {
/** /**
*sets pageId for the current media object *sets pageId for the current media object
*/ */
private void setPageId(String pageId) { public void setPageId(String pageId) {
this.pageId = pageId; this.pageId = pageId;
} }
public String getThumbUrl() { public String getThumbUrl() {
return thumbUrl; return thumbUrl;
} }
/**
* Gets tag of media
* @param key Media key
* @return Media tag
*/
public Object getTag(String key) {
return tags.get(key);
}
/**
* Modifies( or creates a) tag of media
* @param key Media key
* @param value Media value
*/
public void setTag(String key, String value) {
tags.put(key, value);
}
/** /**
* Gets media display title * Gets media display title
* @return Media title * @return Media title
@ -340,22 +305,11 @@ public class Media implements Parcelable {
/** /**
* @return depictions associated with the current media * @return depictions associated with the current media
*/ */
public ArrayList<Map<String, String>> getDepiction() { public List<Map<String, String>> getDepiction() {
return depictionList; return depictionList;
} }
/**
* 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
*
* key of the HashMap is the language and value is the caption in the corresponding language
*
* returns list of captions stored in hashmap
*/
public Map<String, String> getCaptions() {
return captions;
}
/** /**
* Sets the file description. * Sets the file description.
@ -423,38 +377,6 @@ public class Media implements Parcelable {
this.creator = creator; this.creator = creator;
} }
/**
* Gets the width of the media.
* @return file width as an int
*/
public int getWidth() {
return width;
}
/**
* Sets the width of the media.
* @param width file width as an int
*/
public void setWidth(int width) {
this.width = width;
}
/**
* Gets the height of the media.
* @return file height as an int
*/
public int getHeight() {
return height;
}
/**
* Sets the height of the media.
* @param height file height as an int
*/
public void setHeight(int height) {
this.height = height;
}
/** /**
* Gets the license name of the file. * Gets the license name of the file.
* @return license as a String * @return license as a String
@ -506,15 +428,8 @@ public class Media implements Parcelable {
* @return file categories as an ArrayList of Strings * @return file categories as an ArrayList of Strings
*/ */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public ArrayList<String> getCategories() { public List<String> getCategories() {
return (ArrayList<String>) categories.clone(); // feels dirty return categories;
}
/**
* @return array list of depictions associated with the current media
*/
public ArrayList<String> getDepictions() {
return (ArrayList<String>) depictions.clone();
} }
/** /**
@ -525,43 +440,7 @@ public class Media implements Parcelable {
* @param categories file categories as a list of Strings * @param categories file categories as a list of Strings
*/ */
public void setCategories(List<String> categories) { public void setCategories(List<String> categories) {
this.categories.clear(); this.categories = categories;
this.categories.addAll(categories);
}
public void setDepictions(List<String> depictions) {
this.depictions.clear();
this.depictions.addAll(depictions);
}
/**
* Modifies (or sets) media descriptions
* @param descriptions Media descriptions
*/
void setDescriptions(Map<String, String> descriptions) {
this.descriptions.clear();
this.descriptions.putAll(descriptions);
}
/**
* Gets media description in preferred language
* @param preferredLanguage Language preferred
* @return Description in preferred language
*/
public String getDescription(String preferredLanguage) {
if (descriptions.containsKey(preferredLanguage)) {
// See if the requested language is there.
return descriptions.get(preferredLanguage);
} else if (descriptions.containsKey("en")) {
// Ah, English. Language of the world, until the Chinese crush us.
return descriptions.get("en");
} else if (descriptions.containsKey("default")) {
// No languages marked...
return descriptions.get("default");
} else {
// FIXME: return the first available non-English description?
return "";
}
} }
@Nullable private static Date safeParseDate(String dateStr) { @Nullable private static Date safeParseDate(String dateStr) {
@ -572,20 +451,19 @@ public class Media implements Parcelable {
} }
} }
/** /**
* Set requested deletion to true * Set requested deletion to true
* @param requestedDeletion
*/ */
public void setRequestedDeletion(){ public void setRequestedDeletion(boolean requestedDeletion){
requestedDeletion = true; this.requestedDeletion = requestedDeletion;
} }
/** /**
* Get the value of requested deletion * Get the value of requested deletion
* @return boolean requestedDeletion * @return boolean requestedDeletion
*/ */
public boolean getRequestedDeletion(){ public boolean isRequestedDeletion(){
return requestedDeletion; return requestedDeletion;
} }
@ -610,15 +488,29 @@ public class Media implements Parcelable {
this.caption = caption; this.caption = caption;
} }
public void setCaptions(HashMap<String, String> captions) { /* Sets depictions for the current media obtained fro Wikibase API*/
this.captions = captions; public void setDepictionList(List<Map<String, String>> depictions) {
this.depictionList = depictions;
} }
/** public void setLocalUri(@Nullable final Uri localUri) {
* Sets depictions for the current media obtained fro Wikibase API this.localUri = localUri;
*/ }
public void setDepiction(ArrayList<Map<String, String>> depictions) {
this.depictionList = depictions; public void setImageUrl(final String imageUrl) {
this.imageUrl = imageUrl;
}
public void setDateUploaded(@Nullable final Date dateUploaded) {
this.dateUploaded = dateUploaded;
}
public void setLicenseUrl(final String licenseUrl) {
this.licenseUrl = licenseUrl;
}
public List<Map<String, String>> getDepictionList() {
return depictionList;
} }
@Override @Override
@ -645,8 +537,6 @@ public class Media implements Parcelable {
dest.writeLong(this.dataLength); dest.writeLong(this.dataLength);
dest.writeLong(this.dateCreated != null ? this.dateCreated.getTime() : -1); dest.writeLong(this.dateCreated != null ? this.dateCreated.getTime() : -1);
dest.writeLong(this.dateUploaded != null ? this.dateUploaded.getTime() : -1); dest.writeLong(this.dateUploaded != null ? this.dateUploaded.getTime() : -1);
dest.writeInt(this.width);
dest.writeInt(this.height);
dest.writeString(this.license); dest.writeString(this.license);
dest.writeString(this.licenseUrl); dest.writeString(this.licenseUrl);
dest.writeString(this.creator); dest.writeString(this.creator);
@ -654,8 +544,6 @@ public class Media implements Parcelable {
dest.writeStringList(this.categories); dest.writeStringList(this.categories);
dest.writeList(this.depictionList); dest.writeList(this.depictionList);
dest.writeByte(this.requestedDeletion ? (byte) 1 : (byte) 0); dest.writeByte(this.requestedDeletion ? (byte) 1 : (byte) 0);
dest.writeSerializable(this.descriptions);
dest.writeSerializable(this.tags);
dest.writeParcelable(this.coordinates, flags); dest.writeParcelable(this.coordinates, flags);
} }
@ -673,8 +561,6 @@ public class Media implements Parcelable {
this.dateCreated = tmpDateCreated == -1 ? null : new Date(tmpDateCreated); this.dateCreated = tmpDateCreated == -1 ? null : new Date(tmpDateCreated);
long tmpDateUploaded = in.readLong(); long tmpDateUploaded = in.readLong();
this.dateUploaded = tmpDateUploaded == -1 ? null : new Date(tmpDateUploaded); this.dateUploaded = tmpDateUploaded == -1 ? null : new Date(tmpDateUploaded);
this.width = in.readInt();
this.height = in.readInt();
this.license = in.readString(); this.license = in.readString();
this.licenseUrl = in.readString(); this.licenseUrl = in.readString();
this.creator = in.readString(); this.creator = in.readString();
@ -684,8 +570,6 @@ public class Media implements Parcelable {
this.categories=list; this.categories=list;
in.readList(depictionList,null); in.readList(depictionList,null);
this.requestedDeletion = in.readByte() != 0; this.requestedDeletion = in.readByte() != 0;
this.descriptions = (HashMap<String, String>) in.readSerializable();
this.tags = (HashMap<String, String>) in.readSerializable();
this.coordinates = in.readParcelable(LatLng.class.getClassLoader()); this.coordinates = in.readParcelable(LatLng.class.getClassLoader());
} }

View file

@ -55,9 +55,9 @@ public class MediaDataExtractor {
final String caption, final JsonObject depiction) { final String caption, final JsonObject depiction) {
media.setDiscussion(discussion); media.setDiscussion(discussion);
media.setCaption(caption); media.setCaption(caption);
media.setDepiction(formatDepictions(depiction)); media.setDepictionList(formatDepictions(depiction));
if (deletionStatus) { if (deletionStatus) {
media.setRequestedDeletion(); media.setRequestedDeletion(true);
} }
return media; return media;
} }

View file

@ -1,34 +1,22 @@
package fr.free.nrw.commons.contributions; package fr.free.nrw.commons.contributions;
import static java.lang.annotation.RetentionPolicy.SOURCE;
import android.content.Context;
import android.net.Uri;
import android.os.Parcel; import android.os.Parcel;
import androidx.annotation.NonNull;
import androidx.annotation.StringDef;
import androidx.room.Entity; import androidx.room.Entity;
import androidx.room.PrimaryKey; import androidx.room.PrimaryKey;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.Media; import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.filepicker.UploadableFile; import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.settings.Prefs; import fr.free.nrw.commons.upload.UploadMediaDetail;
import fr.free.nrw.commons.utils.ConfigUtils; import fr.free.nrw.commons.upload.UploadModel.UploadItem;
import java.lang.annotation.Retention; import fr.free.nrw.commons.upload.WikidataPlace;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.HashMap;
import java.util.Locale; import java.util.List;
import java.util.Map; import java.util.Map;
import org.apache.commons.lang3.StringUtils; import org.wikipedia.dataclient.mwapi.MwQueryLogEvent;
@Entity(tableName = "contribution") @Entity(tableName = "contribution")
public class Contribution extends Media { public class Contribution extends Media {
//{{According to Exif data|2009-01-09}}
private static final String TEMPLATE_DATE_ACC_TO_EXIF = "{{According to Exif data|%s}}";
//2009-01-09 9 January 2009
private static final String TEMPLATE_DATA_OTHER_SOURCE = "%s";
// No need to be bitwise - they're mutually exclusive // No need to be bitwise - they're mutually exclusive
public static final int STATE_COMPLETED = -1; public static final int STATE_COMPLETED = -1;
@ -36,256 +24,133 @@ public class Contribution extends Media {
public static final int STATE_QUEUED = 2; public static final int STATE_QUEUED = 2;
public static final int STATE_IN_PROGRESS = 3; public static final int STATE_IN_PROGRESS = 3;
@Retention(SOURCE)
@StringDef({SOURCE_CAMERA, SOURCE_GALLERY, SOURCE_EXTERNAL})
public @interface FileSource {}
public static final String SOURCE_CAMERA = "camera";
public static final String SOURCE_GALLERY = "gallery";
public static final String SOURCE_EXTERNAL = "external";
@PrimaryKey (autoGenerate = true) @PrimaryKey (autoGenerate = true)
@NonNull private long _id;
public long _id; private int state;
public Uri contentUri; private long transferred;
public String source; private String decimalCoords;
public String editSummary; private String dateCreatedSource;
public int state; private WikidataPlace wikidataPlace;
public long transferred;
public String decimalCoords;
public boolean isMultiple;
public String wikiDataEntityId;
public String wikiItemName;
private String p18Value;
public Uri contentProviderUri;
public String dateCreatedSource;
/** /**
* Each depiction loaded in depictions activity is associated with a wikidata entity id, * Each depiction loaded in depictions activity is associated with a wikidata entity id,
* this Id is in turn used to upload depictions to wikibase * this Id is in turn used to upload depictions to wikibase
*/ */
public ArrayList<String> depictionsEntityIds; 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<>();
public Contribution(Uri contentUri, String filename, Uri localUri, String imageUrl, Date dateCreated, public Contribution() {
int state, long dataLength, Date dateUploaded, long transferred,
String source, Map<String, String> captions, String description, String creator, boolean isMultiple,
int width, int height, String license) {
super(localUri, imageUrl, filename, captions, description, dataLength, dateCreated, dateUploaded, creator);
this.contentUri = contentUri;
this.state = state;
this.transferred = transferred;
this.source = source;
this.isMultiple = isMultiple;
this.width = width;
this.height = height;
this.license = license;
this.dateCreatedSource = "";
} }
public Contribution(Uri localUri, String imageUrl, String filename, Map<String, String> captions, String description, long dataLength, public Contribution(final UploadItem item, final SessionManager sessionManager,
Date dateCreated, Date dateUploaded, String creator, String editSummary, ArrayList<String> depictionsEntityIds, String decimalCoords) { final List<DepictedItem> depictedItems, final List<String> categories) {
super(localUri, imageUrl, filename, captions, description, dataLength, dateCreated, dateUploaded, creator); super(item.getMediaUri(),
this.decimalCoords = decimalCoords; item.getFileName(),
this.editSummary = editSummary; UploadMediaDetail.formatList(item.getUploadMediaDetails()),
this.dateCreatedSource = ""; sessionManager.getAuthorName(),
this.depictionsEntityIds = depictionsEntityIds; categories);
captions = UploadMediaDetail.formatCaptions(item.getUploadMediaDetails());
decimalCoords = item.getGpsCoords().getDecimalCoords();
dateCreatedSource = "";
this.depictedItems = depictedItems;
wikidataPlace = WikidataPlace.from(item.getPlace());
} }
public Contribution(Uri localUri, String imageUrl, String filename, Map<String, String> captions, String description, long dataLength, public Contribution(final MwQueryLogEvent queryLogEvent, final String user) {
Date dateCreated, Date dateUploaded, String creator, String editSummary, String decimalCoords, int state) { super(queryLogEvent.title(),queryLogEvent.date(), user);
super(localUri, imageUrl, filename, captions, description, dataLength, dateCreated, dateUploaded, creator); decimalCoords = "";
this.decimalCoords = decimalCoords; dateCreatedSource = "";
this.editSummary = editSummary; state = STATE_COMPLETED;
this.dateCreatedSource = "";
this.state=state;
} }
public void setDateCreatedSource(final String dateCreatedSource) {
public void setDateCreatedSource(String dateCreatedSource) {
this.dateCreatedSource = dateCreatedSource; this.dateCreatedSource = dateCreatedSource;
} }
public boolean getMultiple() { public String getDateCreatedSource() {
return isMultiple; return dateCreatedSource;
}
public void setMultiple(boolean multiple) {
isMultiple = multiple;
} }
public long getTransferred() { public long getTransferred() {
return transferred; return transferred;
} }
public void setTransferred(long transferred) { public void setTransferred(final long transferred) {
this.transferred = transferred; this.transferred = transferred;
} }
public String getEditSummary() {
return editSummary != null ? editSummary : CommonsApplication.DEFAULT_EDIT_SUMMARY;
}
public Uri getContentUri() {
return contentUri;
}
public void setContentUri(Uri contentUri) {
this.contentUri = contentUri;
}
public int getState() { public int getState() {
return state; return state;
} }
public void setState(int state) { public void setState(final int state) {
this.state = state; this.state = state;
} }
public void setDateUploaded(Date date) {
this.dateUploaded = date;
}
/**
* sets depiction entity ids for the given contribution
*/
public void setDepictions(ArrayList<String> depictionsEntityIds) {
this.depictionsEntityIds = depictionsEntityIds;
}
public String getPageContents(Context applicationContext) {
StringBuilder buffer = new StringBuilder();
buffer
.append("== {{int:filedesc}} ==\n")
.append("{{Information\n")
.append("|description=").append(getDescription()).append("\n")
.append("|source=").append("{{own}}\n")
.append("|author=[[User:").append(creator).append("|").append(creator).append("]]\n");
String templatizedCreatedDate = getTemplatizedCreatedDate();
if (!StringUtils.isBlank(templatizedCreatedDate)) {
buffer.append("|date=").append(templatizedCreatedDate);
}
buffer.append("}}").append("\n");
//Only add Location template (e.g. {{Location|37.51136|-77.602615}} ) if coords is not null
if (decimalCoords != null) {
buffer.append("{{Location|").append(decimalCoords).append("}}").append("\n");
}
buffer.append("== {{int:license-header}} ==\n")
.append(licenseTemplateFor(getLicense())).append("\n\n")
.append("{{Uploaded from Mobile|platform=Android|version=")
.append(ConfigUtils.getVersionNameWithSha(applicationContext)).append("}}\n");
if(categories!=null&&categories.size()!=0) {
for (int i = 0; i < categories.size(); i++) {
String category = categories.get(i);
buffer.append("\n[[Category:").append(category).append("]]");
}
}
else
buffer.append("{{subst:unc}}");
return buffer.toString();
}
/**
* Returns upload date in either TEMPLATE_DATE_ACC_TO_EXIF or TEMPLATE_DATA_OTHER_SOURCE
* @return
*/
private String getTemplatizedCreatedDate() {
if (dateCreated != null) {
java.text.SimpleDateFormat dateFormat = new java.text.SimpleDateFormat("yyyy-MM-dd");
if (UploadableFile.DateTimeWithSource.EXIF_SOURCE.equals(dateCreatedSource)) {
return String.format(Locale.ENGLISH, TEMPLATE_DATE_ACC_TO_EXIF, dateFormat.format(dateCreated)) + "\n";
} else {
return String.format(Locale.ENGLISH, TEMPLATE_DATA_OTHER_SOURCE, dateFormat.format(dateCreated)) + "\n";
}
}
return "";
}
@Override
public void setFilename(String filename) {
this.filename = filename;
}
public void setImageUrl(String imageUrl) {
this.imageUrl = imageUrl;
}
public Contribution() {
}
public String getSource() {
return source;
}
public void setSource(String source) {
this.source = source;
}
@NonNull
private String licenseTemplateFor(String license) {
switch (license) {
case Prefs.Licenses.CC_BY_3:
return "{{self|cc-by-3.0}}";
case Prefs.Licenses.CC_BY_4:
return "{{self|cc-by-4.0}}";
case Prefs.Licenses.CC_BY_SA_3:
return "{{self|cc-by-sa-3.0}}";
case Prefs.Licenses.CC_BY_SA_4:
return "{{self|cc-by-sa-4.0}}";
case Prefs.Licenses.CC0:
return "{{self|cc-zero}}";
}
throw new RuntimeException("Unrecognized license value: " + license);
}
public String getWikiDataEntityId() {
return wikiDataEntityId;
}
public String getWikiItemName() {
return wikiItemName;
}
/**
* When the corresponding wikidata entity is known as in case of nearby uploads, it can be set
* using the setter method
* @param wikiDataEntityId wikiDataEntityId
*/
public void setWikiDataEntityId(String wikiDataEntityId) {
this.wikiDataEntityId = wikiDataEntityId;
}
public void setWikiItemName(String wikiItemName) {
this.wikiItemName = wikiItemName;
}
public String getP18Value() {
return p18Value;
}
/**
* When the corresponding image property of wiki entity is known as in case of nearby uploads,
* it can be set using the setter method
* @param p18Value p18 value, image property of the wikidata item
*/
public void setP18Value(String p18Value) {
this.p18Value = p18Value;
}
public void setContentProviderUri(Uri contentProviderUri) {
this.contentProviderUri = contentProviderUri;
}
/** /**
* @return array list of entityids for the depictions * @return array list of entityids for the depictions
*/ */
public ArrayList<String> getDepictionsEntityIds() { public List<DepictedItem> getDepictedItems() {
return depictionsEntityIds; return depictedItems;
}
public void setWikidataPlace(final WikidataPlace wikidataPlace) {
this.wikidataPlace = wikidataPlace;
}
public WikidataPlace getWikidataPlace() {
return wikidataPlace;
}
public long get_id() {
return _id;
}
public void set_id(final long _id) {
this._id = _id;
}
public String getDecimalCoords() {
return decimalCoords;
}
public void setDecimalCoords(final String decimalCoords) {
this.decimalCoords = decimalCoords;
}
public void setDepictedItems(final List<DepictedItem> depictedItems) {
this.depictedItems = depictedItems;
}
public void setMimeType(String mimeType) {
this.mimeType = mimeType;
}
public String getMimeType() {
return 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
*
* key of the HashMap is the language and value is the caption in the corresponding language
*
* 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 @Override
@ -294,48 +159,34 @@ public class Contribution extends Media {
} }
@Override @Override
public void writeToParcel(Parcel dest, int flags) { public void writeToParcel(final Parcel dest, final int flags) {
super.writeToParcel(dest, flags); super.writeToParcel(dest, flags);
dest.writeLong(this._id); dest.writeLong(_id);
dest.writeParcelable(this.contentUri, flags); dest.writeInt(state);
dest.writeString(this.source); dest.writeLong(transferred);
dest.writeString(this.editSummary); dest.writeString(decimalCoords);
dest.writeInt(this.state); dest.writeString(dateCreatedSource);
dest.writeLong(this.transferred); dest.writeSerializable((HashMap) captions);
dest.writeString(this.decimalCoords);
dest.writeByte(this.isMultiple ? (byte) 1 : (byte) 0);
dest.writeString(this.wikiDataEntityId);
dest.writeString(this.wikiItemName);
dest.writeString(this.p18Value);
dest.writeParcelable(this.contentProviderUri, flags);
dest.writeString(this.dateCreatedSource);
} }
protected Contribution(Parcel in) { protected Contribution(final Parcel in) {
super(in); super(in);
this._id = in.readLong(); _id = in.readLong();
this.contentUri = in.readParcelable(Uri.class.getClassLoader()); state = in.readInt();
this.source = in.readString(); transferred = in.readLong();
this.editSummary = in.readString(); decimalCoords = in.readString();
this.state = in.readInt(); dateCreatedSource = in.readString();
this.transferred = in.readLong(); captions = (HashMap<String, String>) in.readSerializable();
this.decimalCoords = in.readString();
this.isMultiple = in.readByte() != 0;
this.wikiDataEntityId = in.readString();
this.wikiItemName = in.readString();
this.p18Value = in.readString();
this.contentProviderUri = in.readParcelable(Uri.class.getClassLoader());
this.dateCreatedSource = in.readString();
} }
public static final Creator<Contribution> CREATOR = new Creator<Contribution>() { public static final Creator<Contribution> CREATOR = new Creator<Contribution>() {
@Override @Override
public Contribution createFromParcel(Parcel source) { public Contribution createFromParcel(final Parcel source) {
return new Contribution(source); return new Contribution(source);
} }
@Override @Override
public Contribution[] newArray(int size) { public Contribution[] newArray(final int size) {
return new Contribution[size]; return new Contribution[size];
} }
}; };

View file

@ -1,19 +1,13 @@
package fr.free.nrw.commons.contributions; package fr.free.nrw.commons.contributions;
import static fr.free.nrw.commons.upload.UploadService.EXTRA_FILES;
import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT;
import android.Manifest; import android.Manifest;
import android.app.Activity; import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.filepicker.DefaultCallback; import fr.free.nrw.commons.filepicker.DefaultCallback;
import fr.free.nrw.commons.filepicker.FilePicker; import fr.free.nrw.commons.filepicker.FilePicker;
@ -23,12 +17,11 @@ import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.upload.UploadActivity; import fr.free.nrw.commons.upload.UploadActivity;
import fr.free.nrw.commons.utils.PermissionUtils; import fr.free.nrw.commons.utils.PermissionUtils;
import fr.free.nrw.commons.utils.ViewUtil; import fr.free.nrw.commons.utils.ViewUtil;
import java.util.ArrayList;
import static fr.free.nrw.commons.contributions.Contribution.SOURCE_CAMERA; import java.util.List;
import static fr.free.nrw.commons.contributions.Contribution.SOURCE_GALLERY; import javax.inject.Inject;
import static fr.free.nrw.commons.upload.UploadService.EXTRA_FILES; import javax.inject.Named;
import static fr.free.nrw.commons.upload.UploadService.EXTRA_SOURCE; import javax.inject.Singleton;
import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT;
@Singleton @Singleton
public class ContributionController { public class ContributionController {
@ -109,7 +102,7 @@ public class ContributionController {
@Override @Override
public void onImagesPicked(@NonNull List<UploadableFile> imagesFiles, FilePicker.ImageSource source, int type) { public void onImagesPicked(@NonNull List<UploadableFile> imagesFiles, FilePicker.ImageSource source, int type) {
Intent intent = handleImagesPicked(activity, imagesFiles, getSourceFromImageSource(source)); Intent intent = handleImagesPicked(activity, imagesFiles);
activity.startActivity(intent); activity.startActivity(intent);
} }
}); });
@ -125,11 +118,9 @@ public class ContributionController {
* Attaches place object for nearby uploads * Attaches place object for nearby uploads
*/ */
private Intent handleImagesPicked(Context context, private Intent handleImagesPicked(Context context,
List<UploadableFile> imagesFiles, List<UploadableFile> imagesFiles) {
String source) {
Intent shareIntent = new Intent(context, UploadActivity.class); Intent shareIntent = new Intent(context, UploadActivity.class);
shareIntent.setAction(ACTION_INTERNAL_UPLOADS); shareIntent.setAction(ACTION_INTERNAL_UPLOADS);
shareIntent.putExtra(EXTRA_SOURCE, source);
shareIntent.putParcelableArrayListExtra(EXTRA_FILES, new ArrayList<>(imagesFiles)); shareIntent.putParcelableArrayListExtra(EXTRA_FILES, new ArrayList<>(imagesFiles));
Place place = defaultKvStore.getJson(PLACE_OBJECT, Place.class); Place place = defaultKvStore.getJson(PLACE_OBJECT, Place.class);
if (place != null) { if (place != null) {
@ -139,13 +130,4 @@ public class ContributionController {
return shareIntent; return shareIntent;
} }
/**
* Get image upload source
*/
private String getSourceFromImageSource(FilePicker.ImageSource source) {
if (source.equals(FilePicker.ImageSource.CAMERA_IMAGE)) {
return SOURCE_CAMERA;
}
return SOURCE_GALLERY;
}
} }

View file

@ -7,13 +7,10 @@ import androidx.room.Insert;
import androidx.room.OnConflictStrategy; import androidx.room.OnConflictStrategy;
import androidx.room.Query; import androidx.room.Query;
import androidx.room.Transaction; import androidx.room.Transaction;
import androidx.room.Update; import androidx.room.Update;
import io.reactivex.disposables.Disposable;
import java.util.List;
import io.reactivex.Completable; import io.reactivex.Completable;
import io.reactivex.Single; import io.reactivex.Single;
import java.util.List;
@Dao @Dao
public abstract class ContributionDao { public abstract class ContributionDao {
@ -40,9 +37,6 @@ public abstract class ContributionDao {
@Delete @Delete
public abstract Single<Integer> delete(Contribution contribution); public abstract Single<Integer> delete(Contribution contribution);
@Query("SELECT * from contribution WHERE contentProviderUri=:uri")
public abstract List<Contribution> getContributionWithUri(String uri);
@Query("SELECT * from contribution WHERE filename=:fileName") @Query("SELECT * from contribution WHERE filename=:fileName")
public abstract List<Contribution> getContributionWithTitle(String fileName); public abstract List<Contribution> getContributionWithTitle(String fileName);

View file

@ -2,8 +2,6 @@ package fr.free.nrw.commons.contributions;
import static fr.free.nrw.commons.depictions.Media.DepictedImagesFragment.PAGE_ID_PREFIX; import static fr.free.nrw.commons.depictions.Media.DepictedImagesFragment.PAGE_ID_PREFIX;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.net.Uri; import android.net.Uri;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.View; import android.view.View;
@ -60,7 +58,7 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
this.contribution = contribution; this.contribution = contribution;
fetchAndDisplayCaption(contribution); fetchAndDisplayCaption(contribution);
this.position = position; this.position = position;
String imageSource = chooseImageSource(contribution.thumbUrl, contribution.getLocalUri()); String imageSource = chooseImageSource(contribution.getThumbUrl(), contribution.getLocalUri());
if (!TextUtils.isEmpty(imageSource)) { if (!TextUtils.isEmpty(imageSource)) {
final ImageRequest imageRequest = final ImageRequest imageRequest =
ImageRequestBuilder.newBuilderWithSource(Uri.parse(imageSource)) ImageRequestBuilder.newBuilderWithSource(Uri.parse(imageSource))

View file

@ -1,5 +1,9 @@
package fr.free.nrw.commons.contributions; package fr.free.nrw.commons.contributions;
import static fr.free.nrw.commons.contributions.Contribution.STATE_FAILED;
import static fr.free.nrw.commons.contributions.MainActivity.CONTRIBUTIONS_TAB_POSITION;
import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween;
import android.Manifest; import android.Manifest;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; import android.content.Context;
@ -12,21 +16,12 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.CheckBox; import android.widget.CheckBox;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentManager.OnBackStackChangedListener; import androidx.fragment.app.FragmentManager.OnBackStackChangedListener;
import androidx.fragment.app.FragmentTransaction; import androidx.fragment.app.FragmentTransaction;
import fr.free.nrw.commons.MediaDataExtractor;
import io.reactivex.disposables.Disposable;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import butterknife.BindView; import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import fr.free.nrw.commons.HandlerService; import fr.free.nrw.commons.HandlerService;
@ -43,7 +38,6 @@ import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.location.LocationServiceManager; import fr.free.nrw.commons.location.LocationServiceManager;
import fr.free.nrw.commons.location.LocationUpdateListener; import fr.free.nrw.commons.location.LocationUpdateListener;
import fr.free.nrw.commons.media.MediaClient;
import fr.free.nrw.commons.media.MediaDetailPagerFragment; import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.media.MediaDetailPagerFragment.MediaDetailProvider; import fr.free.nrw.commons.media.MediaDetailPagerFragment.MediaDetailProvider;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
@ -61,14 +55,11 @@ import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import java.util.ArrayList; import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import timber.log.Timber; import timber.log.Timber;
import static fr.free.nrw.commons.contributions.Contribution.STATE_FAILED;
import static fr.free.nrw.commons.contributions.MainActivity.CONTRIBUTIONS_TAB_POSITION;
import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween;
public class ContributionsFragment public class ContributionsFragment
extends CommonsDaggerSupportFragment extends CommonsDaggerSupportFragment
implements implements
@ -224,7 +215,7 @@ public class ContributionsFragment
@Override @Override
public void fetchMediaUriFor(Contribution contribution) { public void fetchMediaUriFor(Contribution contribution) {
Timber.d("Fetching thumbnail for %s", contribution.filename); Timber.d("Fetching thumbnail for %s", contribution.getFilename());
contributionsPresenter.fetchMediaDetails(contribution); contributionsPresenter.fetchMediaDetails(contribution);
} }
}); });

View file

@ -62,7 +62,7 @@ public class ContributionsListAdapter extends RecyclerView.Adapter<ContributionV
@Override @Override
public long getItemId(int position) { public long getItemId(int position) {
return contributions.get(position)._id; return contributions.get(position).get_id();
} }
public interface Callback { public interface Callback {

View file

@ -1,46 +1,30 @@
package fr.free.nrw.commons.contributions; package fr.free.nrw.commons.contributions;
import android.content.Context; import android.content.Context;
import android.database.Cursor;
import android.database.DataSetObserver;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData; import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer;
import fr.free.nrw.commons.MediaDataExtractor;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.Media; import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.MediaDataExtractor;
import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.contributions.ContributionsContract.UserActionListener; import fr.free.nrw.commons.contributions.ContributionsContract.UserActionListener;
import fr.free.nrw.commons.db.AppDatabase; import fr.free.nrw.commons.db.AppDatabase;
import fr.free.nrw.commons.di.CommonsApplicationModule; import fr.free.nrw.commons.di.CommonsApplicationModule;
import fr.free.nrw.commons.mwapi.UserClient; import fr.free.nrw.commons.mwapi.UserClient;
import fr.free.nrw.commons.utils.ExecutorUtils;
import fr.free.nrw.commons.utils.NetworkUtils; import fr.free.nrw.commons.utils.NetworkUtils;
import io.reactivex.Observable;
import io.reactivex.Scheduler; import io.reactivex.Scheduler;
import io.reactivex.SingleObserver;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers; 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 timber.log.Timber;
import static fr.free.nrw.commons.contributions.Contribution.STATE_COMPLETED;
/** /**
* The presenter class for Contributions * The presenter class for Contributions
*/ */
@ -106,12 +90,7 @@ public class ContributionsPresenter implements UserActionListener {
.observeOn(mainThreadScheduler) .observeOn(mainThreadScheduler)
.doOnNext(mwQueryLogEvent -> Timber.d("Received image %s", mwQueryLogEvent.title())) .doOnNext(mwQueryLogEvent -> Timber.d("Received image %s", mwQueryLogEvent.title()))
.filter(mwQueryLogEvent -> !mwQueryLogEvent.isDeleted()).doOnNext(mwQueryLogEvent -> Timber.d("Image %s passed filters", mwQueryLogEvent.title())) .filter(mwQueryLogEvent -> !mwQueryLogEvent.isDeleted()).doOnNext(mwQueryLogEvent -> Timber.d("Image %s passed filters", mwQueryLogEvent.title()))
.map(image -> { .map(image -> new Contribution(image, user))
Contribution contribution = new Contribution(null, null, image.title(),
new HashMap<>(), "", -1, image.date(), image.date(), user,
"", "", STATE_COMPLETED);
return contribution;
})
.toList() .toList()
.subscribe(this::saveContributionsToDB, error -> { .subscribe(this::saveContributionsToDB, error -> {
Timber.e("Failed to fetch contributions: %s", error.getMessage()); Timber.e("Failed to fetch contributions: %s", error.getMessage());
@ -198,11 +177,11 @@ public class ContributionsPresenter implements UserActionListener {
@Override @Override
public void fetchMediaDetails(Contribution contribution) { public void fetchMediaDetails(Contribution contribution) {
compositeDisposable.add(mediaDataExtractor compositeDisposable.add(mediaDataExtractor
.getMediaFromFileName(contribution.filename) .getMediaFromFileName(contribution.getFilename())
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(media -> { .subscribe(media -> {
contribution.thumbUrl=media.thumbUrl; contribution.setThumbUrl(media.getThumbUrl());
updateContribution(contribution); updateContribution(contribution);
})); }));
} }

View file

@ -7,9 +7,10 @@ import com.google.gson.reflect.TypeToken;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.di.ApplicationlessInjection; import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.location.LatLng;
import java.util.ArrayList; import fr.free.nrw.commons.upload.WikidataPlace;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.List;
import java.util.Map; import java.util.Map;
/** /**
@ -42,53 +43,74 @@ public class Converters {
} }
@TypeConverter @TypeConverter
public static String listObjectToString(ArrayList<String> objectList) { public static String listObjectToString(List<String> objectList) {
return objectList == null ? null : getGson().toJson(objectList); return writeObjectToString(objectList);
} }
@TypeConverter @TypeConverter
public static ArrayList<String> stringToArrayListObject(String objectList) { public static List<String> stringToListObject(String objectList) {
return objectList == null ? null : getGson().fromJson(objectList,new TypeToken<ArrayList<String>>(){}.getType()); return readObjectWithTypeToken(objectList, new TypeToken<List<String>>() {});
} }
@TypeConverter @TypeConverter
public static String mapObjectToString(HashMap<String,String> objectList) { public static String mapObjectToString(Map<String,String> objectList) {
return objectList == null ? null : getGson().toJson(objectList); return writeObjectToString(objectList);
} }
@TypeConverter @TypeConverter
public static HashMap<String,String> stringToHashMap(String objectList) { public static Map<String,String> stringToMap(String objectList) {
return objectList == null ? null : getGson().fromJson(objectList,new TypeToken<HashMap<String,String>>(){}.getType()); return readObjectWithTypeToken(objectList, new TypeToken<Map<String,String>>(){});
} }
@TypeConverter @TypeConverter
public static String latlngObjectToString(LatLng latlng) { public static String latlngObjectToString(LatLng latlng) {
return latlng == null ? null : getGson().toJson(latlng); return writeObjectToString(latlng);
} }
@TypeConverter @TypeConverter
public static LatLng stringToLatLng(String objectList) { public static LatLng stringToLatLng(String objectList) {
return objectList == null ? null : getGson().fromJson(objectList,LatLng.class); return readObjectFromString(objectList,LatLng.class);
} }
@TypeConverter @TypeConverter
public static String listOfMapToString(ArrayList<Map<String,String>> listOfMaps) { public static String listOfMapToString(List<Map<String,String>> listOfMaps) {
return listOfMaps == null ? null : getGson().toJson(listOfMaps); return writeObjectToString(listOfMaps);
} }
@TypeConverter @TypeConverter
public static ArrayList<Map<String,String>> stringToListOfMap(String listOfMaps) { public static List<Map<String,String>> stringToListOfMap(String listOfMaps) {
return listOfMaps == null ? null :getGson().fromJson(listOfMaps,new TypeToken<ArrayList<Map<String,String>>>(){}.getType()); return readObjectWithTypeToken(listOfMaps, new TypeToken<List<Map<String, String>>>() {});
} }
@TypeConverter @TypeConverter
public static String mapToString(Map<String,String> map) { public static String wikidataPlaceToString(WikidataPlace wikidataPlace) {
return map == null ? null : getGson().toJson(map); return writeObjectToString(wikidataPlace);
} }
@TypeConverter @TypeConverter
public static Map<String,String> stringToMap(String map) { public static WikidataPlace stringToWikidataPlace(String wikidataPlace) {
return map == null ? null :getGson().fromJson(map,new TypeToken<Map<String,String>>(){}.getType()); return readObjectFromString(wikidataPlace, WikidataPlace.class);
} }
@TypeConverter
public static String depictionListToString(List<DepictedItem> depictedItems) {
return writeObjectToString(depictedItems);
}
@TypeConverter
public static List<DepictedItem> stringToList(String depictedItems) {
return readObjectWithTypeToken(depictedItems, new TypeToken<List<DepictedItem>>() {});
}
private static String writeObjectToString(Object object) {
return object == null ? null : getGson().toJson(object);
}
private static<T> T readObjectFromString(String objectAsString, Class<T> clazz) {
return objectAsString == null ? null : getGson().fromJson(objectAsString, clazz);
}
private static <T> T readObjectWithTypeToken(String objectList, TypeToken<T> typeToken) {
return objectList == null ? null : getGson().fromJson(objectList, typeToken.getType());
}
} }

View file

@ -1,22 +1,12 @@
package fr.free.nrw.commons.delete; package fr.free.nrw.commons.delete;
import static fr.free.nrw.commons.notification.NotificationHelper.NOTIFICATION_DELETE;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Locale;
import java.util.concurrent.Callable;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.Media; import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
@ -29,10 +19,16 @@ import io.reactivex.Single;
import io.reactivex.SingleSource; import io.reactivex.SingleSource;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Locale;
import java.util.concurrent.Callable;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import timber.log.Timber; import timber.log.Timber;
import static fr.free.nrw.commons.notification.NotificationHelper.NOTIFICATION_DELETE;
/** /**
* Refactored async task to Rx * Refactored async task to Rx
*/ */
@ -104,7 +100,7 @@ public class DeleteHelper {
} }
String creatorName = creator.replace(" (page does not exist)", ""); String creatorName = creator.replace(" (page does not exist)", "");
return pageEditClient.prependEdit(media.filename, fileDeleteString + "\n", summary) return pageEditClient.prependEdit(media.getFilename(), fileDeleteString + "\n", summary)
.flatMap(result -> { .flatMap(result -> {
if (result) { if (result) {
return pageEditClient.edit("Commons:Deletion_requests/" + media.getFilename(), subpageString + "\n", summary); return pageEditClient.edit("Commons:Deletion_requests/" + media.getFilename(), subpageString + "\n", summary);

View file

@ -141,7 +141,7 @@ public class SubDepictionListPresenter implements SubDepictionListContract.UserA
this.queryList.addAll(mediaList); this.queryList.addAll(mediaList);
view.onSuccess(mediaList); view.onSuccess(mediaList);
for (DepictedItem m : mediaList) { for (DepictedItem m : mediaList) {
fetchThumbnailForEntityId(m.getEntityId(), size++); fetchThumbnailForEntityId(m.getId(), size++);
} }
} }
} }

View file

@ -197,8 +197,8 @@ public class WikidataItemDetailsActivity extends NavigationBaseActivity implemen
public static void startYourself(Context context, DepictedItem depictedItem) { public static void startYourself(Context context, DepictedItem depictedItem) {
Intent intent = new Intent(context, WikidataItemDetailsActivity.class); Intent intent = new Intent(context, WikidataItemDetailsActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_SINGLE_TOP); intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_SINGLE_TOP);
intent.putExtra("wikidataItemName", depictedItem.getDepictsLabel()); intent.putExtra("wikidataItemName", depictedItem.getName());
intent.putExtra("entityId", depictedItem.getEntityId()); intent.putExtra("entityId", depictedItem.getId());
context.startActivity(intent); context.startActivity(intent);
} }
} }

View file

@ -18,7 +18,6 @@ import fr.free.nrw.commons.review.ReviewInterface;
import fr.free.nrw.commons.upload.UploadInterface; import fr.free.nrw.commons.upload.UploadInterface;
import fr.free.nrw.commons.upload.WikiBaseInterface; import fr.free.nrw.commons.upload.WikiBaseInterface;
import fr.free.nrw.commons.upload.depicts.DepictsInterface; import fr.free.nrw.commons.upload.depicts.DepictsInterface;
import fr.free.nrw.commons.upload.mediaDetails.CaptionInterface;
import fr.free.nrw.commons.wikidata.WikidataInterface; import fr.free.nrw.commons.wikidata.WikidataInterface;
import java.io.File; import java.io.File;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -163,12 +162,6 @@ public class NetworkingModule {
return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, ReviewInterface.class); return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, ReviewInterface.class);
} }
@Provides
@Singleton
public CaptionInterface provideCaptionInterface(@Named(NAMED_WIKI_DATA_WIKI_SITE) WikiSite wikidataWikiSite) {
return ServiceFactory.get(wikidataWikiSite, BuildConfig.WIKIDATA_URL, CaptionInterface.class);
}
@Provides @Provides
@Singleton @Singleton
public DepictsInterface provideDepictsInterface(@Named(NAMED_WIKI_DATA_WIKI_SITE) WikiSite wikidataWikiSite) { public DepictsInterface provideDepictsInterface(@Named(NAMED_WIKI_DATA_WIKI_SITE) WikiSite wikidataWikiSite) {

View file

@ -12,7 +12,6 @@ import java.security.NoSuchAlgorithmException;
import java.text.ParseException; import java.text.ParseException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
@ -113,8 +112,7 @@ public class DepictsClient {
Media media = new Media(null, Media media = new Media(null,
getUrl(s.getTitle()), getUrl(s.getTitle()),
s.getTitle(), s.getTitle(),
new HashMap<>(), "",
"",
0, 0,
safeParseDate(s.getTimestamp()), safeParseDate(s.getTimestamp()),
safeParseDate(s.getTimestamp()), safeParseDate(s.getTimestamp()),

View file

@ -158,7 +158,7 @@ public class SearchDepictionsFragmentPresenter extends CommonsDaggerSupportFragm
view.onSuccess(mediaList); view.onSuccess(mediaList);
offset=queryList.size(); offset=queryList.size();
for (DepictedItem m : mediaList) { for (DepictedItem m : mediaList) {
fetchThumbnailForEntityId(m.getEntityId(), size++); fetchThumbnailForEntityId(m.getId(), size++);
} }
} }
} }

View file

@ -78,7 +78,7 @@ public class SearchDepictionsRenderer extends Renderer<DepictedItem> {
@Override @Override
public void render() { public void render() {
DepictedItem item = getContent(); DepictedItem item = getContent();
tvDepictionLabel.setText(item.getDepictsLabel()); tvDepictionLabel.setText(item.getName());
tvDepictionDesc.setText(item.getDescription()); tvDepictionDesc.setText(item.getDescription());
imageView.setImageDrawable(getContext().getResources().getDrawable(R.drawable.ic_wikidata_logo_24dp)); imageView.setImageDrawable(getContext().getResources().getDrawable(R.drawable.ic_wikidata_logo_24dp));

View file

@ -1,10 +1,13 @@
package fr.free.nrw.commons.media; package fr.free.nrw.commons.media;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.graphics.drawable.Animatable;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.content.Intent;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Animatable;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.text.Editable; import android.text.Editable;
@ -22,28 +25,17 @@ import android.widget.ScrollView;
import android.widget.Spinner; import android.widget.Spinner;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.Nullable;
import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.drawee.interfaces.DraweeController;
import com.facebook.drawee.controller.BaseControllerListener;
import com.facebook.drawee.controller.ControllerListener;
import com.facebook.drawee.view.SimpleDraweeView;
import com.facebook.imagepipeline.image.ImageInfo;
import com.facebook.imagepipeline.request.ImageRequest;
import org.apache.commons.lang3.StringUtils;
import org.wikipedia.util.DateUtil;
import java.util.ArrayList;
import java.util.Date;
import java.util.Locale;
import javax.inject.Inject;
import butterknife.BindView; import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import butterknife.OnClick; import butterknife.OnClick;
import androidx.annotation.Nullable; import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.drawee.controller.BaseControllerListener;
import com.facebook.drawee.controller.ControllerListener;
import com.facebook.drawee.interfaces.DraweeController;
import com.facebook.drawee.view.SimpleDraweeView;
import com.facebook.imagepipeline.image.ImageInfo;
import com.facebook.imagepipeline.request.ImageRequest;
import fr.free.nrw.commons.Media; import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.MediaDataExtractor; import fr.free.nrw.commons.MediaDataExtractor;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
@ -63,13 +55,15 @@ import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable; import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import java.util.ArrayList;
import java.util.Date;
import java.util.Locale;
import java.util.Map; import java.util.Map;
import javax.inject.Inject;
import org.apache.commons.lang3.StringUtils;
import org.wikipedia.util.DateUtil;
import timber.log.Timber; import timber.log.Timber;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
public class MediaDetailFragment extends CommonsDaggerSupportFragment { public class MediaDetailFragment extends CommonsDaggerSupportFragment {
private boolean editable; private boolean editable;
@ -555,8 +549,8 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
item.setOnClickListener(view -> { item.setOnClickListener(view -> {
DepictedItem depictedItem = new DepictedItem(depictionName, "", "", false, entityId); DepictedItem depictedItem = new DepictedItem(depictionName, "", "", false, entityId);
Intent intent = new Intent(getContext(), WikidataItemDetailsActivity.class); Intent intent = new Intent(getContext(), WikidataItemDetailsActivity.class);
intent.putExtra("wikidataItemName", depictedItem.getDepictsLabel()); intent.putExtra("wikidataItemName", depictedItem.getName());
intent.putExtra("entityId", depictedItem.getEntityId()); intent.putExtra("entityId", depictedItem.getId());
getContext().startActivity(intent); getContext().startActivity(intent);
}); });
} }
@ -609,7 +603,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
private String prettyDescription(Media media) { private String prettyDescription(Media media) {
// @todo use UI language when multilingual descs are available // @todo use UI language when multilingual descs are available
String desc = media.getDescription(locale.getLanguage()).trim(); String desc = media.getDescription();
if (desc.equals("")) { if (desc.equals("")) {
return getString(R.string.detail_description_empty); return getString(R.string.detail_description_empty);
} else { } else {
@ -655,7 +649,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
} }
private void checkDeletion(Media media){ private void checkDeletion(Media media){
if (media.getRequestedDeletion()){ if (media.isRequestedDeletion()){
delete.setVisibility(GONE); delete.setVisibility(GONE);
nominatedForDeletion.setVisibility(VISIBLE); nominatedForDeletion.setVisibility(VISIBLE);
} else if (!isCategoryImage) { } else if (!isCategoryImage) {

View file

@ -170,13 +170,12 @@ public class UploadRemoteDataSource {
* *
* @param uploadableFile * @param uploadableFile
* @param place * @param place
* @param source
* @param similarImageInterface * @param similarImageInterface
* @return * @return
*/ */
public Observable<UploadItem> preProcessImage(UploadableFile uploadableFile, Place place, public Observable<UploadItem> preProcessImage(UploadableFile uploadableFile, Place place,
String source, SimilarImageInterface similarImageInterface) { SimilarImageInterface similarImageInterface) {
return uploadModel.preProcessImage(uploadableFile, place, source, similarImageInterface); return uploadModel.preProcessImage(uploadableFile, place, similarImageInterface);
} }
/** /**
@ -213,7 +212,7 @@ public class UploadRemoteDataSource {
*/ */
public void onDepictedItemClicked(DepictedItem depictedItem) { public void onDepictedItemClicked(DepictedItem depictedItem) {
depictModel.onDepictItemClicked(depictedItem); uploadModel.onDepictItemClicked(depictedItem);
} }
/** /**
@ -222,7 +221,7 @@ public class UploadRemoteDataSource {
*/ */
public List<DepictedItem> getSelectedDepictions() { public List<DepictedItem> getSelectedDepictions() {
return depictModel.getSelectedDepictions(); return uploadModel.getSelectedDepictions();
} }
/** /**
@ -233,14 +232,6 @@ public class UploadRemoteDataSource {
return depictModel.searchAllEntities(query); return depictModel.searchAllEntities(query);
} }
public void setSelectedDepictions(List<String> selectedDepictions) {
uploadModel.setSelectedDepictions(selectedDepictions);
}
public List<String> depictionsEntityIdList() {
return depictModel.depictionsEntityIdList();
}
public void useSimilarPictureCoordinates(ImageCoordinates imageCoordinates, int uploadItemIndex) { public void useSimilarPictureCoordinates(ImageCoordinates imageCoordinates, int uploadItemIndex) {
uploadModel.useSimilarPictureCoordinates(imageCoordinates, uploadItemIndex); uploadModel.useSimilarPictureCoordinates(imageCoordinates, uploadItemIndex);
} }

View file

@ -115,10 +115,6 @@ public class UploadRepository {
remoteDataSource.setSelectedCategories(categoryStringList); remoteDataSource.setSelectedCategories(categoryStringList);
} }
public void setSelectedDepictions(List<String> selectedDepictions) {
remoteDataSource.setSelectedDepictions(selectedDepictions);
}
/** /**
* handles the category selection/deselection * handles the category selection/deselection
* *
@ -180,14 +176,12 @@ public class UploadRepository {
* *
* @param uploadableFile * @param uploadableFile
* @param place * @param place
* @param source
* @param similarImageInterface * @param similarImageInterface
* @return * @return
*/ */
public Observable<UploadItem> preProcessImage(UploadableFile uploadableFile, Place place, public Observable<UploadItem> preProcessImage(UploadableFile uploadableFile, Place place,
String source, SimilarImageInterface similarImageInterface) { SimilarImageInterface similarImageInterface) {
return remoteDataSource return remoteDataSource.preProcessImage(uploadableFile, place, similarImageInterface);
.preProcessImage(uploadableFile, place, source, similarImageInterface);
} }
/** /**
@ -294,10 +288,6 @@ public class UploadRepository {
return remoteDataSource.searchAllEntities(query); return remoteDataSource.searchAllEntities(query);
} }
public List<String> getDepictionsEntityIdList() {
return remoteDataSource.depictionsEntityIdList();
}
/** /**
* Returns nearest place matching the passed latitude and longitude * Returns nearest place matching the passed latitude and longitude
* @param decLatitude * @param decLatitude

View file

@ -0,0 +1,109 @@
package fr.free.nrw.commons.upload;
import android.content.Context;
import androidx.annotation.NonNull;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.filepicker.UploadableFile.DateTimeWithSource;
import fr.free.nrw.commons.settings.Prefs.Licenses;
import fr.free.nrw.commons.utils.ConfigUtils;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import javax.inject.Inject;
import org.apache.commons.lang3.StringUtils;
class PageContentsCreator {
//{{According to Exif data|2009-01-09}}
private static final String TEMPLATE_DATE_ACC_TO_EXIF = "{{According to Exif data|%s}}";
//2009-01-09 9 January 2009
private static final String TEMPLATE_DATA_OTHER_SOURCE = "%s";
private final Context context;
@Inject
public PageContentsCreator(Context context) {
this.context = context;
}
public String createFrom(Contribution contribution) {
StringBuilder buffer = new StringBuilder();
buffer
.append("== {{int:filedesc}} ==\n")
.append("{{Information\n")
.append("|description=").append(contribution.getDescription()).append("\n")
.append("|source=").append("{{own}}\n")
.append("|author=[[User:").append(contribution.getCreator()).append("|")
.append(contribution.getCreator()).append("]]\n");
String templatizedCreatedDate = getTemplatizedCreatedDate(
contribution.getDateCreated(), contribution.getDateCreatedSource());
if (!StringUtils.isBlank(templatizedCreatedDate)) {
buffer.append("|date=").append(templatizedCreatedDate);
}
buffer.append("}}").append("\n");
//Only add Location template (e.g. {{Location|37.51136|-77.602615}} ) if coords is not null
final String decimalCoords = contribution.getDecimalCoords();
if (decimalCoords != null) {
buffer.append("{{Location|").append(decimalCoords).append("}}").append("\n");
}
buffer.append("== {{int:license-header}} ==\n")
.append(licenseTemplateFor(contribution.getLicense())).append("\n\n")
.append("{{Uploaded from Mobile|platform=Android|version=")
.append(ConfigUtils.getVersionNameWithSha(context)).append("}}\n");
final List<String> categories = contribution.getCategories();
if (categories != null && categories.size() != 0) {
for (int i = 0; i < categories.size(); i++) {
buffer.append("\n[[Category:").append(categories.get(i)).append("]]");
}
} else {
buffer.append("{{subst:unc}}");
}
return buffer.toString();
}
/**
* Returns upload date in either TEMPLATE_DATE_ACC_TO_EXIF or TEMPLATE_DATA_OTHER_SOURCE
*
* @param dateCreated
* @param dateCreatedSource
* @return
*/
private String getTemplatizedCreatedDate(Date dateCreated, String dateCreatedSource) {
if (dateCreated != null) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
return String.format(Locale.ENGLISH,
isExif(dateCreatedSource) ? TEMPLATE_DATE_ACC_TO_EXIF : TEMPLATE_DATA_OTHER_SOURCE,
dateFormat.format(dateCreated)
) + "\n";
}
return "";
}
private boolean isExif(String dateCreatedSource) {
return DateTimeWithSource.EXIF_SOURCE.equals(dateCreatedSource);
}
@NonNull
private String licenseTemplateFor(String license) {
switch (license) {
case Licenses.CC_BY_3:
return "{{self|cc-by-3.0}}";
case Licenses.CC_BY_4:
return "{{self|cc-by-4.0}}";
case Licenses.CC_BY_SA_3:
return "{{self|cc-by-sa-3.0}}";
case Licenses.CC_BY_SA_4:
return "{{self|cc-by-sa-4.0}}";
case Licenses.CC0:
return "{{self|cc-zero}}";
}
throw new RuntimeException("Unrecognized license value: " + license);
}
}

View file

@ -1,5 +1,9 @@
package fr.free.nrw.commons.upload; package fr.free.nrw.commons.upload;
import static fr.free.nrw.commons.contributions.ContributionController.ACTION_INTERNAL_UPLOADS;
import static fr.free.nrw.commons.upload.UploadService.EXTRA_FILES;
import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT;
import android.Manifest; import android.Manifest;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.ProgressDialog; import android.app.ProgressDialog;
@ -10,7 +14,6 @@ import android.widget.ImageButton;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.RelativeLayout; import android.widget.RelativeLayout;
import android.widget.TextView; import android.widget.TextView;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.cardview.widget.CardView; import androidx.cardview.widget.CardView;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
@ -20,14 +23,6 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager; import androidx.viewpager.widget.ViewPager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import butterknife.BindView; import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import butterknife.OnClick; import butterknife.OnClick;
@ -36,7 +31,6 @@ import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.LoginActivity; import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.category.CategoriesModel; import fr.free.nrw.commons.category.CategoriesModel;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.contributions.ContributionController; import fr.free.nrw.commons.contributions.ContributionController;
import fr.free.nrw.commons.filepicker.UploadableFile; import fr.free.nrw.commons.filepicker.UploadableFile;
import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.kvstore.JsonKvStore;
@ -48,18 +42,18 @@ import fr.free.nrw.commons.upload.depicts.DepictsFragment;
import fr.free.nrw.commons.upload.license.MediaLicenseFragment; import fr.free.nrw.commons.upload.license.MediaLicenseFragment;
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment; import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment;
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.UploadMediaDetailFragmentCallback; import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.UploadMediaDetailFragmentCallback;
import fr.free.nrw.commons.upload.structure.depictions.DepictModel;
import fr.free.nrw.commons.utils.PermissionUtils; import fr.free.nrw.commons.utils.PermissionUtils;
import fr.free.nrw.commons.utils.ViewUtil; import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers; 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 timber.log.Timber;
import static fr.free.nrw.commons.contributions.ContributionController.ACTION_INTERNAL_UPLOADS;
import static fr.free.nrw.commons.upload.UploadService.EXTRA_FILES;
import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT;
public class UploadActivity extends BaseActivity implements UploadContract.View, UploadBaseFragment.Callback { public class UploadActivity extends BaseActivity implements UploadContract.View, UploadBaseFragment.Callback {
@Inject @Inject
ContributionController contributionController; ContributionController contributionController;
@ -108,8 +102,6 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
private MediaLicenseFragment mediaLicenseFragment; private MediaLicenseFragment mediaLicenseFragment;
private ThumbnailsAdapter thumbnailsAdapter; private ThumbnailsAdapter thumbnailsAdapter;
private String source;
private Place place; private Place place;
private List<UploadableFile> uploadableFiles = Collections.emptyList(); private List<UploadableFile> uploadableFiles = Collections.emptyList();
private int currentSelectedPosition = 0; private int currentSelectedPosition = 0;
@ -325,7 +317,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
fragments = new ArrayList<>(); fragments = new ArrayList<>();
for (UploadableFile uploadableFile : uploadableFiles) { for (UploadableFile uploadableFile : uploadableFiles) {
UploadMediaDetailFragment uploadMediaDetailFragment = new UploadMediaDetailFragment(); UploadMediaDetailFragment uploadMediaDetailFragment = new UploadMediaDetailFragment();
uploadMediaDetailFragment.setImageTobeUploaded(uploadableFile, source, place); uploadMediaDetailFragment.setImageTobeUploaded(uploadableFile, place);
uploadMediaDetailFragment.setCallback(new UploadMediaDetailFragmentCallback() { uploadMediaDetailFragment.setCallback(new UploadMediaDetailFragmentCallback() {
@Override @Override
public void deletePictureAtIndex(int index) { public void deletePictureAtIndex(int index) {
@ -387,16 +379,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
private void receiveInternalSharedItems() { private void receiveInternalSharedItems() {
Intent intent = getIntent(); Intent intent = getIntent();
if (intent.hasExtra(UploadService.EXTRA_SOURCE)) { Timber.d("Received intent %s with action %s", intent.toString(), intent.getAction());
source = intent.getStringExtra(UploadService.EXTRA_SOURCE);
} else {
source = Contribution.SOURCE_EXTERNAL;
}
Timber.d("Received intent %s with action %s and from source %s",
intent.toString(),
intent.getAction(),
source);
uploadableFiles = intent.getParcelableArrayListExtra(EXTRA_FILES); uploadableFiles = intent.getParcelableArrayListExtra(EXTRA_FILES);
Timber.i("Received multiple upload %s", uploadableFiles.size()); Timber.i("Received multiple upload %s", uploadableFiles.size());

View file

@ -1,35 +1,36 @@
package fr.free.nrw.commons.upload; package fr.free.nrw.commons.upload;
import static fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_CSRF;
import android.content.Context; import android.content.Context;
import android.net.Uri; import android.net.Uri;
import fr.free.nrw.commons.CommonsApplication;
import org.wikipedia.csrf.CsrfTokenClient;
import java.io.File;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.upload.UploadService.NotificationUpdateProgressListener; import fr.free.nrw.commons.upload.UploadService.NotificationUpdateProgressListener;
import io.reactivex.Observable; import io.reactivex.Observable;
import java.io.File;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import okhttp3.MediaType; import okhttp3.MediaType;
import okhttp3.MultipartBody; import okhttp3.MultipartBody;
import okhttp3.RequestBody; import okhttp3.RequestBody;
import org.wikipedia.csrf.CsrfTokenClient;
import static fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_CSRF;
@Singleton @Singleton
public class UploadClient { public class UploadClient {
private final UploadInterface uploadInterface; private final UploadInterface uploadInterface;
private final CsrfTokenClient csrfTokenClient; private final CsrfTokenClient csrfTokenClient;
private final PageContentsCreator pageContentsCreator;
@Inject @Inject
public UploadClient(UploadInterface uploadInterface, @Named(NAMED_COMMONS_CSRF) CsrfTokenClient csrfTokenClient) { public UploadClient(UploadInterface uploadInterface,
@Named(NAMED_COMMONS_CSRF) CsrfTokenClient csrfTokenClient,
PageContentsCreator pageContentsCreator) {
this.uploadInterface = uploadInterface; this.uploadInterface = uploadInterface;
this.csrfTokenClient = csrfTokenClient; this.csrfTokenClient = csrfTokenClient;
this.pageContentsCreator = pageContentsCreator;
} }
Observable<UploadResult> uploadFileToStash(Context context, String filename, File file, Observable<UploadResult> uploadFileToStash(Context context, String filename, File file,
@ -61,8 +62,8 @@ public class UploadClient {
try { try {
return uploadInterface return uploadInterface
.uploadFileFromStash(csrfTokenClient.getTokenBlocking(), .uploadFileFromStash(csrfTokenClient.getTokenBlocking(),
contribution.getPageContents(context), pageContentsCreator.createFrom(contribution),
contribution.getEditSummary(), CommonsApplication.DEFAULT_EDIT_SUMMARY,
uniqueFileName, uniqueFileName,
fileKey).map(uploadResponse -> uploadResponse.getUpload()); fileKey).map(uploadResponse -> uploadResponse.getUpload());
} catch (Throwable throwable) { } catch (Throwable throwable) {

View file

@ -115,10 +115,6 @@ public class UploadController {
contribution.setDescription(""); contribution.setDescription("");
} }
if (contribution.getCaption() == null) {
contribution.setCaption("");
}
final String license = store.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_3); final String license = store.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_3);
contribution.setLicense(license); contribution.setLicense(license);
@ -152,7 +148,7 @@ public class UploadController {
if (mimeType != null) { if (mimeType != null) {
Timber.d("MimeType is: %s", mimeType); Timber.d("MimeType is: %s", mimeType);
contribution.setTag("mimeType", mimeType); contribution.setMimeType(mimeType);
if(mimeType.startsWith("image/") && contribution.getDateCreated() == null){ if(mimeType.startsWith("image/") && contribution.getDateCreated() == null){
contribution.setDateCreated(resolveDateTakenOrNow(contentResolver, contribution)); contribution.setDateCreated(resolveDateTakenOrNow(contentResolver, contribution));
} }
@ -162,7 +158,7 @@ public class UploadController {
} }
private String resolveMimeType(final ContentResolver contentResolver, final Contribution contribution) { private String resolveMimeType(final ContentResolver contentResolver, final Contribution contribution) {
final String mimeType = (String) contribution.getTag("mimeType"); final String mimeType = contribution.getMimeType();
if (mimeType == null || TextUtils.isEmpty(mimeType) || mimeType.endsWith("*")) { if (mimeType == null || TextUtils.isEmpty(mimeType) || mimeType.endsWith("*")) {
return contentResolver.getType(contribution.getLocalUri()); return contentResolver.getType(contribution.getLocalUri());
} }

View file

@ -89,13 +89,13 @@ public class UploadDepictsRenderer extends Renderer<DepictedItem> {
public void render() { public void render() {
DepictedItem item = getContent(); DepictedItem item = getContent();
checkedView.setChecked(item.isSelected()); checkedView.setChecked(item.isSelected());
depictsLabel.setText(item.getDepictsLabel()); depictsLabel.setText(item.getName());
description.setText(item.getDescription()); description.setText(item.getDescription());
if (!TextUtils.isEmpty(item.getImageUrl())) { if (!TextUtils.isEmpty(item.getImageUrl())) {
if (!item.getImageUrl().equals(NO_IMAGE_FOR_DEPICTION)) if (!item.getImageUrl().equals(NO_IMAGE_FOR_DEPICTION))
setImageView(Uri.parse(item.getImageUrl()), imageView); setImageView(Uri.parse(item.getImageUrl()), imageView);
}else{ }else{
listener.fetchThumbnailUrlForEntity(item.getEntityId(),item.getPosition()); listener.fetchThumbnailUrlForEntity(item.getId(),item.getPosition());
} }
} }

View file

@ -46,7 +46,7 @@ data class UploadMediaDetail constructor(
*/ */
@JvmStatic @JvmStatic
fun formatCaptions(uploadMediaDetails: List<UploadMediaDetail>) = fun formatCaptions(uploadMediaDetails: List<UploadMediaDetail>) =
uploadMediaDetails.associate { it.languageCode to it.captionText } uploadMediaDetails.associate { it.languageCode to it.captionText }.filter { it.value.isNotBlank() }
/** /**
* Formats the list of descriptions into the format Commons requires for uploads. * Formats the list of descriptions into the format Commons requires for uploads.
@ -56,11 +56,7 @@ data class UploadMediaDetail constructor(
*/ */
@JvmStatic @JvmStatic
fun formatList(descriptions: List<UploadMediaDetail>) = fun formatList(descriptions: List<UploadMediaDetail>) =
descriptions.joinToString { descriptions.filter { it.descriptionText.isNotEmpty() }
if (it.descriptionText.isNotEmpty()) .joinToString { "{{${it.languageCode}|1=${it.descriptionText}}}" }
"{{${it.languageCode}|1=${it.descriptionText}}}"
else
""
}
} }
} }

View file

@ -4,13 +4,13 @@ import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.net.Uri; import android.net.Uri;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.filepicker.UploadableFile; import fr.free.nrw.commons.filepicker.UploadableFile;
import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.nearby.Place; import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.settings.Prefs; import fr.free.nrw.commons.settings.Prefs;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import fr.free.nrw.commons.utils.ImageUtils; import fr.free.nrw.commons.utils.ImageUtils;
import io.reactivex.Observable; import io.reactivex.Observable;
import io.reactivex.Single; import io.reactivex.Single;
@ -18,9 +18,7 @@ import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.subjects.BehaviorSubject; import io.reactivex.subjects.BehaviorSubject;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -44,8 +42,8 @@ public class UploadModel {
private final SessionManager sessionManager; private final SessionManager sessionManager;
private final FileProcessor fileProcessor; private final FileProcessor fileProcessor;
private final ImageProcessingService imageProcessingService; private final ImageProcessingService imageProcessingService;
private List<String> selectedCategories; private List<String> selectedCategories = new ArrayList<>();
private ArrayList<String> selectedDepictions; private List<DepictedItem> selectedDepictions = new ArrayList<>();
@Inject @Inject
UploadModel(@Named("licenses") final List<String> licenses, UploadModel(@Named("licenses") final List<String> licenses,
@ -71,28 +69,23 @@ public class UploadModel {
public void cleanUp() { public void cleanUp() {
compositeDisposable.clear(); compositeDisposable.clear();
fileProcessor.cleanup(); fileProcessor.cleanup();
this.items.clear(); items.clear();
if (this.selectedCategories != null) { selectedCategories.clear();
this.selectedCategories.clear(); selectedDepictions.clear();
}
if (this.selectedDepictions != null) {
this.selectedDepictions.clear();
}
} }
public void setSelectedCategories(List<String> selectedCategories) { public void setSelectedCategories(List<String> selectedCategories) {
this.selectedCategories = newListOf(selectedCategories); this.selectedCategories = selectedCategories;
} }
/** /**
* pre process a one item at a time * pre process a one item at a time
*/ */
public Observable<UploadItem> preProcessImage(final UploadableFile uploadableFile, public Observable<UploadItem> preProcessImage(final UploadableFile uploadableFile,
final Place place, final Place place,
final String source, final SimilarImageInterface similarImageInterface) {
final SimilarImageInterface similarImageInterface) {
return Observable.just( return Observable.just(
createAndAddUploadItem(uploadableFile, place, source, similarImageInterface)); createAndAddUploadItem(uploadableFile, place, similarImageInterface));
} }
public Single<Integer> getImageQuality(final UploadItem uploadItem) { public Single<Integer> getImageQuality(final UploadItem uploadItem) {
@ -100,9 +93,8 @@ public class UploadModel {
} }
private UploadItem createAndAddUploadItem(final UploadableFile uploadableFile, private UploadItem createAndAddUploadItem(final UploadableFile uploadableFile,
final Place place, final Place place,
final String source, final SimilarImageInterface similarImageInterface) {
final SimilarImageInterface similarImageInterface) {
final UploadableFile.DateTimeWithSource dateTimeWithSource = uploadableFile final UploadableFile.DateTimeWithSource dateTimeWithSource = uploadableFile
.getFileCreatedDate(context); .getFileCreatedDate(context);
long fileCreatedDate = -1; long fileCreatedDate = -1;
@ -116,7 +108,7 @@ public class UploadModel {
.processFileCoordinates(similarImageInterface, uploadableFile.getFilePath()); .processFileCoordinates(similarImageInterface, uploadableFile.getFilePath());
final UploadItem uploadItem = new UploadItem(uploadableFile.getContentUri(), final UploadItem uploadItem = new UploadItem(uploadableFile.getContentUri(),
Uri.parse(uploadableFile.getFilePath()), Uri.parse(uploadableFile.getFilePath()),
uploadableFile.getMimeType(context), source, imageCoordinates, place, fileCreatedDate, uploadableFile.getMimeType(context), imageCoordinates, place, fileCreatedDate,
createdTimestampSource); createdTimestampSource);
if (place != null) { if (place != null) {
uploadItem.getUploadMediaDetails().set(0, new UploadMediaDetail(place)); uploadItem.getUploadMediaDetails().set(0, new UploadMediaDetail(place));
@ -151,29 +143,8 @@ public class UploadModel {
public Observable<Contribution> buildContributions() { public Observable<Contribution> buildContributions() {
return Observable.fromIterable(items).map(item -> return Observable.fromIterable(items).map(item ->
{ {
final Contribution contribution = new Contribution(item.mediaUri, null, final Contribution contribution = new Contribution(
item.getFileName(), item.uploadMediaDetails.size()!=0? UploadMediaDetail.formatCaptions(item.uploadMediaDetails):new HashMap<>(), item, sessionManager, newListOf(selectedDepictions), newListOf(selectedCategories));
UploadMediaDetail.formatList(item.uploadMediaDetails), -1,
null, null, sessionManager.getAuthorName(),
CommonsApplication.DEFAULT_EDIT_SUMMARY, new ArrayList<>(selectedDepictions), item.gpsCoords.getDecimalCoords());
if (item.place != null) {
contribution.setWikiDataEntityId(item.place.getWikiDataEntityId());
contribution.setWikiItemName(item.place.getName());
// If item already has an image, we need to know it. We don't want to override existing image later
contribution.setP18Value(item.place.pic);
}
if (null == selectedCategories) {//Just a fail safe, this should never be null
selectedCategories = new ArrayList<>();
}
if (selectedDepictions == null) {
selectedDepictions = new ArrayList<>();
}
contribution.setCategories(selectedCategories);
contribution.setTag("mimeType", item.mimeType);
contribution.setSource(item.source);
contribution.setContentProviderUri(item.mediaUri);
contribution.setDateUploaded(new Date());
Timber.d("Created timestamp while building contribution is %s, %s", Timber.d("Created timestamp while building contribution is %s, %s",
item.getCreatedTimestamp(), item.getCreatedTimestamp(),
new Date(item.getCreatedTimestamp())); new Date(item.getCreatedTimestamp()));
@ -208,12 +179,16 @@ public class UploadModel {
uploadItem1.setMediaDetails(uploadItem.uploadMediaDetails); uploadItem1.setMediaDetails(uploadItem.uploadMediaDetails);
} }
public void setSelectedDepictions(final List<String> selectedDepictions) { public void onDepictItemClicked(DepictedItem depictedItem) {
this.selectedDepictions = newListOf(selectedDepictions); if (depictedItem.isSelected()) {
selectedDepictions.add(depictedItem);
} else {
selectedDepictions.remove(depictedItem);
}
} }
@NotNull @NotNull
private <T> ArrayList<T> newListOf(final List<T> items) { private <T> List<T> newListOf(final List<T> items) {
return items != null ? new ArrayList<>(items) : new ArrayList<>(); return items != null ? new ArrayList<>(items) : new ArrayList<>();
} }
@ -222,13 +197,16 @@ public class UploadModel {
items.get(uploadItemIndex).setGpsCoords(imageCoordinates); items.get(uploadItemIndex).setGpsCoords(imageCoordinates);
} }
public List<DepictedItem> getSelectedDepictions() {
return selectedDepictions;
}
@SuppressWarnings("WeakerAccess") @SuppressWarnings("WeakerAccess")
public static class UploadItem { public static class UploadItem {
private final Uri originalContentUri; private final Uri originalContentUri;
private final Uri mediaUri; private final Uri mediaUri;
private final String mimeType; private final String mimeType;
private final String source;
private ImageCoordinates gpsCoords; private ImageCoordinates gpsCoords;
private List<UploadMediaDetail> uploadMediaDetails; private List<UploadMediaDetail> uploadMediaDetails;
private final Place place; private final Place place;
@ -237,17 +215,17 @@ public class UploadModel {
private final BehaviorSubject<Integer> imageQuality; private final BehaviorSubject<Integer> imageQuality;
@SuppressLint("CheckResult") @SuppressLint("CheckResult")
UploadItem(final Uri originalContentUri, UploadItem(final Uri originalContentUri,
final Uri mediaUri, final String mimeType, final String source, final ImageCoordinates gpsCoords, final Uri mediaUri, final String mimeType,
final Place place, final ImageCoordinates gpsCoords,
final long createdTimestamp, final Place place,
final String createdTimestampSource) { final long createdTimestamp,
final String createdTimestampSource) {
this.originalContentUri = originalContentUri; this.originalContentUri = originalContentUri;
this.createdTimestampSource = createdTimestampSource; this.createdTimestampSource = createdTimestampSource;
uploadMediaDetails = new ArrayList<>(Arrays.asList(new UploadMediaDetail())); uploadMediaDetails = new ArrayList<>(Arrays.asList(new UploadMediaDetail()));
this.place = place; this.place = place;
this.mediaUri = mediaUri; this.mediaUri = mediaUri;
this.mimeType = mimeType; this.mimeType = mimeType;
this.source = source;
this.gpsCoords = gpsCoords; this.gpsCoords = gpsCoords;
this.createdTimestamp = createdTimestamp; this.createdTimestamp = createdTimestamp;
imageQuality = BehaviorSubject.createDefault(ImageUtils.IMAGE_WAIT); imageQuality = BehaviorSubject.createDefault(ImageUtils.IMAGE_WAIT);
@ -257,10 +235,6 @@ public class UploadModel {
return createdTimestampSource; return createdTimestampSource;
} }
public String getSource() {
return source;
}
public ImageCoordinates getGpsCoords() { public ImageCoordinates getGpsCoords() {
return gpsCoords; return gpsCoords;
} }
@ -323,5 +297,8 @@ public class UploadModel {
this.gpsCoords = gpsCoords; this.gpsCoords = gpsCoords;
} }
public String getMimeType() {
return mimeType;
}
} }
} }

View file

@ -2,4 +2,16 @@ package fr.free.nrw.commons.upload
import org.wikipedia.gallery.ImageInfo import org.wikipedia.gallery.ImageInfo
class UploadResult(val result: String, val filekey: String, val filename: String, val sessionkey: String, val imageinfo: ImageInfo) private const val RESULT_SUCCESS = "Success"
data class UploadResult(
val result: String,
val filekey: String,
val filename: String,
val sessionkey: String,
val imageinfo: ImageInfo
) {
fun isSuccessful(): Boolean = result == RESULT_SUCCESS
fun createCanonicalFileName() = "File:$filename"
}

View file

@ -7,21 +7,8 @@ import android.content.Intent;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat; import androidx.core.app.NotificationManagerCompat;
import java.io.File;
import java.io.IOException;
import java.text.ParseException;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.inject.Inject;
import javax.inject.Named;
import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.HandlerService; import fr.free.nrw.commons.HandlerService;
@ -38,6 +25,15 @@ import io.reactivex.Observable;
import io.reactivex.Scheduler; import io.reactivex.Scheduler;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import java.io.File;
import java.io.IOException;
import java.text.ParseException;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.inject.Inject;
import javax.inject.Named;
import timber.log.Timber; import timber.log.Timber;
public class UploadService extends HandlerService<Contribution> { public class UploadService extends HandlerService<Contribution> {
@ -47,7 +43,6 @@ public class UploadService extends HandlerService<Contribution> {
public static final int ACTION_UPLOAD_FILE = 1; public static final int ACTION_UPLOAD_FILE = 1;
public static final String ACTION_START_SERVICE = EXTRA_PREFIX + ".upload"; public static final String ACTION_START_SERVICE = EXTRA_PREFIX + ".upload";
public static final String EXTRA_SOURCE = EXTRA_PREFIX + ".source";
public static final String EXTRA_FILES = EXTRA_PREFIX + ".files"; public static final String EXTRA_FILES = EXTRA_PREFIX + ".files";
@Inject WikidataEditService wikidataEditService; @Inject WikidataEditService wikidataEditService;
@Inject SessionManager sessionManager; @Inject SessionManager sessionManager;
@ -151,7 +146,6 @@ public class UploadService extends HandlerService<Contribution> {
@Override @Override
public void queue(int what, Contribution contribution) { public void queue(int what, Contribution contribution) {
Timber.d("Upload service queue has contribution with wiki data entity id as %s", contribution.getWikiDataEntityId());
switch (what) { switch (what) {
case ACTION_UPLOAD_FILE: case ACTION_UPLOAD_FILE:
@ -168,7 +162,7 @@ public class UploadService extends HandlerService<Contribution> {
.subscribeOn(ioThreadScheduler) .subscribeOn(ioThreadScheduler)
.observeOn(mainThreadScheduler) .observeOn(mainThreadScheduler)
.subscribe(aLong->{ .subscribe(aLong->{
contribution._id = aLong; contribution.set_id(aLong);
UploadService.super.queue(what, contribution); UploadService.super.queue(what, contribution);
}, Throwable::printStackTrace)); }, Throwable::printStackTrace));
break; break;
@ -251,20 +245,19 @@ public class UploadService extends HandlerService<Contribution> {
Timber.d("Stash upload response 1 is %s", uploadStash.toString()); Timber.d("Stash upload response 1 is %s", uploadStash.toString());
String resultStatus = uploadStash.getResult(); if (uploadStash.isSuccessful()) {
if (!resultStatus.equals("Success")) {
Timber.d("Contribution upload failed. Wikidata entity won't be edited");
showFailedNotification(contribution);
return Observable.never();
} else {
Timber.d("making sure of uniqueness of name: %s", filename); Timber.d("making sure of uniqueness of name: %s", filename);
String uniqueFilename = findUniqueFilename(filename); String uniqueFilename = findUniqueFilename(filename);
unfinishedUploads.add(uniqueFilename); unfinishedUploads.add(uniqueFilename);
return uploadClient.uploadFileFromStash( return uploadClient.uploadFileFromStash(
getApplicationContext(), getApplicationContext(),
contribution, contribution,
uniqueFilename, uniqueFilename,
uploadStash.getFilekey()); uploadStash.getFilekey());
} else {
Timber.d("Contribution upload failed. Wikidata entity won't be edited");
showFailedNotification(contribution);
return Observable.never();
} }
}) })
.subscribe( .subscribe(
@ -282,44 +275,38 @@ public class UploadService extends HandlerService<Contribution> {
notificationManager.cancel(notificationTag, NOTIFICATION_UPLOAD_IN_PROGRESS); notificationManager.cancel(notificationTag, NOTIFICATION_UPLOAD_IN_PROGRESS);
String resultStatus = uploadResult.getResult(); if (uploadResult.isSuccessful()) {
if (!resultStatus.equals("Success")) { onSuccessfulUpload(contribution, uploadResult);
} else {
Timber.d("Contribution upload failed. Wikidata entity won't be edited"); Timber.d("Contribution upload failed. Wikidata entity won't be edited");
showFailedNotification(contribution); showFailedNotification(contribution);
} else {
String canonicalFilename = "File:" + uploadResult.getFilename();
final String wikiDataEntityId = contribution.getWikiDataEntityId();
Timber.d("Contribution upload success. Initiating Wikidata edit for entity id %s",
wikiDataEntityId);
// to perform upload of depictions we pass on depiction entityId of the selected depictions to the wikidataEditService
final String p18Value = contribution.getP18Value();
final String wikiItemName = contribution.getWikiItemName();
if (contribution.getDepictionsEntityIds() != null) {
for (String depictionEntityId : contribution.getDepictionsEntityIds()) {
wikidataEditService.createClaimWithLogging(depictionEntityId,
wikiItemName, canonicalFilename, p18Value);
}
}
Timber.d("Contribution upload success. Initiating Wikidata edit for"
+ " entity id %s if necessary (if P18 is null). P18 value is %s",
wikiDataEntityId, p18Value);
wikidataEditService.createClaimWithLogging(
wikiDataEntityId, wikiItemName, canonicalFilename,p18Value);
wikidataEditService.createLabelforWikidataEntity(canonicalFilename, contribution.getCaptions());
contribution.setFilename(canonicalFilename);
contribution.setImageUrl(uploadResult.getImageinfo().getOriginalUrl());
contribution.setState(Contribution.STATE_COMPLETED);
contribution.setDateUploaded(CommonsDateUtil.getIso8601DateFormatTimestamp()
.parse(uploadResult.getImageinfo().getTimestamp()));
compositeDisposable.add(contributionDao
.save(contribution)
.subscribeOn(ioThreadScheduler)
.observeOn(mainThreadScheduler)
.subscribe());
} }
} }
private void onSuccessfulUpload(Contribution contribution, UploadResult uploadResult)
throws ParseException {
compositeDisposable
.add(wikidataEditService.addDepictionsAndCaptions(uploadResult, contribution));
WikidataPlace wikidataPlace = contribution.getWikidataPlace();
if (wikidataPlace != null && wikidataPlace.getImageValue() == null) {
wikidataEditService.createImageClaim(wikidataPlace, uploadResult);
}
saveCompletedContribution(contribution, uploadResult);
}
private void saveCompletedContribution(Contribution contribution, UploadResult uploadResult) throws ParseException {
contribution.setFilename(uploadResult.createCanonicalFileName());
contribution.setImageUrl(uploadResult.getImageinfo().getOriginalUrl());
contribution.setState(Contribution.STATE_COMPLETED);
contribution.setDateUploaded(CommonsDateUtil.getIso8601DateFormatTimestamp()
.parse(uploadResult.getImageinfo().getTimestamp()));
compositeDisposable.add(contributionDao
.save(contribution)
.subscribeOn(ioThreadScheduler)
.observeOn(mainThreadScheduler)
.subscribe());
}
@SuppressLint("StringFormatInvalid") @SuppressLint("StringFormatInvalid")
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
private void showFailedNotification(Contribution contribution) { private void showFailedNotification(Contribution contribution) {

View file

@ -1,11 +1,11 @@
package fr.free.nrw.commons.upload; package fr.free.nrw.commons.upload;
import androidx.annotation.NonNull; import static org.wikipedia.dataclient.Service.MW_API_PREFIX;
import androidx.annotation.NonNull;
import io.reactivex.Observable;
import org.wikipedia.dataclient.mwapi.MwPostResponse; import org.wikipedia.dataclient.mwapi.MwPostResponse;
import org.wikipedia.dataclient.mwapi.MwQueryResponse; import org.wikipedia.dataclient.mwapi.MwQueryResponse;
import io.reactivex.Observable;
import retrofit2.http.Field; import retrofit2.http.Field;
import retrofit2.http.FormUrlEncoded; import retrofit2.http.FormUrlEncoded;
import retrofit2.http.GET; import retrofit2.http.GET;
@ -13,8 +13,6 @@ import retrofit2.http.Headers;
import retrofit2.http.POST; import retrofit2.http.POST;
import retrofit2.http.Query; import retrofit2.http.Query;
import static org.wikipedia.dataclient.Service.MW_API_PREFIX;
/** /**
* Retrofit calls for managing responses network calls of entity ids required for uploading depictions * Retrofit calls for managing responses network calls of entity ids required for uploading depictions
*/ */
@ -31,4 +29,18 @@ public interface WikiBaseInterface {
@GET(MW_API_PREFIX + "action=query&prop=info") @GET(MW_API_PREFIX + "action=query&prop=info")
Observable<MwQueryResponse> getFileEntityId(@Query("titles") String fileName); Observable<MwQueryResponse> getFileEntityId(@Query("titles") String fileName);
} /**
* Upload Captions for the image when upload is successful
*
* @param fileEntityId enityId for the uploaded file
* @param editToken editToken for the file
* @param captionValue value of the caption to be uploaded
*/
@FormUrlEncoded
@POST(MW_API_PREFIX + "action=wbsetlabel")
Observable<MwPostResponse> addLabelstoWikidata(@Field("id") String fileEntityId,
@Field("token") String editToken,
@Field("language") String language,
@Field("value") String captionValue);
}

View file

@ -0,0 +1,6 @@
package fr.free.nrw.commons.upload
interface WikidataItem {
val id:String
val name:String
}

View file

@ -0,0 +1,21 @@
package fr.free.nrw.commons.upload
import android.os.Parcelable
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?) :
WikidataItem,Parcelable {
constructor(place: Place) : this(
place.wikiDataEntityId!!,
place.name,
place.pic.takeIf { it.isNotBlank() })
companion object {
@JvmStatic
fun from(place: Place?): WikidataPlace? {
return place?.let { WikidataPlace(it) }
}
}
}

View file

@ -126,7 +126,6 @@ public class DepictsPresenter implements DepictsContract.UserActionListener {
public void verifyDepictions() { public void verifyDepictions() {
List<DepictedItem> selectedDepictions = repository.getSelectedDepictions(); List<DepictedItem> selectedDepictions = repository.getSelectedDepictions();
if (selectedDepictions != null && !selectedDepictions.isEmpty()) { if (selectedDepictions != null && !selectedDepictions.isEmpty()) {
repository.setSelectedDepictions(repository.getDepictionsEntityIdList());
view.goToNextScreen(); view.goToNextScreen();
} else { } else {
view.noDepictionSelected(); view.noDepictionSelected();

View file

@ -1,31 +0,0 @@
package fr.free.nrw.commons.upload.mediaDetails;
import org.wikipedia.dataclient.mwapi.MwPostResponse;
import java.util.Map;
import io.reactivex.Observable;
import retrofit2.http.Field;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.POST;
import static org.wikipedia.dataclient.Service.MW_API_PREFIX;
public interface CaptionInterface {
/**
* Upload Captions for the image when upload is successful
*
* @param FileEntityId enityId for the uploaded file
* @param editToken editToken for the file
* @param captionValue value of the caption to be uploaded
* @param caption additional data associated with caption
*/
@FormUrlEncoded
@POST(MW_API_PREFIX + "action=wbsetlabel&language=en")
Observable<MwPostResponse> addLabelstoWikidata(@Field("id") String FileEntityId,
@Field("token") String editToken,
@Field("value") String captionValue,
@Field("data") Map<String, String> caption);
}

View file

@ -81,7 +81,6 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
JsonKvStore defaultKvStore; JsonKvStore defaultKvStore;
private UploadableFile uploadableFile; private UploadableFile uploadableFile;
private String source;
private Place place; private Place place;
private boolean isExpanded = true; private boolean isExpanded = true;
@ -97,9 +96,8 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
} }
public void setImageTobeUploaded(UploadableFile uploadableFile, String source, Place place) { public void setImageTobeUploaded(UploadableFile uploadableFile, Place place) {
this.uploadableFile = uploadableFile; this.uploadableFile = uploadableFile;
this.source = source;
this.place = place; this.place = place;
} }
@ -122,7 +120,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
callback.getTotalNumberOfSteps())); callback.getTotalNumberOfSteps()));
initRecyclerView(); initRecyclerView();
initPresenter(); initPresenter();
presenter.receiveImage(uploadableFile, source, place); presenter.receiveImage(uploadableFile, place);
if (callback.getIndexInViewFlipper(this) == 0) { if (callback.getIndexInViewFlipper(this) == 0) {
btnPrevious.setEnabled(false); btnPrevious.setEnabled(false);

View file

@ -1,15 +1,13 @@
package fr.free.nrw.commons.upload.mediaDetails; package fr.free.nrw.commons.upload.mediaDetails;
import fr.free.nrw.commons.upload.ImageCoordinates;
import java.util.List;
import fr.free.nrw.commons.BasePresenter; import fr.free.nrw.commons.BasePresenter;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.filepicker.UploadableFile; import fr.free.nrw.commons.filepicker.UploadableFile;
import fr.free.nrw.commons.nearby.Place; import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.upload.UploadMediaDetail; import fr.free.nrw.commons.upload.ImageCoordinates;
import fr.free.nrw.commons.upload.SimilarImageInterface; import fr.free.nrw.commons.upload.SimilarImageInterface;
import fr.free.nrw.commons.upload.UploadMediaDetail;
import fr.free.nrw.commons.upload.UploadModel.UploadItem; import fr.free.nrw.commons.upload.UploadModel.UploadItem;
import java.util.List;
/** /**
* The contract with with UploadMediaDetails and its presenter would talk to each other * The contract with with UploadMediaDetails and its presenter would talk to each other
@ -41,8 +39,7 @@ public interface UploadMediaDetailsContract {
interface UserActionListener extends BasePresenter<View> { interface UserActionListener extends BasePresenter<View> {
void receiveImage(UploadableFile uploadableFile, @Contribution.FileSource String source, void receiveImage(UploadableFile uploadableFile, Place place);
Place place);
void verifyImageQuality(UploadItem uploadItem); void verifyImageQuality(UploadItem uploadItem);

View file

@ -64,16 +64,14 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt
/** /**
* Receives the corresponding uploadable file, processes it and return the view with and uplaod item * Receives the corresponding uploadable file, processes it and return the view with and uplaod item
* * @param uploadableFile
* @param uploadableFile
* @param source
* @param place * @param place
*/ */
@Override @Override
public void receiveImage(UploadableFile uploadableFile, String source, Place place) { public void receiveImage(UploadableFile uploadableFile, Place place) {
view.showProgress(true); view.showProgress(true);
Disposable uploadItemDisposable = repository Disposable uploadItemDisposable = repository
.preProcessImage(uploadableFile, place, source, this) .preProcessImage(uploadableFile, place, this)
.subscribeOn(ioScheduler) .subscribeOn(ioScheduler)
.observeOn(mainThreadScheduler) .observeOn(mainThreadScheduler)
.subscribe(uploadItem -> .subscribe(uploadItem ->

View file

@ -2,7 +2,7 @@ package fr.free.nrw.commons.upload.structure.depictions
import fr.free.nrw.commons.upload.depicts.DepictsInterface import fr.free.nrw.commons.upload.depicts.DepictsInterface
import io.reactivex.Observable import io.reactivex.Observable
import java.util.Locale import java.util.*
import javax.inject.Inject import javax.inject.Inject
/** /**
@ -14,24 +14,6 @@ class DepictModel @Inject constructor(private val depictsInterface: DepictsInter
private const val SEARCH_DEPICTS_LIMIT = 25 private const val SEARCH_DEPICTS_LIMIT = 25
} }
val selectedDepictions = mutableListOf<DepictedItem>()
fun onDepictItemClicked(depictedItem: DepictedItem) {
if (depictedItem.isSelected) {
selectDepictItem(depictedItem)
} else {
unselectDepiction(depictedItem)
}
}
private fun unselectDepiction(depictedItem: DepictedItem) {
selectedDepictions.remove(depictedItem)
}
private fun selectDepictItem(depictedItem: DepictedItem) {
selectedDepictions.add(depictedItem)
}
/** /**
* Search for depictions * Search for depictions
*/ */
@ -44,5 +26,4 @@ class DepictModel @Inject constructor(private val depictsInterface: DepictsInter
.map(::DepictedItem) .map(::DepictedItem)
} }
fun depictionsEntityIdList() = selectedDepictions.map { it.entityId }
} }

View file

@ -1,17 +1,18 @@
package fr.free.nrw.commons.upload.structure.depictions package fr.free.nrw.commons.upload.structure.depictions
import fr.free.nrw.commons.upload.WikidataItem
import fr.free.nrw.commons.wikidata.model.DepictSearchItem import fr.free.nrw.commons.wikidata.model.DepictSearchItem
/** /**
* Model class for Depicted Item in Upload and Explore * Model class for Depicted Item in Upload and Explore
*/ */
data class DepictedItem constructor( data class DepictedItem constructor(
val depictsLabel: String?, override val name: String,
val description: String?, val description: String?,
var imageUrl: String, var imageUrl: String,
var isSelected: Boolean, var isSelected: Boolean,
val entityId: String override val id: String
) { ):WikidataItem {
constructor(depictSearchItem: DepictSearchItem) : this( constructor(depictSearchItem: DepictSearchItem) : this(
depictSearchItem.label, depictSearchItem.label,
depictSearchItem.description, depictSearchItem.description,
@ -24,12 +25,12 @@ data class DepictedItem constructor(
override fun equals(o: Any?) = when { override fun equals(o: Any?) = when {
this === o -> true this === o -> true
o is DepictedItem -> depictsLabel == o.depictsLabel o is DepictedItem -> name == o.name
else -> false else -> false
} }
override fun hashCode(): Int { override fun hashCode(): Int {
return depictsLabel?.hashCode() ?: 0 return name?.hashCode() ?: 0
} }
} }

View file

@ -50,7 +50,7 @@ public class DepictionRenderer extends Renderer<DepictedItem> {
public void render() { public void render() {
DepictedItem item = getContent(); DepictedItem item = getContent();
checkedView.setChecked(item.isSelected()); checkedView.setChecked(item.isSelected());
depictsLabel.setText(item.getDepictsLabel()); depictsLabel.setText(item.getName());
description.setText(item.getDescription()); description.setText(item.getDescription());
} }
} }

View file

@ -1,16 +1,17 @@
package fr.free.nrw.commons.wikidata; package fr.free.nrw.commons.wikidata;
import org.wikipedia.csrf.CsrfTokenClient; import static fr.free.nrw.commons.depictions.Media.DepictedImagesFragment.PAGE_ID_PREFIX;
import static fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_CSRF;
import fr.free.nrw.commons.upload.UploadResult;
import fr.free.nrw.commons.upload.WikiBaseInterface;
import io.reactivex.Observable;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
import javax.inject.Singleton; import javax.inject.Singleton;
import org.wikipedia.csrf.CsrfTokenClient;
import fr.free.nrw.commons.upload.WikiBaseInterface; import org.wikipedia.dataclient.mwapi.MwPostResponse;
import fr.free.nrw.commons.utils.ConfigUtils; import timber.log.Timber;
import io.reactivex.Observable;
import static fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_CSRF;
/** /**
* Wikibase Client for calling WikiBase APIs * Wikibase Client for calling WikiBase APIs
@ -18,27 +19,43 @@ import static fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_CSRF;
@Singleton @Singleton
public class WikiBaseClient { public class WikiBaseClient {
private final WikiBaseInterface wikiBaseInterface; private final WikiBaseInterface wikiBaseInterface;
private final CsrfTokenClient csrfTokenClient; private final CsrfTokenClient csrfTokenClient;
@Inject @Inject
public WikiBaseClient(WikiBaseInterface wikiBaseInterface, public WikiBaseClient(WikiBaseInterface wikiBaseInterface,
@Named(NAMED_COMMONS_CSRF) CsrfTokenClient csrfTokenClient) { @Named(NAMED_COMMONS_CSRF) CsrfTokenClient csrfTokenClient) {
this.wikiBaseInterface = wikiBaseInterface; this.wikiBaseInterface = wikiBaseInterface;
this.csrfTokenClient = csrfTokenClient; this.csrfTokenClient = csrfTokenClient;
} }
public Observable<Boolean> postEditEntity(String fileEntityId, String data) { public Observable<Boolean> postEditEntity(String fileEntityId, String data) {
try { return csrfToken()
return wikiBaseInterface.postEditEntity(fileEntityId, csrfTokenClient.getTokenBlocking(), data) .switchMap(editToken -> wikiBaseInterface.postEditEntity(fileEntityId, editToken, data)
.map(response -> (response.getSuccessVal() == 1)); .map(response -> (response.getSuccessVal() == 1)));
} catch (Throwable throwable) { }
return Observable.just(false);
}
}
public Observable<Long> getFileEntityId(String fileName) { public Observable<Long> getFileEntityId(UploadResult uploadResult) {
return wikiBaseInterface.getFileEntityId(fileName) return wikiBaseInterface.getFileEntityId(uploadResult.createCanonicalFileName())
.map(response -> (long) (response.query().pages().get(0).pageId())); .map(response -> (long) (response.query().pages().get(0).pageId()));
} }
}
public Observable<MwPostResponse> addLabelstoWikidata(long fileEntityId,
String languageCode, String captionValue) {
return csrfToken()
.switchMap(editToken -> wikiBaseInterface
.addLabelstoWikidata(PAGE_ID_PREFIX + fileEntityId, editToken, languageCode, captionValue));
}
private Observable<String> csrfToken() {
return Observable.fromCallable(() -> {
try {
return csrfTokenClient.getTokenBlocking();
} catch (Throwable throwable) {
Timber.e(throwable);
return "";
}
});
}
}

View file

@ -1,5 +1,6 @@
package fr.free.nrw.commons.wikidata; package fr.free.nrw.commons.wikidata;
import fr.free.nrw.commons.upload.WikidataItem;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import javax.inject.Inject; import javax.inject.Inject;
@ -24,15 +25,16 @@ public class WikidataClient {
/** /**
* Create wikidata claim to add P18 value * Create wikidata claim to add P18 value
* @param entityId wikidata entity ID * @param entity wikidata entity ID
* @param value value of the P18 edit * @param value value of the P18 edit
* @return revisionID of the edit * @return revisionID of the edit
*/ */
Observable<Long> createClaim(String entityId, String value) { Observable<Long> createImageClaim(WikidataItem entity, String value) {
return getCsrfToken() return getCsrfToken()
.flatMap(csrfToken -> wikidataInterface.postCreateClaim(toRequestBody(entityId), .flatMap(csrfToken -> wikidataInterface.postCreateClaim(
toRequestBody(entity.getId()),
toRequestBody("value"), toRequestBody("value"),
toRequestBody("P18"), toRequestBody(WikidataProperties.IMAGE.getPropertyName()),
toRequestBody(value), toRequestBody(value),
toRequestBody("en"), toRequestBody("en"),
toRequestBody(csrfToken))) toRequestBody(csrfToken)))

View file

@ -4,27 +4,28 @@ import static fr.free.nrw.commons.depictions.Media.DepictedImagesFragment.PAGE_I
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import androidx.annotation.NonNull; import androidx.annotation.Nullable;
import com.google.gson.JsonArray; import com.google.gson.JsonArray;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.upload.mediaDetails.CaptionInterface; import fr.free.nrw.commons.upload.UploadResult;
import fr.free.nrw.commons.upload.WikidataItem;
import fr.free.nrw.commons.upload.WikidataPlace;
import fr.free.nrw.commons.utils.ConfigUtils; import fr.free.nrw.commons.utils.ConfigUtils;
import fr.free.nrw.commons.utils.ViewUtil; import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.Observable; import io.reactivex.Observable;
import io.reactivex.ObservableSource;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import java.util.HashMap; import java.util.ArrayList;
import java.util.Locale; import java.util.Locale;
import java.util.Map;
import java.util.concurrent.Callable;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
import javax.inject.Singleton; import javax.inject.Singleton;
import org.wikipedia.csrf.CsrfTokenClient; import org.jetbrains.annotations.NotNull;
import org.wikipedia.dataclient.mwapi.MwPostResponse;
import timber.log.Timber; import timber.log.Timber;
/** /**
@ -35,279 +36,193 @@ import timber.log.Timber;
@Singleton @Singleton
public class WikidataEditService { public class WikidataEditService {
private final static String COMMONS_APP_TAG = "wikimedia-commons-app"; private static final String COMMONS_APP_TAG = "wikimedia-commons-app";
private final static String COMMONS_APP_EDIT_REASON = "Add tag for edits made using Android Commons app"; private static final String COMMONS_APP_EDIT_REASON = "Add tag for edits made using Android Commons app";
private final Context context; private final Context context;
private final WikidataEditListener wikidataEditListener; private final WikidataEditListener wikidataEditListener;
private final JsonKvStore directKvStore; private final JsonKvStore directKvStore;
private final CaptionInterface captionInterface; private final WikiBaseClient wikiBaseClient;
private final WikiBaseClient wikiBaseClient;
private final WikidataClient wikidataClient; private final WikidataClient wikidataClient;
private final CsrfTokenClient csrfTokenClient;
@Inject @Inject
public WikidataEditService(final Context context, public WikidataEditService(final Context context,
final WikidataEditListener wikidataEditListener, final WikidataEditListener wikidataEditListener,
@Named("default_preferences") final JsonKvStore directKvStore, @Named("default_preferences") final JsonKvStore directKvStore,
final WikiBaseClient wikiBaseClient, final WikiBaseClient wikiBaseClient,
final CaptionInterface captionInterface, final WikidataClient wikidataClient) {
final WikidataClient wikidataClient,
@Named("commons-csrf") final CsrfTokenClient csrfTokenClient) {
this.context = context; this.context = context;
this.wikidataEditListener = wikidataEditListener; this.wikidataEditListener = wikidataEditListener;
this.directKvStore = directKvStore; this.directKvStore = directKvStore;
this.captionInterface = captionInterface;
this.wikiBaseClient = wikiBaseClient; this.wikiBaseClient = wikiBaseClient;
this.wikidataClient = wikidataClient; this.wikidataClient = wikidataClient;
this.csrfTokenClient = csrfTokenClient;
} }
/** /**
* Create a P18 claim and log the edit with custom tag * Edits the wikibase entity by adding DEPICTS property.
* * Adding DEPICTS property requires call to the wikibase API to set tag against the entity.
* @param wikidataEntityId a unique id of each Wikidata items */
* @param fileName name of the file we will upload @SuppressLint("CheckResult")
* @param p18Value pic attribute of Wikidata item private Observable<Boolean> addDepictsProperty(final String fileEntityId,
*/ final WikidataItem depictedItem) {
public void createClaimWithLogging(String wikidataEntityId, String wikiItemName, String fileName, String p18Value) { // Wikipedia:Sandbox (Q10)
if (wikidataEntityId == null) { final String data = depictionJson(ConfigUtils.isBetaFlavour() ? "Q10" : depictedItem.getId());
Timber.d("Skipping creation of claim as Wikidata entity ID is null");
return;
}
if (fileName == null) { return wikiBaseClient.postEditEntity(PAGE_ID_PREFIX + fileEntityId, data)
Timber.d("Skipping creation of claim as fileName entity ID is null"); .doOnNext(success -> {
return; if (success) {
} Timber.d("DEPICTS property was set successfully for %s", fileEntityId);
} else {
if (!(directKvStore.getBoolean("Picture_Has_Correct_Location", true))) { Timber.d("Unable to set DEPICTS property for %s", fileEntityId);
Timber.d("Image location and nearby place location mismatched, so Wikidata item won't be edited"); }
return; })
} .doOnError( throwable -> {
Timber.e(throwable, "Error occurred while setting DEPICTS property");
if (p18Value != null && !p18Value.trim().isEmpty()) { ViewUtil.showLongToast(context, throwable.toString());
Timber.d("Skipping creation of claim as p18Value is not empty, we won't override existing image"); })
return; .subscribeOn(Schedulers.io());
}
editWikidataProperty(wikidataEntityId, wikiItemName, fileName);;
editWikiBaseDepictsProperty(wikidataEntityId, fileName);
} }
@NotNull
private String depictionJson(final String entityId) {
final JsonObject value = new JsonObject();
value.addProperty("entity-type", "item");
value.addProperty("numeric-id", entityId.replace("Q", ""));
value.addProperty("id", entityId);
final JsonObject dataValue = new JsonObject();
dataValue.add("value", value);
dataValue.addProperty("type", "wikibase-entityid");
/** final JsonObject mainSnak = new JsonObject();
* Edits the wikidata entity by adding the P18 property to it. mainSnak.addProperty("snaktype", "value");
* Adding the P18 edit requires calling the wikidata API to create a claim against the entity mainSnak.addProperty("property", WikidataProperties.DEPICTS.getPropertyName());
* mainSnak.add("datavalue", dataValue);
* @param wikidataEntityId
* @param fileName
*/
@SuppressLint("CheckResult")
private void editWikidataProperty(String wikidataEntityId, String wikiItemName, String fileName) {
Timber.d("Upload successful with wiki data entity id as %s", wikidataEntityId);
Timber.d("Attempting to edit Wikidata property %s", wikidataEntityId);
final String propertyValue = getFileName(fileName); final JsonObject claim = new JsonObject();
claim.add("mainsnak", mainSnak);
claim.addProperty("type", "statement");
claim.addProperty("rank", "preferred");
Timber.d("Entity id is %s and property value is %s", wikidataEntityId, propertyValue); final JsonArray claims = new JsonArray();
wikidataClient.createClaim(wikidataEntityId, propertyValue) claims.add(claim);
.flatMap(revisionId -> {
if (revisionId != -1) {
return wikidataClient.addEditTag(revisionId, COMMONS_APP_TAG, COMMONS_APP_EDIT_REASON);
}
throw new RuntimeException("Unable to edit wikidata item");
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(revisionId -> handleClaimResult(wikidataEntityId, wikiItemName, String.valueOf(revisionId)), throwable -> {
Timber.e(throwable, "Error occurred while making claim");
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
});
}
/** final JsonObject jsonData = new JsonObject();
* Edits the wikibase entity by adding DEPICTS property. jsonData.add("claims", claims);
* Adding DEPICTS property requires call to the wikibase API to set tag against the entity.
*
* @param wikidataEntityId
* @param fileName
*/
@SuppressLint("CheckResult")
private void editWikiBaseDepictsProperty(final String wikidataEntityId, final String fileName) {
wikiBaseClient.getFileEntityId(fileName)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(fileEntityId -> {
if (fileEntityId != null) {
Timber.d("EntityId for image was received successfully: %s", fileEntityId);
addDepictsProperty(wikidataEntityId, fileEntityId.toString());
} else {
Timber.d("Error acquiring EntityId for image: %s", fileName);
}
}, throwable -> {
Timber.e(throwable, "Error occurred while getting EntityID to set DEPICTS property");
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
});
}
@SuppressLint("CheckResult") return jsonData.toString();
private void addDepictsProperty(String entityId, final String fileEntityId) { }
if (ConfigUtils.isBetaFlavour()) {
entityId = "Q10"; // Wikipedia:Sandbox (Q10)
}
final JsonObject value = new JsonObject(); /**
value.addProperty("entity-type", "item");
value.addProperty("numeric-id", entityId.replace("Q", ""));
value.addProperty("id", entityId);
final JsonObject dataValue = new JsonObject();
dataValue.add("value", value);
dataValue.addProperty("type", "wikibase-entityid");
final JsonObject mainSnak = new JsonObject();
mainSnak.addProperty("snaktype", "value");
mainSnak.addProperty("property", BuildConfig.DEPICTS_PROPERTY);
mainSnak.add("datavalue", dataValue);
final JsonObject claim = new JsonObject();
claim.add("mainsnak", mainSnak);
claim.addProperty("type", "statement");
claim.addProperty("rank", "preferred");
final JsonArray claims = new JsonArray();
claims.add(claim);
final JsonObject jsonData = new JsonObject();
jsonData.add("claims", claims);
final String data = jsonData.toString();
Observable.defer((Callable<ObservableSource<Boolean>>) () ->
wikiBaseClient.postEditEntity(PAGE_ID_PREFIX + fileEntityId, data))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(success -> {
if (success)
Timber.d("DEPICTS property was set successfully for %s", fileEntityId);
else
Timber.d("Unable to set DEPICTS property for %s", fileEntityId);
},
throwable -> {
Timber.e(throwable, "Error occurred while setting DEPICTS property");
ViewUtil.showLongToast(context, throwable.toString());
});
}
private void handleClaimResult(String wikidataEntityId, String wikiItemName, String revisionId) {
if (revisionId != null) {
if (wikidataEditListener != null) {
wikidataEditListener.onSuccessfulWikidataEdit();
}
showSuccessToast(wikiItemName);
} else {
Timber.d("Unable to make wiki data edit for entity %s", wikidataEntityId);
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
}
}
/**
* Show a success toast when the edit is made successfully * Show a success toast when the edit is made successfully
*/ */
private void showSuccessToast(String wikiItemName) { private void showSuccessToast(final String wikiItemName) {
String successStringTemplate = context.getString(R.string.successful_wikidata_edit); final String successStringTemplate = context.getString(R.string.successful_wikidata_edit);
String successMessage = String.format(Locale.getDefault(), successStringTemplate, wikiItemName); final String successMessage = String.format(Locale.getDefault(), successStringTemplate, wikiItemName);
ViewUtil.showLongToast(context, successMessage); ViewUtil.showLongToast(context, successMessage);
} }
/** /**
* Formats and returns the filename as accepted by the wiki base API
* https://www.mediawiki.org/wiki/Wikibase/API#wbcreateclaim
*
* @param fileName
* @return
*/
private String getFileName(String fileName) {
fileName = String.format("\"%s\"", fileName.replace("File:", ""));
Timber.d("Wikidata property name is %s", fileName);
return fileName;
}
/**
* Adding captions as labels after image is successfully uploaded
*/
@SuppressLint("CheckResult")
public void createLabelforWikidataEntity(final String fileName,
final Map<String, String> captions) {
Observable.fromCallable(() -> wikiBaseClient.getFileEntityId(fileName))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(fileEntityId -> {
if (fileEntityId != null) {
for (final Map.Entry<String, String> entry : captions.entrySet()) {
final Map<String, String> caption = new HashMap<>();
caption.put(entry.getKey(), entry.getValue());
try {
wikidataAddLabels(fileEntityId.toString(), caption);
} catch (final Throwable throwable) {
throwable.printStackTrace();
}
}
} else {
Timber.d("Error acquiring EntityId for image");
}
}, throwable -> {
Timber.e(throwable, "Error occurred while getting EntityID for the file");
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
});
}
/**
* Adds label to Wikidata using the fileEntityId and the edit token, obtained from csrfTokenClient * Adds label to Wikidata using the fileEntityId and the edit token, obtained from csrfTokenClient
* *
* @param fileEntityId * @param fileEntityId
* @param caption * @return
*/ */
@SuppressLint("CheckResult") @SuppressLint("CheckResult")
private void wikidataAddLabels(final String fileEntityId, final Map<String, String> caption) { private Observable<Boolean> addCaption(final long fileEntityId, final String languageCode,
Observable.fromCallable(() -> { final String captionValue) {
try { return wikiBaseClient.addLabelstoWikidata(fileEntityId, languageCode, captionValue)
return csrfTokenClient.getTokenBlocking(); .doOnNext(mwPostResponse -> onAddCaptionResponse(fileEntityId, mwPostResponse) )
} catch (Throwable throwable) { .doOnError(throwable -> {
throwable.printStackTrace(); Timber.e(throwable, "Error occurred while setting Captions");
return null; ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
} })
}) .map(mwPostResponse -> mwPostResponse != null);
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(editToken -> {
if (editToken != null) {
Observable.fromCallable(() -> captionInterface.addLabelstoWikidata(fileEntityId, editToken, caption.get(0), caption))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(revisionId ->
{
if (revisionId != null) {
Timber.d("Caption successfully set, revision id = %s", revisionId);
} else {
Timber.d("Error occurred while setting Captions, fileEntityId = %s", fileEntityId);
}
},
throwable -> {
Timber.e(throwable, "Error occurred while setting Captions");
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
});
}else {
Timber.d("Error acquiring EntityId for image");
}
}, throwable -> {
Timber.e(throwable, "Error occurred while getting EntityID for the File");
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
});
} }
private void onAddCaptionResponse(Long fileEntityId, MwPostResponse response) {
if (response != null) {
Timber.d("Caption successfully set, revision id = %s", response);
} else {
Timber.d("Error occurred while setting Captions, fileEntityId = %s", fileEntityId);
}
}
public void createImageClaim(@Nullable final WikidataPlace wikidataPlace, final UploadResult imageUpload) {
if (!(directKvStore.getBoolean("Picture_Has_Correct_Location", true))) {
Timber.d("Image location and nearby place location mismatched, so Wikidata item won't be edited");
return;
}
editWikidataImageProperty(wikidataPlace, imageUpload);
}
@SuppressLint("CheckResult")
private void editWikidataImageProperty(final WikidataItem wikidataItem, final UploadResult imageUpload) {
wikidataClient.createImageClaim(wikidataItem, String.format("\"%s\"", imageUpload.getFilename()))
.flatMap(revisionId -> {
if (revisionId != -1) {
return wikidataClient.addEditTag(revisionId, COMMONS_APP_TAG, COMMONS_APP_EDIT_REASON);
}
throw new RuntimeException("Unable to edit wikidata item");
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(revisionId -> handleImageClaimResult(wikidataItem, String.valueOf(revisionId)), throwable -> {
Timber.e(throwable, "Error occurred while making claim");
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
});
}
private void handleImageClaimResult(final WikidataItem wikidataItem, final String revisionId) {
if (revisionId != null) {
if (wikidataEditListener != null) {
wikidataEditListener.onSuccessfulWikidataEdit();
}
showSuccessToast(wikidataItem.getName());
} else {
Timber.d("Unable to make wiki data edit for entity %s", wikidataItem);
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
}
}
public Disposable addDepictionsAndCaptions(UploadResult uploadResult, Contribution contribution) {
return wikiBaseClient.getFileEntityId(uploadResult)
.doOnError(throwable -> {
Timber.e(throwable, "Error occurred while getting EntityID to set DEPICTS property");
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
})
.subscribeOn(Schedulers.io())
.switchMap(fileEntityId -> {
if (fileEntityId != null) {
Timber.d("EntityId for image was received successfully: %s", fileEntityId);
return Observable.concat(
depictionEdits(contribution, fileEntityId),
captionEdits(contribution, fileEntityId)
);
} else {
Timber.d("Error acquiring EntityId for image: %s", uploadResult);
return Observable.empty();
}
}
).subscribe(
success -> Timber.d("edit response: %s", success),
throwable -> Timber.e(throwable, "posting edits failed")
);
}
private Observable<Boolean> captionEdits(Contribution contribution, Long fileEntityId) {
return Observable.fromIterable(contribution.getCaptions().entrySet())
.concatMap(entry -> addCaption(fileEntityId, entry.getKey(), entry.getValue()));
}
private Observable<Boolean> depictionEdits(Contribution contribution, Long fileEntityId) {
final ArrayList<WikidataItem> depictedItems = new ArrayList<>(contribution.getDepictedItems());
final WikidataPlace wikidataPlace = contribution.getWikidataPlace();
if (wikidataPlace != null) {
depictedItems.add(wikidataPlace);
}
return Observable.fromIterable(depictedItems)
.concatMap( wikidataItem -> addDepictsProperty(fileEntityId.toString(), wikidataItem));
}
} }

View file

@ -0,0 +1,18 @@
package fr.free.nrw.commons.wikidata;
import fr.free.nrw.commons.BuildConfig;
enum WikidataProperties {
IMAGE("P18"),
DEPICTS(BuildConfig.DEPICTS_PROPERTY);
private final String propertyName;
WikidataProperties(final String propertyName) {
this.propertyName = propertyName;
}
public String getPropertyName() {
return propertyName;
}
}

View file

@ -2,11 +2,26 @@ package fr.free.nrw.commons.wikidata.model
/** /**
* Model class for Depiction item returned from API after calling searchForDepicts * Model class for Depiction item returned from API after calling searchForDepicts
*/ "search": [
{
"repository": "local",
"id": "Q23444",
"concepturi": "http://www.wikidata.org/entity/Q23444",
"title": "Q23444",
"pageid": 26835,
"url": "//www.wikidata.org/wiki/Q23444",
"label": "white",
"description": "color",
"match": {
"type": "label",
"language": "en",
"text": "white"
}
}*/
class DepictSearchItem( class DepictSearchItem(
val id: String, val id: String,
val pageid: String, val pageid: String,
val url: String, val url: String,
val label: String?, val label: String,
val description: String? val description: String?
) )

View file

@ -2,22 +2,19 @@ package fr.free.nrw.commons.delete
import android.content.Context import android.content.Context
import com.nhaarman.mockitokotlin2.eq import com.nhaarman.mockitokotlin2.eq
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever
import fr.free.nrw.commons.Media import fr.free.nrw.commons.Media
import fr.free.nrw.commons.actions.PageEditClient import fr.free.nrw.commons.actions.PageEditClient
import fr.free.nrw.commons.notification.NotificationHelper
import fr.free.nrw.commons.utils.ViewUtilWrapper
import io.reactivex.Observable import io.reactivex.Observable
import org.junit.Assert.* import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.mockito.ArgumentMatchers import org.mockito.ArgumentMatchers
import org.mockito.InjectMocks
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations import org.mockito.MockitoAnnotations
import javax.inject.Inject
import javax.inject.Named
/** /**
* Tests for delete helper * Tests for delete helper
@ -25,23 +22,15 @@ import javax.inject.Named
class DeleteHelperTest { class DeleteHelperTest {
@Mock @Mock
@field:[Inject Named("commons-page-edit")] internal lateinit var pageEditClient: PageEditClient
internal var pageEditClient: PageEditClient? = null
@Mock @Mock
internal var context: Context? = null internal lateinit var context: Context
@Mock @Mock
internal var notificationHelper: NotificationHelper? = null internal lateinit var media: Media
@Mock lateinit var deleteHelper: DeleteHelper
internal var viewUtil: ViewUtilWrapper? = null
@Mock
internal var media: Media? = null
@InjectMocks
var deleteHelper: DeleteHelper? = null
/** /**
* Init mocks for test * Init mocks for test
@ -49,6 +38,7 @@ class DeleteHelperTest {
@Before @Before
fun setup() { fun setup() {
MockitoAnnotations.initMocks(this) MockitoAnnotations.initMocks(this)
deleteHelper = DeleteHelper(mock(), pageEditClient, mock(), "")
} }
/** /**
@ -56,23 +46,23 @@ class DeleteHelperTest {
*/ */
@Test @Test
fun makeDeletion() { fun makeDeletion() {
`when`(pageEditClient?.prependEdit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString())) whenever(pageEditClient.prependEdit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString()))
.thenReturn(Observable.just(true)) .thenReturn(Observable.just(true))
`when`(pageEditClient?.appendEdit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString())) whenever(pageEditClient.appendEdit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString()))
.thenReturn(Observable.just(true)) .thenReturn(Observable.just(true))
`when`(pageEditClient?.edit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString())) whenever(pageEditClient.edit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString()))
.thenReturn(Observable.just(true)) .thenReturn(Observable.just(true))
`when`(media?.displayTitle).thenReturn("Test file") whenever(media.displayTitle).thenReturn("Test file")
media?.filename="Test file.jpg"
val creatorName = "Creator" val creatorName = "Creator"
`when`(media?.getCreator()).thenReturn("$creatorName (page does not exist)") whenever(media.creator).thenReturn("$creatorName (page does not exist)")
whenever(media.filename).thenReturn("Test file.jpg")
val makeDeletion = deleteHelper?.makeDeletion(context, media, "Test reason")?.blockingGet() val makeDeletion = deleteHelper.makeDeletion(context, media, "Test reason")?.blockingGet()
assertNotNull(makeDeletion) assertNotNull(makeDeletion)
assertTrue(makeDeletion!!) assertTrue(makeDeletion!!)
verify(pageEditClient)?.appendEdit(eq("User_Talk:$creatorName"), ArgumentMatchers.anyString(), ArgumentMatchers.anyString()) verify(pageEditClient).appendEdit(eq("User_Talk:$creatorName"), ArgumentMatchers.anyString(), ArgumentMatchers.anyString())
} }
/** /**
@ -80,63 +70,63 @@ class DeleteHelperTest {
*/ */
@Test(expected = RuntimeException::class) @Test(expected = RuntimeException::class)
fun makeDeletionForPrependEditFailure() { fun makeDeletionForPrependEditFailure() {
`when`(pageEditClient?.prependEdit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString())) whenever(pageEditClient.prependEdit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString()))
.thenReturn(Observable.just(false)) .thenReturn(Observable.just(false))
`when`(pageEditClient?.appendEdit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString())) whenever(pageEditClient.appendEdit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString()))
.thenReturn(Observable.just(true)) .thenReturn(Observable.just(true))
`when`(pageEditClient?.edit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString())) whenever(pageEditClient.edit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString()))
.thenReturn(Observable.just(true)) .thenReturn(Observable.just(true))
`when`(media?.displayTitle).thenReturn("Test file") whenever(media.displayTitle).thenReturn("Test file")
`when`(media?.filename).thenReturn("Test file.jpg") whenever(media.filename).thenReturn("Test file.jpg")
`when`(media?.creator).thenReturn("Creator (page does not exist)") whenever(media.creator).thenReturn("Creator (page does not exist)")
deleteHelper?.makeDeletion(context, media, "Test reason")?.blockingGet() deleteHelper.makeDeletion(context, media, "Test reason")?.blockingGet()
} }
@Test(expected = RuntimeException::class) @Test(expected = RuntimeException::class)
fun makeDeletionForEditFailure() { fun makeDeletionForEditFailure() {
`when`(pageEditClient?.prependEdit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString())) whenever(pageEditClient.prependEdit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString()))
.thenReturn(Observable.just(true)) .thenReturn(Observable.just(true))
`when`(pageEditClient?.appendEdit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString())) whenever(pageEditClient.appendEdit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString()))
.thenReturn(Observable.just(true)) .thenReturn(Observable.just(true))
`when`(pageEditClient?.edit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString())) whenever(pageEditClient.edit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString()))
.thenReturn(Observable.just(false)) .thenReturn(Observable.just(false))
`when`(media?.displayTitle).thenReturn("Test file") whenever(media.displayTitle).thenReturn("Test file")
`when`(media?.filename).thenReturn("Test file.jpg") whenever(media.filename).thenReturn("Test file.jpg")
`when`(media?.creator).thenReturn("Creator (page does not exist)") whenever(media.creator).thenReturn("Creator (page does not exist)")
deleteHelper?.makeDeletion(context, media, "Test reason")?.blockingGet() deleteHelper.makeDeletion(context, media, "Test reason")?.blockingGet()
} }
@Test(expected = RuntimeException::class) @Test(expected = RuntimeException::class)
fun makeDeletionForAppendEditFailure() { fun makeDeletionForAppendEditFailure() {
`when`(pageEditClient?.prependEdit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString())) whenever(pageEditClient.prependEdit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString()))
.thenReturn(Observable.just(true)) .thenReturn(Observable.just(true))
`when`(pageEditClient?.appendEdit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString())) whenever(pageEditClient.appendEdit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString()))
.thenReturn(Observable.just(false)) .thenReturn(Observable.just(false))
`when`(pageEditClient?.edit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString())) whenever(pageEditClient.edit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString()))
.thenReturn(Observable.just(true)) .thenReturn(Observable.just(true))
`when`(media?.displayTitle).thenReturn("Test file") whenever(media.displayTitle).thenReturn("Test file")
`when`(media?.filename).thenReturn("Test file.jpg") whenever(media.filename).thenReturn("Test file.jpg")
`when`(media?.creator).thenReturn("Creator (page does not exist)") whenever(media.creator).thenReturn("Creator (page does not exist)")
deleteHelper?.makeDeletion(context, media, "Test reason")?.blockingGet() deleteHelper.makeDeletion(context, media, "Test reason")?.blockingGet()
} }
@Test(expected = RuntimeException::class) @Test(expected = RuntimeException::class)
fun makeDeletionForEmptyCreatorName() { fun makeDeletionForEmptyCreatorName() {
`when`(pageEditClient?.prependEdit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString())) whenever(pageEditClient.prependEdit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString()))
.thenReturn(Observable.just(true)) .thenReturn(Observable.just(true))
`when`(pageEditClient?.appendEdit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString())) whenever(pageEditClient.appendEdit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString()))
.thenReturn(Observable.just(true)) .thenReturn(Observable.just(true))
`when`(pageEditClient?.edit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString())) whenever(pageEditClient.edit(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyString()))
.thenReturn(Observable.just(true)) .thenReturn(Observable.just(true))
`when`(media?.displayTitle).thenReturn("Test file") whenever(media.displayTitle).thenReturn("Test file")
media?.filename="Test file.jpg" media.filename ="Test file.jpg"
`when`(media?.getCreator()).thenReturn(null) whenever(media.creator).thenReturn(null)
deleteHelper?.makeDeletion(context, media, "Test reason")?.blockingGet() deleteHelper.makeDeletion(context, media, "Test reason")?.blockingGet()
} }
} }

View file

@ -74,11 +74,10 @@ class UploadMediaPresenterTest {
repository.preProcessImage( repository.preProcessImage(
ArgumentMatchers.any(UploadableFile::class.java), ArgumentMatchers.any(UploadableFile::class.java),
ArgumentMatchers.any(Place::class.java), ArgumentMatchers.any(Place::class.java),
ArgumentMatchers.anyString(),
ArgumentMatchers.any(UploadMediaPresenter::class.java) ArgumentMatchers.any(UploadMediaPresenter::class.java)
) )
).thenReturn(testObservableUploadItem) ).thenReturn(testObservableUploadItem)
uploadMediaPresenter.receiveImage(uploadableFile, ArgumentMatchers.anyString(), place) uploadMediaPresenter.receiveImage(uploadableFile, place)
verify(view).showProgress(true) verify(view).showProgress(true)
testScheduler.triggerActions() testScheduler.triggerActions()
verify(view).onImageProcessed( verify(view).onImageProcessed(

View file

@ -1,12 +1,12 @@
package fr.free.nrw.commons.wikidata package fr.free.nrw.commons.wikidata
import com.nhaarman.mockitokotlin2.mock
import fr.free.nrw.commons.wikidata.model.AddEditTagResponse import fr.free.nrw.commons.wikidata.model.AddEditTagResponse
import fr.free.nrw.commons.wikidata.model.WbCreateClaimResponse
import io.reactivex.Observable import io.reactivex.Observable
import okhttp3.RequestBody
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.mockito.ArgumentMatchers.* import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.anyString
import org.mockito.InjectMocks import org.mockito.InjectMocks
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito.`when` import org.mockito.Mockito.`when`
@ -32,25 +32,29 @@ class WikidataClientTest {
`when`(mwQueryResult!!.csrfToken()).thenReturn("test_token") `when`(mwQueryResult!!.csrfToken()).thenReturn("test_token")
`when`(mwQueryResponse.query()).thenReturn(mwQueryResult) `when`(mwQueryResponse.query()).thenReturn(mwQueryResult)
`when`(wikidataInterface!!.getCsrfToken()) `when`(wikidataInterface!!.getCsrfToken())
.thenReturn(Observable.just(mwQueryResponse)) .thenReturn(Observable.just(mwQueryResponse))
} }
@Test @Test
fun createClaim() { fun createClaim() {
`when`(wikidataInterface!!.postCreateClaim(any(RequestBody::class.java), `when`(
any(RequestBody::class.java), wikidataInterface!!.postCreateClaim(
any(RequestBody::class.java), any(),
any(RequestBody::class.java), any(),
any(RequestBody::class.java), any(),
any(RequestBody::class.java))) any(),
.thenReturn(Observable.just(mock(WbCreateClaimResponse::class.java))) any(),
wikidataClient!!.createClaim("Q1", "test.jpg") any()
)
)
.thenReturn(Observable.just(mock()))
wikidataClient!!.createImageClaim(mock(), "test.jpg")
} }
@Test @Test
fun addEditTag() { fun addEditTag() {
`when`(wikidataInterface!!.addEditTag(anyString(), anyString(), anyString(), anyString())) `when`(wikidataInterface!!.addEditTag(anyString(), anyString(), anyString(), anyString()))
.thenReturn(Observable.just(mock(AddEditTagResponse::class.java))) .thenReturn(Observable.just(mock(AddEditTagResponse::class.java)))
wikidataClient!!.addEditTag(1L, "test", "test") wikidataClient!!.addEditTag(1L, "test", "test")
} }
} }

View file

@ -1,9 +1,12 @@
package fr.free.nrw.commons.wikidata package fr.free.nrw.commons.wikidata
import android.content.Context import android.content.Context
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.verifyZeroInteractions import com.nhaarman.mockitokotlin2.verifyZeroInteractions
import com.nhaarman.mockitokotlin2.whenever import com.nhaarman.mockitokotlin2.whenever
import fr.free.nrw.commons.kvstore.JsonKvStore import fr.free.nrw.commons.kvstore.JsonKvStore
import fr.free.nrw.commons.upload.UploadResult
import fr.free.nrw.commons.upload.WikidataPlace
import fr.free.nrw.commons.wikidata.model.AddEditTagResponse import fr.free.nrw.commons.wikidata.model.AddEditTagResponse
import io.reactivex.Observable import io.reactivex.Observable
import org.junit.Before import org.junit.Before
@ -37,42 +40,27 @@ class WikidataEditServiceTest {
MockitoAnnotations.initMocks(this) MockitoAnnotations.initMocks(this)
} }
@Test
fun noClaimsWhenEntityIdIsNull() {
wikidataEditService.createClaimWithLogging(null, null,"Test.jpg","")
verifyZeroInteractions(wikidataClient)
}
@Test
fun noClaimsWhenFileNameIsNull() {
wikidataEditService.createClaimWithLogging("Q1", "Test", null,"")
verifyZeroInteractions(wikidataClient)
}
@Test
fun noClaimsWhenP18IsNotEmpty() {
wikidataEditService.createClaimWithLogging("Q1", "Test","Test.jpg","Previous.jpg")
verifyZeroInteractions(wikidataClient)
}
@Test @Test
fun noClaimsWhenLocationIsNotCorrect() { fun noClaimsWhenLocationIsNotCorrect() {
whenever(directKvStore.getBoolean("Picture_Has_Correct_Location", true)) whenever(directKvStore.getBoolean("Picture_Has_Correct_Location", true))
.thenReturn(false) .thenReturn(false)
wikidataEditService.createClaimWithLogging("Q1", "", "Test.jpg", "") wikidataEditService.createImageClaim(mock(), mock())
verifyZeroInteractions(wikidataClient) verifyZeroInteractions(wikidataClient)
} }
@Test @Test
fun createClaimWithLogging() { fun createImageClaim() {
whenever(directKvStore.getBoolean("Picture_Has_Correct_Location", true)) whenever(directKvStore.getBoolean("Picture_Has_Correct_Location", true))
.thenReturn(true) .thenReturn(true)
whenever(wikidataClient.createClaim(anyString(), anyString())) whenever(wikidataClient.createImageClaim(any(), any()))
.thenReturn(Observable.just(1L)) .thenReturn(Observable.just(1L))
whenever(wikidataClient.addEditTag(anyLong(), anyString(), anyString())) whenever(wikidataClient.addEditTag(anyLong(), anyString(), anyString()))
.thenReturn(Observable.just(mock(AddEditTagResponse::class.java))) .thenReturn(Observable.just(mock(AddEditTagResponse::class.java)))
whenever(wikibaseClient.getFileEntityId(any())).thenReturn(Observable.just(1L)) whenever(wikibaseClient.getFileEntityId(any())).thenReturn(Observable.just(1L))
wikidataEditService.createClaimWithLogging("Q1", "", "Test.jpg", "") val wikidataPlace:WikidataPlace = mock()
verify(wikidataClient, times(1)).createClaim(anyString(), anyString()) val uploadResult = mock<UploadResult>()
whenever(uploadResult.filename).thenReturn("file")
wikidataEditService.createImageClaim(wikidataPlace, uploadResult)
verify(wikidataClient, times(1)).createImageClaim(wikidataPlace, """"file"""")
} }
} }