Fixes #3861 Use the APIs to fetch leaderboard’s based on uploads via mobile app (all time) and display it in the Leaderboard screen. (#3865)

This commit is contained in:
Madhur Gupta 2020-07-08 19:51:39 +05:30 committed by GitHub
parent eec0f14cb5
commit 196b9141d2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 610 additions and 3 deletions

View file

@ -85,11 +85,13 @@ public class NetworkingModule {
public OkHttpJsonApiClient provideOkHttpJsonApiClient(OkHttpClient okHttpClient,
DepictsClient depictsClient,
@Named("tools_forge") HttpUrl toolsForgeUrl,
@Named("test_tools_forge") HttpUrl testToolsForgeUrl,
@Named("default_preferences") JsonKvStore defaultKvStore,
Gson gson) {
return new OkHttpJsonApiClient(okHttpClient,
depictsClient,
toolsForgeUrl,
testToolsForgeUrl,
WIKIDATA_SPARQL_QUERY_URL,
BuildConfig.WIKIMEDIA_CAMPAIGNS_URL,
gson);
@ -124,6 +126,14 @@ public class NetworkingModule {
return HttpUrl.parse(TOOLS_FORGE_URL);
}
@Provides
@Named("test_tools_forge")
@NonNull
@SuppressWarnings("ConstantConditions")
public HttpUrl provideTestToolsForgeUrl() {
return HttpUrl.parse(TEST_TOOLS_FORGE_URL);
}
@Provides
@Singleton
@Named(NAMED_COMMONS_WIKI_SITE)

View file

@ -11,6 +11,7 @@ import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.nearby.model.NearbyResponse;
import fr.free.nrw.commons.nearby.model.NearbyResultItem;
import fr.free.nrw.commons.profile.leaderboard.LeaderboardResponse;
import fr.free.nrw.commons.upload.FileUtils;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import fr.free.nrw.commons.utils.ConfigUtils;
@ -40,6 +41,7 @@ public class OkHttpJsonApiClient {
private final OkHttpClient okHttpClient;
private final DepictsClient depictsClient;
private final HttpUrl wikiMediaToolforgeUrl;
private final HttpUrl wikiMediaTestToolforgeUrl;
private final String sparqlQueryUrl;
private final String campaignsUrl;
private final Gson gson;
@ -49,17 +51,58 @@ public class OkHttpJsonApiClient {
public OkHttpJsonApiClient(OkHttpClient okHttpClient,
DepictsClient depictsClient,
HttpUrl wikiMediaToolforgeUrl,
HttpUrl wikiMediaTestToolforgeUrl,
String sparqlQueryUrl,
String campaignsUrl,
Gson gson) {
this.okHttpClient = okHttpClient;
this.depictsClient = depictsClient;
this.wikiMediaToolforgeUrl = wikiMediaToolforgeUrl;
this.wikiMediaTestToolforgeUrl = wikiMediaTestToolforgeUrl;
this.sparqlQueryUrl = sparqlQueryUrl;
this.campaignsUrl = campaignsUrl;
this.gson = gson;
}
@NonNull
public Single<LeaderboardResponse> getLeaderboard(String userName, String duration, String category, String limit, String offset) {
final String fetchLeaderboardUrlTemplate = wikiMediaTestToolforgeUrl
+ "/leaderboard.py";
return Single.fromCallable(() -> {
String url = String.format(Locale.ENGLISH,
fetchLeaderboardUrlTemplate,
userName,
duration,
category,
limit,
offset);
HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
urlBuilder.addQueryParameter("user", userName);
urlBuilder.addQueryParameter("duration", duration);
urlBuilder.addQueryParameter("category", category);
urlBuilder.addQueryParameter("limit", limit);
urlBuilder.addQueryParameter("offset", offset);
Timber.i("Url %s", urlBuilder.toString());
Request request = new Request.Builder()
.url(urlBuilder.toString())
.build();
Response response = okHttpClient.newCall(request).execute();
if (response != null && response.body() != null && response.isSuccessful()) {
String json = response.body().string();
if (json == null) {
return null;
}
Timber.d("Response for leaderboard is %s", json);
try {
return gson.fromJson(json, LeaderboardResponse.class);
} catch (Exception e) {
return new LeaderboardResponse();
}
}
return null;
});
}
@NonNull
public Single<Integer> getUploadCount(String userName) {
HttpUrl.Builder urlBuilder = wikiMediaToolforgeUrl.newBuilder();

View file

@ -1,18 +1,162 @@
package fr.free.nrw.commons.profile.leaderboard;
import android.accounts.Account;
import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import butterknife.BindView;
import butterknife.ButterKnife;
import com.facebook.drawee.view.SimpleDraweeView;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
import java.util.List;
import java.util.Objects;
import javax.inject.Inject;
import timber.log.Timber;
public class LeaderboardFragment extends CommonsDaggerSupportFragment {
@BindView(R.id.avatar)
SimpleDraweeView avatar;
@BindView(R.id.username)
TextView username;
@BindView(R.id.rank)
TextView rank;
@BindView(R.id.count)
TextView count;
@BindView(R.id.leaderboard_list)
RecyclerView leaderboardListRecyclerView;
@BindView(R.id.progressBar)
ProgressBar progressBar;
@Inject
SessionManager sessionManager;
@Inject
OkHttpJsonApiClient okHttpJsonApiClient;
private String avatarSourceURL = "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0a/%s/1024px-%s.png";
private CompositeDisposable compositeDisposable = new CompositeDisposable();
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_leaderboard, container, false);
ButterKnife.bind(this, rootView);
progressBar.setVisibility(View.VISIBLE);
hideLayouts();
setLeaderboard();
return rootView;
}
/**
* To call the API to get results in form Single<JSONObject>
* which then calls parseJson when results are fetched
*/
private void setLeaderboard() {
if (checkAccount()) {
try{
compositeDisposable.add(okHttpJsonApiClient
.getLeaderboard(Objects.requireNonNull(sessionManager.getCurrentAccount()).name,
"all_time", "upload", null, null)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
response -> {
if (response != null && response.getStatus() == 200) {
setLeaderboardUser(response);
setLeaderboardList(response.getLeaderboardList());
}
},
t -> {
Timber.e(t, "Fetching leaderboard statistics failed");
onError();
}
));
}
catch (Exception e){
Timber.d(e+"success");
}
}
}
private void setLeaderboardUser(LeaderboardResponse response) {
hideProgressBar();
avatar.setImageURI(
Uri.parse(String.format(avatarSourceURL, response.getAvatar(), response.getAvatar())));
username.setText(response.getUsername());
rank.setText(String.format("%s %d", getString(R.string.rank_prefix), response.getRank()));
count.setText(String.format("%s %d", getString(R.string.count_prefix), response.getCategoryCount()));
}
private void setLeaderboardList(List<LeaderboardList> leaderboardList) {
LeaderboardListAdapter leaderboardListAdapter = new LeaderboardListAdapter(leaderboardList);
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(getContext());
leaderboardListRecyclerView.setLayoutManager(linearLayoutManager);
leaderboardListRecyclerView.setAdapter(leaderboardListAdapter);
}
/**
* to hide progressbar
*/
private void hideProgressBar() {
if (progressBar != null) {
progressBar.setVisibility(View.GONE);
avatar.setVisibility(View.VISIBLE);
username.setVisibility(View.VISIBLE);
rank.setVisibility(View.VISIBLE);
leaderboardListRecyclerView.setVisibility(View.VISIBLE);
}
}
/**
* used to hide the layouts while fetching results from api
*/
private void hideLayouts(){
avatar.setVisibility(View.INVISIBLE);
username.setVisibility(View.INVISIBLE);
rank.setVisibility(View.INVISIBLE);
leaderboardListRecyclerView.setVisibility(View.INVISIBLE);
}
/**
* check to ensure that user is logged in
* @return
*/
private boolean checkAccount(){
Account currentAccount = sessionManager.getCurrentAccount();
if (currentAccount == null) {
Timber.d("Current account is null");
ViewUtil.showLongToast(getActivity(), getResources().getString(R.string.user_not_logged_in));
sessionManager.forceLogin(getActivity());
return false;
}
return true;
}
/**
* Shows a generic error toast when error occurs while loading leaderboard
*/
private void onError() {
ViewUtil.showLongToast(getActivity(), getResources().getString(R.string.error_occurred));
progressBar.setVisibility(View.GONE);
}
}

View file

@ -0,0 +1,53 @@
package fr.free.nrw.commons.profile.leaderboard;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
public class LeaderboardList {
@SerializedName("username")
@Expose
private String username;
@SerializedName("category_count")
@Expose
private Integer categoryCount;
@SerializedName("avatar")
@Expose
private String avatar;
@SerializedName("rank")
@Expose
private Integer rank;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Integer getCategoryCount() {
return categoryCount;
}
public void setCategoryCount(Integer categoryCount) {
this.categoryCount = categoryCount;
}
public String getAvatar() {
return avatar;
}
public void setAvatar(String avatar) {
this.avatar = avatar;
}
public Integer getRank() {
return rank;
}
public void setRank(Integer rank) {
this.rank = rank;
}
}

View file

@ -0,0 +1,73 @@
package fr.free.nrw.commons.profile.leaderboard;
import android.content.Context;
import android.net.Uri;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.facebook.drawee.view.SimpleDraweeView;
import fr.free.nrw.commons.R;
import java.util.List;
public class LeaderboardListAdapter extends RecyclerView.Adapter<LeaderboardListAdapter.ListViewHolder> {
private List<LeaderboardList> leaderboardList;
private String avatarSourceURL = "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0a/%s/1024px-%s.png";
public class ListViewHolder extends RecyclerView.ViewHolder {
TextView rank;
SimpleDraweeView avatar;
TextView username;
TextView count;
public ListViewHolder(View itemView) {
super(itemView);
this.rank = itemView.findViewById(R.id.user_rank);
this.avatar = itemView.findViewById(R.id.user_avatar);
this.username = itemView.findViewById(R.id.user_name);
this.count = itemView.findViewById(R.id.user_count);
}
public Context getContext() {
return itemView.getContext();
}
}
public LeaderboardListAdapter(List<LeaderboardList> leaderboardList) {
this.leaderboardList = leaderboardList;
}
@NonNull
@Override
public LeaderboardListAdapter.ListViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.leaderboard_list_element, parent, false);
return new ListViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull LeaderboardListAdapter.ListViewHolder holder, int position) {
TextView rank = holder.rank;
SimpleDraweeView avatar = holder.avatar;
TextView username = holder.username;
TextView count = holder.count;
rank.setText(leaderboardList.get(position).getRank().toString());
avatar.setImageURI(
Uri.parse(String.format(avatarSourceURL, leaderboardList.get(position).getAvatar(),
leaderboardList.get(position).getAvatar())));
username.setText(leaderboardList.get(position).getUsername());
count.setText(leaderboardList.get(position).getCategoryCount().toString());
}
@Override
public int getItemCount() {
return leaderboardList.size();
}
}

