Fix 4615: Option for editing caption and description (#4672)

* DescriptionEditHelper implemented

* Description extracted

* Description editable

* No description condition handled

* Code cleanup

* Added javadocs

* toolbar added

* API call done

* Caption edit available

* Progress dialog added

* Log

* Problem with ButterKnife

* Caption is editable

* Removed unused import

* Manifest file reverted

* Manifest file reverted

* Manifest file reverted

* View binding added

* Post operation test added

* Java docs added

* Java docs added

* MediaDetailFragment unit tests added

* Test added
This commit is contained in:
Ayan Sarkar 2021-12-07 01:20:54 +05:30 committed by GitHub
parent e910b1d14f
commit 0269894c64
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 855 additions and 14 deletions

View file

@ -36,6 +36,10 @@
android:requestLegacyExternalStorage = "true"
tools:ignore="GoogleAppIndexingWarning">
<activity
android:name=".description.DescriptionEditActivity"
android:exported="true" />
<activity android:name="org.acra.dialog.CrashReportDialog"
android:process=":acra"
android:launchMode="singleInstance"

View file

@ -50,4 +50,9 @@ class MediaDataExtractor @Inject constructor(private val mediaClient: MediaClien
}
fun getHtmlOfPage(title: String) = mediaClient.getPageHtml(title);
/**
* Fetches wikitext from mediaClient
*/
fun getCurrentWikiText(title: String) = mediaClient.getCurrentWikiText(title);
}

View file

