Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

User dashboard: add quick summary of num missions, labels, accuracy, etc. #2280

Closed
4 tasks done
jonfroehlich opened this issue Sep 28, 2020 · 10 comments · Fixed by #2289
Closed
4 tasks done

User dashboard: add quick summary of num missions, labels, accuracy, etc. #2280

jonfroehlich opened this issue Sep 28, 2020 · 10 comments · Fixed by #2289

Comments

@jonfroehlich
Copy link
Member

jonfroehlich commented Sep 28, 2020

I am splitting up #1363 into individual pieces for the user dashboard.

This Issue covers removing the line graph and historic activity table and replacing them with a quick summary of activity, including missions, labels, validations, and, crucially, accuracy.

Old design:
image

Implemented redesign:
image

A few things remain:

image

@jonfroehlich
Copy link
Member Author

@misaugstad, can you help me with this query for calculating the number of validations performed by the given user:

/**
   * Counts the number of validations performed by this user (given the supplied userId)
   *
   * @param userId
   * @returns the number of validations performed by this user
   */
  def countValidationsByUserId(userId: UUID): Int = db.withSession { implicit session =>
    // TODO: JEF and Mikey
    -1
  }

@misaugstad
Copy link
Member

This should be as simple as

SELECT COUNT(*)
FROM label_validation
WHERE user_id = '';

And in Slick,

def countValidationsByUserId(userId: UUID): Int = db.withSession { implicit session =>
  validationLabels.filter(_.userId === userId.toString).size.run
}

I'd just put it in LabelValidationTable.scala

@misaugstad
Copy link
Member

First off, @Messages("dashboard.distance") just gets the text "Total Distance Explored". You were looking at the header there.

As for internationalization, I'm actually going to give you way more info than you need, but it should help you to easily incorporate translations in the future. There are two places that translations are stored, one for JavaScript and one for Scala/Play. For JavaScript, translations are stored in public/locales/<language-code>. In particular, in public/locales/en-US/common.json we have these lines:

    "measurement-system": "IS",
    "unit-abbreviation-distance-user-dashboard": "mi",
    "unit-abbreviation-mission-distance": "ft",
    "unit-distance": "miles",