View file

@ -0,0 +1,120 @@
package fr.free.nrw.commons.profile.leaderboard;
import java.util.List;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
public class LeaderboardResponse {
@SerializedName("status")
@Expose
private Integer status;
@SerializedName("username")
@Expose
private String username;
@SerializedName("category_count")
@Expose
private Integer categoryCount;
@SerializedName("limit")
@Expose
private Object limit;
@SerializedName("avatar")
@Expose
private String avatar;
@SerializedName("offset")
@Expose
private Object offset;
@SerializedName("duration")
@Expose
private String duration;
@SerializedName("leaderboard_list")
@Expose
private List<LeaderboardList> leaderboardList = null;
@SerializedName("category")
@Expose
private String category;
@SerializedName("rank")
@Expose
private Integer rank;
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Integer getCategoryCount() {
return categoryCount;
}
public void setCategoryCount(Integer categoryCount) {
this.categoryCount = categoryCount;
}
public Object getLimit() {
return limit;
}
public void setLimit(Object limit) {
this.limit = limit;
}
public String getAvatar() {
return avatar;
}
public void setAvatar(String avatar) {
this.avatar = avatar;
}
public Object getOffset() {
return offset;
}
public void setOffset(Object offset) {
this.offset = offset;
}
public String getDuration() {
return duration;
}
public void setDuration(String duration) {
this.duration = duration;
}
public List<LeaderboardList> getLeaderboardList() {
return leaderboardList;
}
public void setLeaderboardList(List<LeaderboardList> leaderboardList) {
this.leaderboardList = leaderboardList;
}
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
public Integer getRank() {
return rank;
}
public void setRank(Integer rank) {
this.rank = rank;
}
}