@ -64,6 +64,24 @@ class PageEditClient(
}
}
/**
* Set new labels to Wikibase server of commons
* @param summary Edit summary
* @param title Title of the page to edit
* @param language Corresponding language of label
* @param value label
* @return 1 when the edit was successful
*/
fun setCaptions(summary: String, title: String,
language: String, value: String) : Observable<Int>{
return try {
pageEditInterface.postCaptions(summary, title, language,
value, csrfTokenClient.tokenBlocking).map { it.success }
} catch (throwable: Throwable) {
Observable.just(0)
}
}
/**
* Get whole WikiText of required file
* @param title : Name of the file

View file

@ -5,6 +5,7 @@ import io.reactivex.Single
import org.wikipedia.dataclient.Service
import org.wikipedia.dataclient.mwapi.MwQueryResponse
import org.wikipedia.edit.Edit
import org.wikipedia.wikidata.Entities
import retrofit2.http.*
/**
@ -73,6 +74,18 @@ interface PageEditInterface {
@Field("token") token: String
): Observable<Edit>
@FormUrlEncoded
@Headers("Cache-Control: no-cache")
@POST(Service.MW_API_PREFIX + "action=wbsetlabel&format=json&site=commonswiki&formatversion=2")
fun postCaptions(
@Field("summary") summary: String,
@Field("title") title: String,
@Field("language") language: String,
@Field("value") value: String,
@Field("token") token: String
): Observable<Entities>
/**
* Get wiki text for provided file names
* @param titles : Name of the file

View file

@ -0,0 +1,181 @@
package fr.free.nrw.commons.description
import android.app.ProgressDialog
import android.content.Intent
import android.os.Bundle
import android.os.Parcelable
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import fr.free.nrw.commons.R
import fr.free.nrw.commons.databinding.ActivityDescriptionEditBinding
import fr.free.nrw.commons.description.EditDescriptionConstants.LIST_OF_DESCRIPTION_AND_CAPTION
import fr.free.nrw.commons.description.EditDescriptionConstants.UPDATED_WIKITEXT
import fr.free.nrw.commons.description.EditDescriptionConstants.WIKITEXT
import fr.free.nrw.commons.kvstore.JsonKvStore
import fr.free.nrw.commons.settings.Prefs
import fr.free.nrw.commons.upload.UploadMediaDetail
import fr.free.nrw.commons.upload.UploadMediaDetailAdapter
import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog
import java.util.*
import javax.inject.Inject
import javax.inject.Named
/**
* Activity for populating and editing existing description and caption
*/
class DescriptionEditActivity : AppCompatActivity(), UploadMediaDetailAdapter.EventListener {
/**
* Adapter for showing UploadMediaDetail in the activity
*/
private lateinit var uploadMediaDetailAdapter: UploadMediaDetailAdapter
/**
* For getting default preference
*/
@JvmField
@Inject
@Named("default_preferences")
var defaultKvStore: JsonKvStore? = null
/**
* Recyclerview for recycling data in views
*/
@JvmField
var rvDescriptions: RecyclerView? = null
/**
* Current wikitext
*/
var wikiText: String? = null
/**
* For showing progress dialog
*/
private var progressDialog: ProgressDialog? = null
private lateinit var binding: ActivityDescriptionEditBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityDescriptionEditBinding.inflate(layoutInflater)
setContentView(binding.root)
val bundle = intent.extras
val descriptionAndCaptions: ArrayList<UploadMediaDetail> =
bundle!!.getParcelableArrayList(LIST_OF_DESCRIPTION_AND_CAPTION)!!
wikiText = bundle.getString(WIKITEXT)
initRecyclerView(descriptionAndCaptions)
binding.btnAddDescription.setOnClickListener(::onButtonAddDescriptionClicked)
binding.btnEditSubmit.setOnClickListener(::onSubmitButtonClicked)
binding.toolbarBackButton.setOnClickListener(::onBackButtonClicked)
}
/**
* Initializes the RecyclerView
* @param descriptionAndCaptions list of description and caption
*/
private fun initRecyclerView(descriptionAndCaptions: ArrayList<UploadMediaDetail>?) {
uploadMediaDetailAdapter = UploadMediaDetailAdapter(
defaultKvStore?.getString(Prefs.DESCRIPTION_LANGUAGE, ""),
descriptionAndCaptions)
uploadMediaDetailAdapter.setCallback { titleStringID: Int, messageStringId: Int ->
showInfoAlert(
titleStringID,
messageStringId
)
}
uploadMediaDetailAdapter.setEventListener(this)
rvDescriptions = binding.rvDescriptionsCaptions
rvDescriptions!!.layoutManager = LinearLayoutManager(this)
rvDescriptions!!.adapter = uploadMediaDetailAdapter
}
/**
* show dialog with info
* @param titleStringID Title ID
* @param messageStringId Message ID
*/
private fun showInfoAlert(titleStringID: Int, messageStringId: Int) {
showAlertDialog(
this, getString(titleStringID),
getString(messageStringId), getString(android.R.string.ok),
null, true
)
}
override fun onPrimaryCaptionTextChange(isNotEmpty: Boolean) {}
private fun onBackButtonClicked(view: View) {
onBackPressed()
}
private fun onButtonAddDescriptionClicked(view: View) {
val uploadMediaDetail = UploadMediaDetail()
uploadMediaDetail.isManuallyAdded = true //This was manually added by the user
uploadMediaDetailAdapter.addDescription(uploadMediaDetail)
rvDescriptions!!.smoothScrollToPosition(uploadMediaDetailAdapter.itemCount - 1)
}
private fun onSubmitButtonClicked(view: View) {
showLoggingProgressBar()
val uploadMediaDetails = uploadMediaDetailAdapter.items
updateDescription(uploadMediaDetails)
finish()
}
/**
* Updates newly added descriptions in the wikiText and send to calling fragment
* @param uploadMediaDetails descriptions and captions
*/
private fun updateDescription(uploadMediaDetails: List<UploadMediaDetail?>) {
var descriptionIndex = wikiText!!.indexOf("description=")
if (descriptionIndex == -1) {
descriptionIndex = wikiText!!.indexOf("Description=")
}
val buffer = StringBuilder()
if (descriptionIndex != -1) {
val descriptionStart = wikiText!!.substring(0, descriptionIndex + 12)
val descriptionToEnd = wikiText!!.substring(descriptionIndex + 12)
val descriptionEndIndex = descriptionToEnd.indexOf("\n")
val descriptionEnd = wikiText!!.substring(
descriptionStart.length
+ descriptionEndIndex
)
buffer.append(descriptionStart)
for (i in uploadMediaDetails.indices) {
val uploadDetails = uploadMediaDetails[i]
if (uploadDetails!!.descriptionText != "") {
buffer.append("{{")
buffer.append(uploadDetails.languageCode)
buffer.append("|1=")
buffer.append(uploadDetails.descriptionText)
buffer.append("}}, ")
}
}
buffer.deleteCharAt(buffer.length - 1)
buffer.deleteCharAt(buffer.length - 1)
buffer.append(descriptionEnd)
}
val returningIntent = Intent()
returningIntent.putExtra(UPDATED_WIKITEXT, buffer.toString())
returningIntent.putParcelableArrayListExtra(
LIST_OF_DESCRIPTION_AND_CAPTION,
uploadMediaDetails as ArrayList<out Parcelable?>
)
setResult(RESULT_OK, returningIntent)
finish()
}
private fun showLoggingProgressBar() {
progressDialog = ProgressDialog(this)
progressDialog!!.isIndeterminate = true
progressDialog!!.setTitle(getString(R.string.updating_caption_title))
progressDialog!!.setMessage(getString(R.string.updating_caption_message))
progressDialog!!.setCanceledOnTouchOutside(false)
progressDialog!!.show()
}
}

View file