You can see how these are used in the old user dashboard code (it's not particularly pretty in this case):

// Calculate total distance audited in kilometers/miles depending on the measurement system used in the user's country.
for (var i = data.features.length - 1; i >= 0; i--) {
    distanceAudited += turf.length(data.features[i], {units: i18next.t('common:unit-distance')});
}
document.getElementById("td-total-distance-audited").innerHTML = distanceAudited.toPrecision(2) + " " + i18next.t("common:unit-abbreviation-distance-user-dashboard");

And you are able to use the i18next.t() if you've imported i18n:

<script type="text/javascript" src='@routes.Assets.at("javascripts/lib/i18next.min.js")'></script>
<script type="text/javascript" src='@routes.Assets.at("javascripts/lib/i18nextXHRBackend.min.js")'></script>

and if you've initialized the i18n library. You can see this in userProfile.scala.html:

    $(document).ready(function () {
        // Gets all translations before loading the choropleth.
        i18next.use(i18nextXHRBackend);
        i18next.init({
            backend: { loadPath: '/assets/locales/{{lng}}/{{ns}}.json' },
            fallbackLng: 'en',
            ns: ['dashboard', 'common'],
            defaultNS: 'dashboard',
            lng: "@lang.code",
            debug: false
        }, function(err, t) {
            var difficultRegionIds = @Json.toJson(RegionTable.difficultRegionIds);
            window.progress = Progress(_, $, c3, L, "@user.get.role", difficultRegionIds);
        });
    });

Note that in here, we know which language to load because Play tells us using @lang.code. So Play knows which language we're using. And the translations that Play/Scala have access to are in conf/messages.*. Again, in conf/messages.en-US you will find the line

measurement.system = IS
admin.overview.distance = mi

and you access using @Messages().

So in summary, if you are doing translations/conversions in Scala, use @Messages() to get things out of the conf/messages.<lang-code> files, and if you are in JavaScript, use i18next.t();` to get them out of the public/locales/<language-code>/*.json files.

AND FINALLY the actual conversion. I think that I would recommend actually pulling the call to MissionTable.getDistanceAudited(user.get.userId) out of userProfile.scala.html, and move it into UserProfileController.scala. In there you can call that function, check for the measurement system, do the conversion, and then add that number as a parameter to userProfile.scala.html.

You can see a very similar example in our conversion of distances for the landing page. In ApplicationController.scala, the conversion happens here:

val auditedDistance: Float =
  if (Messages("measurement.system") == "metric") StreetEdgeTable.auditedStreetDistance(1) * 1.60934.toFloat
  else StreetEdgeTable.auditedStreetDistance(1)

Then that value is passed in to the landing page HTML (index.scala.html) and used as-is:

var distanceAnim = new CountUp("distance", 0, @("%.1f".format(auditedDistance)),1,2.5,{suffix:''});

But in the actual HTML, we check for the correct unit:

<span class="ps-skyline-stats-holder-stat-label">@Messages("landing.stats.distance")</span>

@misaugstad
Copy link
Member

The distance calculation I'm using is returning different results

Yeah on the current user dashboard we have been calculating distance using the JavaScript turf library, looking at the list of completed street edges. We've moved just about everything over to looking at the some of completed mission distance instead of that, so I think it's good that you're switching to using @MissionTable.getDistanceAudited(user.get.userId) which should give a slightly different answer.

jonfroehlich added a commit that referenced this issue Sep 28, 2020
@jonfroehlich
Copy link
Member Author

Added the validations part:

image

Still need to work on metric/imperial conversion and printing out right units.

@jonfroehlich
Copy link
Member Author

jonfroehlich commented Sep 29, 2020

OK, in trying to complete the final piece of this this morning, I have too many questions to finish it. So, I'll need to wait until you're back online to make progress.

First, regarding shifting the method MissionTable.getDistanceAudited(userId: UUID) to UserProfileController.scala
The methods in UserProfileController.scala appear to all be implemented as asynchronous calls: UserAwareAction.async. Currently, getDistanceAudited is not implemented that way. Should it be?

Secondly, MissionTable.getDistanceAudited currently relies on the reference to missions to compute distance audited. The full line of code is:

missions.filter(_.userId === userId.toString).map(_.distanceProgress).sum.run.getOrElse(0F)

But obviously I don't have access to the missions variable in UserProfileController.scala, so I would need to rewrite this function. Maybe something like:

MissionTable.selectMissions(user.userId).map(_.distanceProgress).sum.run.getOrElse(0F)

Thirdly, none of the user dashboard stats calls in my new implementation are asynchronous. Should they be? See below:

<span class="ps-skyline-stats-holder-stat-number">@MissionTable.countCompletedMissionsByUserId(user.get.userId, true)</span>
<span class="ps-skyline-stats-holder-stat-number">@("%.1f".format(MissionTable.getDistanceAudited(user.get.userId))) mi</span>
<span class="ps-skyline-stats-holder-stat-number">@LabelTable.countLabelsByUserId(user.get.userId)</span>
<span class="ps-skyline-stats-holder-stat-number">@LabelValidationTable.countValidationsByUserId(user.get.userId)</span>
<span class="ps-skyline-stats-holder-stat-number">@LabelValidationTable.getUserAccuracy(user.get.userId).map(a => "%.1f%%".format(a * 100)).getOrElse("N/A")</span>

@misaugstad
Copy link
Member

regarding shifting the method MissionTable.getDistanceAudited(userId: UUID) to UserProfileController.scala

I was not suggesting that you move the method there. I was suggesting that instead of calling it in userProfile.scala.html you call it in UserProfileController.scala and pass it as a parameter into userProfile.scala.html.

none of the user dashboard stats calls in my new implementation are asynchronous. Should they be?

That is a great question. I think that in the case where we care about performance (page load time) above all else, these should all be asynchronous calls. In practice, writing it this way is just so much simpler than asynchronous calls, because to do that, you need to add a new route to the conf/routes file, and you need to write a new function in a controller that calls the functions you are using directly here. THEN you need to add the async call in the JavaScript and use that data to edit the HTML element.

However, since all of these queries should be quite fast, I think that the current method you're using is ideal. Much less code. No noticeable difference in performance.

@jonfroehlich
Copy link
Member Author

Gotcha. Thanks.

Could you re-explain how to do the unit conversion for the distance stuff? I didn't fully grok it.

We store things in the database in metric, right?

So this call MissionTable.getDistanceAudited(user.get.userId))) will actually be in meters, correct? (Oops, update: looks like we do a METERS_TO_MILES conversion in that function, so no). But then I'll want to convert to imperial (if necessary) and prettify it (e.g., display feet vs. miles or meters vs. kilometers).

@misaugstad
Copy link
Member

I think that explaining it in so many words can be a bit confusing. It is probably easier to look at an example and possibly the documentation. This is for doing it in Scala.

@jonfroehlich
Copy link
Member Author

jonfroehlich commented Oct 1, 2020

Now has adaptive km vs. mi print outs and also Spanish translations:

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
2 participants