View file

@ -1,6 +1,110 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:fresco="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/avatar"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_margin="20dp"
fresco:roundAsCircle="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
style="?android:textAppearanceMedium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/avatar" />
<TextView
android:id="@+id/rank"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
style="?android:textAppearanceMedium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/username" />
<TextView
android:id="@+id/count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
style="?android:textAppearanceMedium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/rank" />
<LinearLayout
android:id="@+id/column_names"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_margin="15dp"
android:weightSum="1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/count">
<TextView
style="?android:textAppearanceButton"
android:gravity="center_vertical|start"
android:layout_width="0dp"
android:layout_weight="0.2"
android:text="@string/leaderboard_column_rank"
android:layout_height="match_parent"/>
<TextView
style="?android:textAppearanceButton"
android:gravity="center_vertical|start"
android:layout_width="0dp"
android:layout_weight="0.6"
android:text="@string/leaderboard_column_user"
android:layout_height="match_parent"/>
<TextView
style="?android:textAppearanceButton"
android:gravity="center_vertical|end"
android:layout_width="0dp"
android:layout_weight="0.2"
android:text="@string/leaderboard_column_count"
android:layout_height="match_parent"/>
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/leaderboard_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/column_names" />
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View file

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:fresco="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:layout_marginStart="15dp"
android:layout_marginEnd="15dp"
android:weightSum="1">
<TextView
android:id="@+id/user_rank"
style="?android:textAppearanceMedium"
android:gravity="center_vertical|center_horizontal"
android:layout_width="0dp"
android:layout_weight="0.1"
android:layout_height="match_parent"/>
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/user_avatar"
android:layout_weight="0.1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="20dp"
fresco:roundAsCircle="true"
fresco:viewAspectRatio="1"
android:gravity="center_vertical|start"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/user_name"
style="?android:textAppearanceMedium"
android:gravity="center_vertical|start"
android:layout_width="0dp"
android:layout_weight="0.6"
android:layout_height="match_parent"/>
<TextView
android:id="@+id/user_count"
style="?android:textAppearanceSmall"
android:gravity="center_vertical|end"
android:layout_width="0dp"
android:layout_weight="0.2"
android:layout_height="match_parent"/>
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View file

@ -651,4 +651,9 @@ Upload your first media by tapping on the add button.</string>
<string name="copy_wikicode_to_clipboard">Copy wikicode to clipboard</string>
<string name="achievements_tab_title">Achievements</string>
<string name="leaderboard_tab_title">Leaderboard</string>
<string name="rank_prefix">Rank:</string>
<string name="count_prefix">Count:</string>
<string name="leaderboard_column_rank">Rank</string>
<string name="leaderboard_column_user">User</string>
<string name="leaderboard_column_count">Count</string>
</resources>