@ -0,0 +1,137 @@
package fr.free.nrw.commons.description;
import static fr.free.nrw.commons.notification.NotificationHelper.NOTIFICATION_EDIT_DESCRIPTION;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.actions.PageEditClient;
import fr.free.nrw.commons.notification.NotificationHelper;
import io.reactivex.Single;
import java.util.Objects;
import javax.inject.Inject;
import javax.inject.Named;
import timber.log.Timber;
/**
* Helper class for edit and update given descriptions and showing notification upgradation
*/
public class DescriptionEditHelper {
/**
* notificationHelper: helps creating notification
*/
private final NotificationHelper notificationHelper;
/**
* * pageEditClient: methods provided by this member posts the edited descriptions
* to the Media wiki api
*/
public final PageEditClient pageEditClient;
@Inject
public DescriptionEditHelper(final NotificationHelper notificationHelper,
@Named("commons-page-edit") final PageEditClient pageEditClient) {
this.notificationHelper = notificationHelper;
this.pageEditClient = pageEditClient;
}
/**
* Replaces new descriptions
*
* @param context context
* @param media to be added
* @param appendText to be added
* @return Observable<Boolean>
*/
public Single<Boolean> addDescription(final Context context, final Media media,
final String appendText) {
Timber.d("thread is description adding %s", Thread.currentThread().getName());
final String summary = "Updating Description";
return pageEditClient.edit(Objects.requireNonNull(media.getFilename()),
appendText, summary)
.flatMapSingle(result -> Single.just(showDescriptionEditNotification(context,
media, result)))
.firstOrError();
}
/**
* Adds new captions
*
* @param context context
* @param media to be added
* @param language to be added
* @param value to be added
* @return Observable<Boolean>
*/
public Single<Boolean> addCaption(final Context context, final Media media,
final String language, final String value) {
Timber.d("thread is caption adding %s", Thread.currentThread().getName());
final String summary = "Updating Caption";
return pageEditClient.setCaptions(summary, Objects.requireNonNull(media.getFilename()),
language, value)
.flatMapSingle(result -> Single.just(showCaptionEditNotification(context,
media, result)))
.firstOrError();
}
/**
* Update captions and shows notification about captions update
* @param context to be added
* @param media to be added
* @param result to be added
* @return boolean
*/
private boolean showCaptionEditNotification(final Context context, final Media media,
final int result) {
final String message;
String title = context.getString(R.string.caption_edit_helper_show_edit_title);
if (result == 1) {
title += ": " + context
.getString(R.string.coordinates_edit_helper_show_edit_title_success);
message = context.getString(R.string.caption_edit_helper_show_edit_message);
} else {
title += ": " + context.getString(R.string.caption_edit_helper_show_edit_title);
message = context.getString(R.string.caption_edit_helper_edit_message_else) ;
}
final String urlForFile = BuildConfig.COMMONS_URL + "/wiki/" + media.getFilename();
final Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(urlForFile));
notificationHelper.showNotification(context, title, message, NOTIFICATION_EDIT_DESCRIPTION,
browserIntent);
return result == 1;
}
/**
* Update descriptions and shows notification about descriptions update
* @param context to be added
* @param media to be added
* @param result to be added
* @return boolean
*/
private boolean showDescriptionEditNotification(final Context context, final Media media,
final boolean result) {
final String message;
String title = context.getString(R.string.description_edit_helper_show_edit_title);
if (result) {
title += ": " + context
.getString(R.string.coordinates_edit_helper_show_edit_title_success);
message = context.getString(R.string.description_edit_helper_show_edit_message);
} else {
title += ": " + context.getString(R.string.description_edit_helper_show_edit_title);
message = context.getString(R.string.description_edit_helper_edit_message_else) ;
}
final String urlForFile = BuildConfig.COMMONS_URL + "/wiki/" + media.getFilename();
final Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(urlForFile));
notificationHelper.showNotification(context, title, message, NOTIFICATION_EDIT_DESCRIPTION,
browserIntent);
return result;
}
}

View file

@ -0,0 +1,10 @@
package fr.free.nrw.commons.description
/**
* For storing required constants for editing descriptions
*/
object EditDescriptionConstants {
const val LIST_OF_DESCRIPTION_AND_CAPTION = "description.descriptionAndCaption"
const val WIKITEXT = "description.wikiText"
const val UPDATED_WIKITEXT = "description.updatedWikiText";
}

View file

@ -157,6 +157,17 @@ class MediaClient @Inject constructor(
fun resetUserNameContinuation(userName: String) =
resetUserContinuation("user_", userName)
/**
* Get whole WikiText of required file
* @param title : Name of the file
* @return Observable<MwQueryResult>
*/
fun getCurrentWikiText(title: String): Single<String?> {
return mediaDetailInterface.getWikiText(title).map {
it.query()?.pages()?.get(0)?.revisions()?.get(0)?.content()
}
}
override fun responseMapper(
networkResult: Single<MwQueryResponse>,
key: String?

View file

@ -6,6 +6,9 @@ import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static fr.free.nrw.commons.category.CategoryClientKt.CATEGORY_NEEDING_CATEGORIES;
import static fr.free.nrw.commons.category.CategoryClientKt.CATEGORY_UNCATEGORISED;
import static fr.free.nrw.commons.description.EditDescriptionConstants.LIST_OF_DESCRIPTION_AND_CAPTION;
import static fr.free.nrw.commons.description.EditDescriptionConstants.UPDATED_WIKITEXT;
import static fr.free.nrw.commons.description.EditDescriptionConstants.WIKITEXT;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
@ -71,18 +74,23 @@ import fr.free.nrw.commons.contributions.ContributionsFragment;
import fr.free.nrw.commons.coordinates.CoordinateEditHelper;
import fr.free.nrw.commons.delete.DeleteHelper;
import fr.free.nrw.commons.delete.ReasonBuilder;
import fr.free.nrw.commons.description.DescriptionEditActivity;
import fr.free.nrw.commons.description.DescriptionEditHelper;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.explore.depictions.WikidataItemDetailsActivity;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.nearby.Label;
import fr.free.nrw.commons.profile.ProfileActivity;
import fr.free.nrw.commons.ui.widget.HtmlTextView;
import fr.free.nrw.commons.upload.UploadMediaDetail;
import fr.free.nrw.commons.utils.ViewUtilWrapper;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@ -99,6 +107,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
CategoryEditHelper.Callback {
private static final int REQUEST_CODE = 1001 ;
private static final int REQUEST_CODE_EDIT_DESCRIPTION = 1002 ;
private boolean editable;
private boolean isCategoryImage;
private MediaDetailPagerFragment.MediaDetailProvider detailProvider;
@ -136,6 +145,8 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
@Inject
CoordinateEditHelper coordinateEditHelper;
@Inject
DescriptionEditHelper descriptionEditHelper;
@Inject
ViewUtilWrapper viewUtil;
@Inject
CategoryClient categoryClient;
@ -225,6 +236,10 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
String descriptionHtmlCode;
@BindView(R.id.progressBarDeletion)
ProgressBar progressBarDeletion;
@BindView(R.id.progressBarEdit)
ProgressBar progressBarEditDescription;
@BindView(R.id.description_edit)
Button editDescription;
private ArrayList<String> categoryNames = new ArrayList<>();
private String categorySearchQuery;
@ -819,8 +834,155 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
.build(getActivity()), REQUEST_CODE);
}
@OnClick(R.id.description_edit)
public void onDescriptionEditClicked() {
progressBarEditDescription.setVisibility(VISIBLE);
editDescription.setVisibility(GONE);
getDescriptionList();
}
/**
* Get the coordinates and update the existing coordinates.
* Gets descriptions from wikitext
*/
private void getDescriptionList() {
compositeDisposable.add(mediaDataExtractor.getCurrentWikiText(
Objects.requireNonNull(media.getFilename()))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::extractCaptionDescription, Timber::e));
}
/**
* Gets captions and descriptions and merge them according to language code and arranges it in a
* single list.
* Send the list to DescriptionEditActivity
* @param s wikitext
*/
private void extractCaptionDescription(final String s) {
final LinkedHashMap<String,String> descriptions = getDescriptions(s);
final LinkedHashMap<String,String> captions = getCaptionsList();
final ArrayList<UploadMediaDetail> descriptionAndCaptions = new ArrayList<>();
if(captions.size() >= descriptions.size()) {
for (final Map.Entry mapElement : captions.entrySet()) {
final String language = (String) mapElement.getKey();
if (descriptions.containsKey(language)) {
descriptionAndCaptions.add(
new UploadMediaDetail(language,
Objects.requireNonNull(descriptions.get(language)),
(String) mapElement.getValue())
);
} else {
descriptionAndCaptions.add(
new UploadMediaDetail(language, "",
(String) mapElement.getValue())
);
}
}
for (final Map.Entry mapElement : descriptions.entrySet()) {
final String language = (String) mapElement.getKey();
if (!captions.containsKey(language)) {
descriptionAndCaptions.add(
new UploadMediaDetail(language,
Objects.requireNonNull(descriptions.get(language)),
"")
);
}
}
} else {
for (final Map.Entry mapElement : descriptions.entrySet()) {
final String language = (String) mapElement.getKey();
if (captions.containsKey(language)) {
descriptionAndCaptions.add(
new UploadMediaDetail(language, (String) mapElement.getValue(),
Objects.requireNonNull(captions.get(language)))
);
} else {
descriptionAndCaptions.add(
new UploadMediaDetail(language, (String) mapElement.getValue(),
"")
);
}
}
for (final Map.Entry mapElement : captions.entrySet()) {
final String language = (String) mapElement.getKey();
if (!descriptions.containsKey(language)) {
descriptionAndCaptions.add(
new UploadMediaDetail(language,
"",
Objects.requireNonNull(descriptions.get(language)))
);
}
}
}
final Intent intent = new Intent(requireContext(), DescriptionEditActivity.class);
final Bundle bundle = new Bundle();
bundle.putParcelableArrayList(LIST_OF_DESCRIPTION_AND_CAPTION, descriptionAndCaptions);
bundle.putString(WIKITEXT, s);
intent.putExtras(bundle);
startActivityForResult(intent, REQUEST_CODE_EDIT_DESCRIPTION);
}
/**
* Filters descriptions from current wikiText and arranges it in LinkedHashmap according to the
* language code
* @param s wikitext
* @return LinkedHashMap<LanguageCode,Description>
*/
private LinkedHashMap<String,String> getDescriptions(String s) {
int descriptionIndex = s.indexOf("description=");
if(descriptionIndex == -1){
descriptionIndex = s.indexOf("Description=");
}
if( descriptionIndex == -1 ){
return new LinkedHashMap<>();
}
final String descriptionToEnd = s.substring(descriptionIndex+12);
final int descriptionEndIndex = descriptionToEnd.indexOf("\n");
final String description = s.substring(descriptionIndex+12, descriptionIndex+12+descriptionEndIndex);
final String[] arr = description.trim().split(",");
final LinkedHashMap<String,String> descriptionList = new LinkedHashMap<>();
if (!description.equals("")) {
for (final String string :
arr) {
final int startCode = string.indexOf("{{");
final int endCode = string.indexOf("|");
final String languageCode = string.substring(startCode + 2, endCode).trim();
final int startDescription = string.indexOf("=");
final int endDescription = string.indexOf("}}");
final String languageDescription = string
.substring(startDescription + 1, endDescription);
descriptionList.put(languageCode, languageDescription);
}
}
return descriptionList;
}
/**
* Gets list of caption and arranges it in a LinkedHashmap according to the language code
* @return LinkedHashMap<LanguageCode,Caption>
*/
private LinkedHashMap<String,String> getCaptionsList() {
final LinkedHashMap<String, String> captionList = new LinkedHashMap<>();
final Map<String, String> captions = media.getCaptions();
for (final Map.Entry<String, String> map : captions.entrySet()) {
final String language = map.getKey();
final String languageCaption = map.getValue();
captionList.put(language, languageCaption);
}
return captionList;
}
/**
* Get the result from another activity and act accordingly.
* @param requestCode
* @param resultCode
* @param data
@ -854,13 +1016,61 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
updateCoordinates(latitude, longitude, accuracy);
}
}
} else if (resultCode == RESULT_CANCELED) {
} else if (requestCode == REQUEST_CODE_EDIT_DESCRIPTION && resultCode == RESULT_OK) {
final String updatedWikiText = data.getStringExtra(UPDATED_WIKITEXT);
compositeDisposable.add(descriptionEditHelper.addDescription(getContext(), media,
updatedWikiText)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(s -> {
Timber.d("Descriptions are added.");
}));
final ArrayList<UploadMediaDetail> uploadMediaDetails
= data.getParcelableArrayListExtra(LIST_OF_DESCRIPTION_AND_CAPTION);
LinkedHashMap<String, String> updatedCaptions = new LinkedHashMap<>();
for (UploadMediaDetail mediaDetail:
uploadMediaDetails) {
compositeDisposable.add(descriptionEditHelper.addCaption(getContext(), media,
mediaDetail.getLanguageCode(), mediaDetail.getCaptionText())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(s -> {
updateCaptions(mediaDetail, updatedCaptions);
Timber.d("Caption is added.");
}));
}
progressBarEditDescription.setVisibility(GONE);
editDescription.setVisibility(VISIBLE);
} else if (requestCode == REQUEST_CODE && resultCode == RESULT_CANCELED) {
viewUtil.showShortToast(getContext(),
Objects.requireNonNull(getContext())
.getString(R.string.coordinates_picking_unsuccessful));
} else if (requestCode == REQUEST_CODE_EDIT_DESCRIPTION && resultCode == RESULT_CANCELED) {
progressBarEditDescription.setVisibility(GONE);
editDescription.setVisibility(VISIBLE);
viewUtil.showShortToast(getContext(),
Objects.requireNonNull(getContext())
.getString(R.string.descriptions_picking_unsuccessful));
}
}
/**
* Adds caption to the map and updates captions
* @param mediaDetail UploadMediaDetail
* @param updatedCaptions updated captionds
*/
private void updateCaptions(UploadMediaDetail mediaDetail,
LinkedHashMap<String, String> updatedCaptions) {
updatedCaptions.put(mediaDetail.getLanguageCode(), mediaDetail.getCaptionText());
media.setCaptions(updatedCaptions);
}
@OnClick(R.id.update_categories_button)
public void onUpdateCategoriesClicked() {
updateCategories(categoryEditSearchRecyclerViewAdapter.getNewCategories());

View file

@ -2,6 +2,8 @@ package fr.free.nrw.commons.media;
import io.reactivex.Observable;
import io.reactivex.Single;
import org.wikipedia.dataclient.Service;
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
import org.wikipedia.wikidata.Entities;
import retrofit2.Call;
import retrofit2.http.GET;
@ -35,4 +37,18 @@ public interface MediaDetailInterface {
*/
@GET("/w/api.php?action=wbgetentities&props=labels&format=json&languagefallback=1&sites=commonswiki")
Observable<Entities> getEntityForImage(@Query("languages") String language, @Query("ids") String wikibaseIdentifier);
/**
* Fetches current wikitext
* @param title file name
* @return Single<MwQueryResponse>
*/
@GET(
Service.MW_API_PREFIX +
"action=query&prop=revisions&rvprop=content|timestamp&rvlimit=1&converttitles="
)
Single<MwQueryResponse> getWikiText(
@Query("titles") String title
);
}

View file

@ -26,6 +26,7 @@ public class NotificationHelper {
public static final int NOTIFICATION_DELETE = 1;
public static final int NOTIFICATION_EDIT_CATEGORY = 2;
public static final int NOTIFICATION_EDIT_COORDINATES = 3;
public static final int NOTIFICATION_EDIT_DESCRIPTION = 4;
private NotificationManager notificationManager;
private NotificationCompat.Builder notificationBuilder;

View file

@ -1,10 +1,13 @@
package fr.free.nrw.commons.upload
import android.os.Parcelable
import fr.free.nrw.commons.nearby.Place
import kotlinx.android.parcel.Parcelize
/**
* Holds a description of an item being uploaded by [UploadActivity]
*/
@Parcelize
data class UploadMediaDetail constructor(
/**
* @return The language code ie. "en" or "fr"
@ -15,7 +18,7 @@ data class UploadMediaDetail constructor(
var languageCode: String? = null,
var descriptionText: String = "",
var captionText: String = ""
) {
) : Parcelable {
fun javaCopy() = copy()
constructor(place: Place) : this(

View file

@ -37,6 +37,13 @@ public class UploadMediaDetailAdapter extends RecyclerView.Adapter<UploadMediaDe
this.savedLanguageValue = savedLanguageValue;
}
public UploadMediaDetailAdapter(final String savedLanguageValue,
List<UploadMediaDetail> uploadMediaDetails) {
this.uploadMediaDetails = uploadMediaDetails;
selectedLanguages = new HashMap<>();
this.savedLanguageValue = savedLanguageValue;
}
public void setCallback(Callback callback) {
this.callback = callback;
}
@ -51,6 +58,10 @@ public class UploadMediaDetailAdapter extends RecyclerView.Adapter<UploadMediaDe
notifyDataSetChanged();
}
public List<UploadMediaDetail> getItems(){
return uploadMediaDetails;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
@ -178,14 +189,14 @@ public class UploadMediaDetailAdapter extends RecyclerView.Adapter<UploadMediaDe
long l) {
description.setSelectedLanguageIndex(position);
String languageCode = ((SpinnerLanguagesAdapter) adapterView.getAdapter())
.getLanguageCode(position);
.getLanguageCode(position);
description.setLanguageCode(languageCode);
selectedLanguages.remove(adapterView);
selectedLanguages.put(adapterView, languageCode);
((SpinnerLanguagesAdapter) adapterView
.getAdapter()).setSelectedLangCode(languageCode);
.getAdapter()).setSelectedLangCode(languageCode);
spinnerDescriptionLanguages.setSelection(position);
Timber.d("Description language code is: "+languageCode);
Timber.d("Description language code is: " + languageCode);
}
@Override
@ -198,8 +209,16 @@ public class UploadMediaDetailAdapter extends RecyclerView.Adapter<UploadMediaDe
if (!TextUtils.isEmpty(savedLanguageValue)) {
// If user has chosen a default language from settings activity
// savedLanguageValue is not null
if(!TextUtils.isEmpty(description.getLanguageCode())) {
spinnerDescriptionLanguages.setSelection(languagesAdapter
.getIndexOfLanguageCode(description.getLanguageCode()));
} else {
spinnerDescriptionLanguages.setSelection(languagesAdapter
.getIndexOfLanguageCode(savedLanguageValue));
}
} else if (!TextUtils.isEmpty(description.getLanguageCode())) {
spinnerDescriptionLanguages.setSelection(languagesAdapter
.getIndexOfLanguageCode(savedLanguageValue));
.getIndexOfLanguageCode(description.getLanguageCode()));
} else {
//Checking whether Language Code attribute is null or not.
if (uploadMediaDetails.get(position).getLanguageCode() != null) {

View file

@ -0,0 +1,80 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".description.DescriptionEditActivity">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/primaryColor"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/toolbar_text_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="72dp"
android:text="@string/description_activity_title"
android:textColor="@color/white"
android:textSize="20sp"
android:layout_marginTop="15dp"
android:layout_marginBottom="15dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<ImageView
android:id="@+id/toolbar_back_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:contentDescription="@string/exit_location_picker"
android:tint="@color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_arrow_back_white" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_descriptions_captions"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/edit_bottom_layout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar" />
<RelativeLayout
android:id="@+id/edit_bottom_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btn_add_description"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="@dimen/fragment_height"
android:text="+" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btn_edit_submit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:text="@string/submit"
android:textColor="@android:color/white" />
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -204,16 +204,44 @@
android:textSize="@dimen/description_text_size"
android:textIsSelectable="true"
tools:text="Description of the media goes here. This can potentially be fairly long, and will need to wrap across multiple lines. We hope it looks nice though." />
</LinearLayout>
<TextView
android:id="@+id/show_caption_description_textview"
style="@style/MediaDetailTextLabelGeneric"
<LinearLayout
android:paddingStart="@dimen/quarter_standard_height"
android:paddingEnd="@dimen/quarter_standard_height"
android:layout_width="match_parent"
android:gravity="center"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/dimen_10"
android:text="@string/media_detail_in_all_languages" />
android:orientation="horizontal">
<TextView
android:id="@+id/show_caption_description_textview"
style="@style/MediaDetailTextLabelGeneric"
android:layout_width="match_parent"
android:gravity="center"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/dimen_10"
android:text="@string/media_detail_in_all_languages" />
<Button
android:id="@+id/description_edit"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="end"
android:background="@drawable/ic_baseline_edit_24" />
<ProgressBar
android:id="@+id/progressBarEdit"
style="?android:progressBarStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/tiny_gap"
android:layout_marginRight="@dimen/tiny_gap"
android:layout_gravity="center_vertical|end"
android:indeterminate="true"
android:indeterminateOnly="true"
android:visibility="gone"
tools:visibility="visible"/>
</LinearLayout>
<View
android:background="?attr/mediaDetailSpacerColor"

View file

@ -46,6 +46,8 @@
<string name="signup">Sign up</string>
<string name="logging_in_title">Logging in</string>
<string name="logging_in_message">Please wait…</string>
<string name="updating_caption_title">Updating captions and descriptions</string>
<string name="updating_caption_message">Please wait…</string>
<string name="login_success">Login success!</string>
<string name="login_failed">Login failed!</string>
<string name="upload_failed">File not found. Please try another file.</string>
@ -371,7 +373,7 @@
<string name="next">Next</string>
<string name="previous">Previous</string>
<string name="submit">Submit</string>
<string formatted="true" name="upload_title_duplicate">A file with the file name %1$s exists. Are you sure you want to proceed?\n\nNote: A suitable suffix will be added to the file name automatically.</string>
<string name="upload_title_duplicate" formatted="true">A file with the file name %1$s exists. Are you sure you want to proceed?\n\nNote: A suitable suffix will be added to the file name automatically.</string>
<string name="map_application_missing">No compatible map application could be found on your device. Please install a map application to use this feature.</string>
<string name="title_page_bookmarks_pictures">Pictures</string>
<string name="title_page_bookmarks_locations">Locations</string>
@ -521,10 +523,18 @@ Upload your first media by tapping on the add button.</string>
<string name="coordinates_edit_helper_make_edit_toast">Trying to update coordinates.</string>
<string name="coordinates_edit_helper_show_edit_title">Coordinates update</string>
<string name="description_edit_helper_show_edit_title">Description update</string>
<string name="caption_edit_helper_show_edit_title">Caption update</string>
<string name="coordinates_edit_helper_show_edit_title_success">Success</string>
<string name="coordinates_edit_helper_show_edit_message">Coordinates %1$s are added.</string>
<string name="description_edit_helper_show_edit_message">Descriptions are added.</string>
<string name="caption_edit_helper_show_edit_message">Caption is added.</string>
<string name="coordinates_edit_helper_edit_message_else">Could not add coordinates.</string>
<string name="description_edit_helper_edit_message_else">Could not add descriptions.</string>
<string name="caption_edit_helper_edit_message_else">Could not add caption.</string>
<string name="coordinates_picking_unsuccessful">Unable to get coordinates.</string>
<string name="descriptions_picking_unsuccessful">Unable to get descriptions.</string>
<string name="description_activity_title">Edit descriptions and captions</string>
<string name="share_image_via">Share image via</string>
<string name="you_have_no_achievements_yet">You haven\'t made any contributions yet</string>

View file

@ -1,5 +1,6 @@
package fr.free.nrw.commons
import com.nhaarman.mockitokotlin2.whenever
import fr.free.nrw.commons.media.MediaClient
import io.reactivex.Single
import org.junit.Assert.assertTrue
@ -11,6 +12,7 @@ import org.mockito.Mock
import org.mockito.Mockito.`when`
import org.mockito.Mockito.mock
import org.mockito.MockitoAnnotations
import org.wikipedia.dataclient.mwapi.MwQueryResponse
/**
* Test methods in media data extractor
@ -49,4 +51,10 @@ class MediaDataExtractorTest {
//assertTrue(fetchMediaDetails is Media)
}
@Test
fun getWikiText() {
`when`(mediaDataExtractor?.getCurrentWikiText(ArgumentMatchers.anyString()))
.thenReturn(Single.just("Test"))
}
}

View file

@ -81,4 +81,15 @@ class PageEditClientTest {
pageEditClient.prependEdit("test", "test", "test")
verify(pageEditInterface).postPrependEdit(eq("test"), eq("test"), eq("test"), eq("test"))
}
/**
* Test setCaptions
*/
@Test
fun testSetCaptions() {
Mockito.`when`(csrfTokenClient.tokenBlocking).thenReturn("test")
pageEditClient.setCaptions("test", "test", "en", "test")
verify(pageEditInterface).postCaptions(eq("test"), eq("test"), eq("en"),
eq("test"), eq("test"))
}
}

View file

@ -199,6 +199,12 @@ class MediaClientTest {
mediaClient.doesPageContainMedia("").test().assertValue(true)
}
@Test
fun getWikiText() {
val wikiText = mock<MwQueryResponse>()
whenever(mediaDetailInterface.getWikiText("File:Test.jpg")).thenReturn(Single.just(wikiText))
}
private fun mockQuery(queryReceiver: MwQueryResult.() -> Unit): MwQueryResponse {
val mwQueryResponse = mock<MwQueryResponse>()
val mwQueryResult = mock<MwQueryResult>()

View file

@ -1,6 +1,8 @@
package fr.free.nrw.commons.media
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle
import android.view.LayoutInflater
@ -49,6 +51,9 @@ import kotlin.collections.HashMap
@LooperMode(LooperMode.Mode.PAUSED)
class MediaDetailFragmentUnitTests {
private val REQUEST_CODE = 1001
private val REQUEST_CODE_EDIT_DESCRIPTION = 1002
private lateinit var fragment: MediaDetailFragment
private lateinit var fragmentManager: FragmentManager
private lateinit var layoutInflater: LayoutInflater
@ -106,6 +111,9 @@ class MediaDetailFragmentUnitTests {
@Mock
private lateinit var searchView: SearchView
@Mock
private lateinit var intent: Intent
@Before
fun setUp() {
@ -140,6 +148,7 @@ class MediaDetailFragmentUnitTests {
Whitebox.setInternalState(fragment, "media", media)
Whitebox.setInternalState(fragment, "progressBar", progressBar)
Whitebox.setInternalState(fragment, "progressBarEditDescription", progressBar)
Whitebox.setInternalState(fragment, "captionsListView", listView)
Whitebox.setInternalState(fragment, "descriptionWebView", webView)
Whitebox.setInternalState(fragment, "detailProvider", detailProvider)
@ -159,6 +168,7 @@ class MediaDetailFragmentUnitTests {
Whitebox.setInternalState(fragment, "dummyCategoryEditContainer", linearLayout)
Whitebox.setInternalState(fragment, "showCaptionAndDescriptionContainer", linearLayout)
Whitebox.setInternalState(fragment, "updateCategoriesButton", button)
Whitebox.setInternalState(fragment, "editDescription", button)
Whitebox.setInternalState(fragment, "categoryContainer", linearLayout)
Whitebox.setInternalState(fragment, "categorySearchView", searchView)
Whitebox.setInternalState(fragment, "mediaDiscussion", textView)
@ -188,6 +198,24 @@ class MediaDetailFragmentUnitTests {
fragment.onCreateView(layoutInflater, null, savedInstanceState)
}
@Test
@Throws(Exception::class)
fun testOnActivityResultLocationPickerActivity() {
fragment.onActivityResult(REQUEST_CODE, Activity.RESULT_CANCELED, intent)
}
@Test
@Throws(Exception::class)
fun `test OnActivity Result Cancelled LocationPickerActivity`() {
fragment.onActivityResult(REQUEST_CODE, Activity.RESULT_CANCELED, intent)
}
@Test
@Throws(Exception::class)
fun `test OnActivity Result Cancelled DescriptionEditActivity`() {
fragment.onActivityResult(REQUEST_CODE, Activity.RESULT_OK, intent)
}
@Test
@Throws(Exception::class)
fun testOnSaveInstanceState() {
@ -252,6 +280,17 @@ class MediaDetailFragmentUnitTests {
method.invoke(fragment)
}
@Test
@Throws(Exception::class)
fun testGetDescriptionList() {
`when`(media.filename).thenReturn("")
val method: Method = MediaDetailFragment::class.java.getDeclaredMethod(
"getDescriptionList"
)
method.isAccessible = true
method.invoke(fragment)
}
@Test
@Throws(Exception::class)
fun testGetCaptions() {
@ -301,6 +340,15 @@ class MediaDetailFragmentUnitTests {
Assert.assertEquals(fragment.updateCategoryDisplay(listOf()), true)
}
@Test
@Throws(Exception::class)
fun testDescriptionEditClicked() {
`when`(progressBar.visibility).thenReturn(View.VISIBLE)
`when`(button.visibility).thenReturn(View.GONE)
`when`(media.filename).thenReturn("")
fragment.onDescriptionEditClicked()
}
@Test
@Throws(Exception::class)
fun testShowCaptionAndDescriptionCaseVisible() {
@ -397,6 +445,28 @@ class MediaDetailFragmentUnitTests {
method.invoke(fragment, "mock")
}
@Test
@Throws(Exception::class)
fun testExtractCaptionDescription() {
val method: Method = MediaDetailFragment::class.java.getDeclaredMethod(
"extractCaptionDescription",
String::class.java
)
method.isAccessible = true
method.invoke(fragment, "mock")
}
@Test
@Throws(Exception::class)
fun testGetDescriptions() {
val method: Method = MediaDetailFragment::class.java.getDeclaredMethod(
"getDescriptions",
String::class.java
)
method.isAccessible = true
method.invoke(fragment, "mock")
}
@Test
@Throws(Exception::class)
fun testPrettyCaptionCaseEmpty() {