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

WIP: Stats email v2 #1311

Merged
merged 17 commits into from
Jul 24, 2018
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 116 additions & 3 deletions app/dashboard/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1127,22 +1127,97 @@ def get_quarterly_stats(self):
dict : containing the following information
'user_total_earned_eth': Total earnings of user in ETH.
'user_total_earned_usd': Total earnings of user in USD.
'user_fulfilled_bounties_count': Total bounties fulfilled by user.
'user_total_funded_usd': Total value of bounties funded by the user on bounties in done status in USD
'user_total_funded_hours': Total hours input by the developers on the fulfillment of bounties created by the user in USD

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E501 line too long (132 > 120 characters)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E501 line too long (132 > 120 characters)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E501 line too long (132 > 120 characters)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E501 line too long (132 > 120 characters)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E501 line too long (132 > 120 characters)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E501 line too long (132 > 120 characters)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E501 line too long (132 > 120 characters)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E501 line too long (132 > 120 characters)

'user_fulfilled_bounties_count': Total bounties fulfilled by user
'user_fufilled_bounties': bool, if the user fulfilled bounties
'user_funded_bounties_count': Total bounties funded by the user
'user_funded_bounties': bool, if the user funded bounties in the last quarter
'user_funded_bounty_developers': Unique set of users that fulfilled bounties funded by the user
'user_avg_hours_per_funded_bounty': Average hours input by developer on fulfillment per bounty
'user_avg_hourly_rate_per_funded_bounty': Average hourly rate in dollars per bounty funded by user
'user_avg_eth_earned_per_bounty': Average earning in ETH earned by user per bounty
'user_avg_usd_earned_per_bounty': Average earning in USD earned by user per bounty
'user_num_completed_bounties': Total no. of bounties completed.
'user_num_funded_fulfilled_bounties': Total bounites that were funded by the user and fulfilled
'user_bounty_completion_percentage': Percentage of bounties successfully completed by the user
'user_funded_fulfilled_percentage': Percentage of bounties funded by the user that were fulfilled
'user_active_in_last_quarter': bool, if the user was active in last quarter
'user_no_of_languages': No of languages user used while working on bounties.
'user_languages': Languages that were used in bounties that were worked on.
'relevant_bounties': a list of Bounty(s) that would match the skillset input by the user into the Match tab of their settings

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E501 line too long (137 > 120 characters)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E501 line too long (137 > 120 characters)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no recommendation on https://www.python.org/dev/peps/pep-0257/, could you advise

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E501 line too long (137 > 120 characters)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E501 line too long (137 > 120 characters)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E501 line too long (137 > 120 characters)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E501 line too long (137 > 120 characters)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E501 line too long (137 > 120 characters)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E501 line too long (137 > 120 characters)

"""
user_active_in_last_quarter = False
user_fulfilled_bounties = False
user_funded_bounties = False
last_quarter = datetime.now() - timedelta(days=90)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

F821 undefined name 'timedelta'

bounties = self.bounties.filter(modified_on__gte=last_quarter)
fulfilled_bounties = [
bounty for bounty in bounties if bounty.is_hunter(self.handle) and bounty.status == 'done'
]
fulfilled_bounties_count = len(fulfilled_bounties)
funded_bounties = [
bounty for bounty in bounties if bounty.is_funder(self.handle)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is any of this data STUBBed out? are you unsure of any of the calcs you did? if so, mind throwing a few TODOs in there for us to work through?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added TODOs

]
funded_bounties_count = len(funded_bounties)
if funded_bounties_count:
total_funded_usd = sum([
bounty.value_in_usdt if bounty.value_in_usdt else 0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use Django Aggregation here.

for bounty in funded_bounties
])
total_funded_hourly_rate = float(0)
hourly_rate_bounties_counted = float(0)
for bounty in funded_bounties:
hourly_rate = bounty.hourly_rate
if hourly_rate:
total_funded_hourly_rate += bounty.hourly_rate
hourly_rate_bounties_counted += 1
funded_bounty_fulfillments = []
for bounty in funded_bounties:
fulfillments = bounty.fulfillments.filter(accepted=True)
for fulfillment in fulfillments:
if isinstance(fulfillment, BountyFulfillment):
funded_bounty_fulfillments.append(fulfillment)
funded_bounty_fulfillments_count = len(funded_bounty_fulfillments)

total_funded_hours = 0
funded_fulfillments_with_hours_counted = 0
if funded_bounty_fulfillments_count:
from decimal import Decimal
for fulfillment in funded_bounty_fulfillments:
if isinstance(fulfillment.fulfiller_hours_worked, Decimal):
total_funded_hours += fulfillment.fulfiller_hours_worked
funded_fulfillments_with_hours_counted += 1

user_funded_bounty_developers = []
for fulfillment in funded_bounty_fulfillments:
user_funded_bounty_developers.append(fulfillment.fulfiller_github_username.lstrip('@'))
user_funded_bounty_developers = [*{*user_funded_bounty_developers}]
if funded_fulfillments_with_hours_counted:
avg_hourly_rate_per_funded_bounty = float(total_funded_hourly_rate) / float(funded_fulfillments_with_hours_counted)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E501 line too long (131 > 120 characters)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E501 line too long (131 > 120 characters)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E501 line too long (131 > 120 characters)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E501 line too long (131 > 120 characters)

avg_hours_per_funded_bounty = float(total_funded_hours) / float(funded_fulfillments_with_hours_counted)
else:
avg_hourly_rate_per_funded_bounty = 0
avg_hours_per_funded_bounty = 0
funded_fulfilled_bounties = [
bounty for bounty in funded_bounties if bounty.status == 'done'
]
num_funded_fulfilled_bounties = len(funded_fulfilled_bounties)
funded_fulfilled_percent = float(
# Round to 0 places of decimals to be displayed in template
round(num_funded_fulfilled_bounties * 1.0 / funded_bounties_count, 2) * 100
)
user_funded_bounties = True

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

F841 local variable 'user_funded_bounties' is assigned to but never used

else:
num_funded_fulfilled_bounties = 0
funded_fulfilled_percent = 0
user_funded_bounties = False
avg_hourly_rate_per_funded_bounty = 0
avg_hours_per_funded_bounty = 0
total_funded_usd = 0
total_funded_hours = 0
user_funded_bounty_developers = []

total_earned_eth = sum([
bounty.value_in_eth if bounty.value_in_eth else 0
for bounty in fulfilled_bounties
Expand All @@ -1165,13 +1240,40 @@ def get_quarterly_stats(self):
if fulfilled_bounties_count:
avg_eth_earned_per_bounty = total_earned_eth / fulfilled_bounties_count
avg_usd_earned_per_bounty = total_earned_usd / fulfilled_bounties_count
user_fulfilled_bounties = True

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

F841 local variable 'user_fulfilled_bounties' is assigned to but never used


user_languages = []
for bounty in fulfilled_bounties:
user_languages += bounty.keywords.split(',')
user_languages = set(user_languages)
user_no_of_languages = len(user_languages)

if num_completed_bounties or fulfilled_bounties_count:
user_active_in_last_quarter = True

relevant_bounties = []
else:
from marketing.utils import get_or_save_email_subscriber
user_coding_languages = get_or_save_email_subscriber(self.email, 'internal').keywords
if user_coding_languages is not None:
potential_bounties = Bounty.objects.all()
relevant_bounties = Bounty.objects.none()
for keyword in user_coding_languages:
relevant_bounties = relevant_bounties.union(potential_bounties.filter(
network=Profile.get_network(),
current_bounty=True,
metadata__icontains=keyword,
idx_status__in=['open'],
).order_by('?')

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

W291 trailing whitespace

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

W291 trailing whitespace

)
relevant_bounties = relevant_bounties[:3]
relevant_bounties = list(relevant_bounties)
# Round to 2 places of decimals to be diplayed in templates
completetion_percent = float('%.2f' % completetion_percent)
funded_fulfilled_percent = float('%.2f' % funded_fulfilled_percent)
avg_eth_earned_per_bounty = float('%.2f' % avg_eth_earned_per_bounty)
avg_usd_earned_per_bounty = float('%.2f' % avg_usd_earned_per_bounty)
avg_hourly_rate_per_funded_bounty = float('%.2f' % avg_hourly_rate_per_funded_bounty)
avg_hours_per_funded_bounty = float('%.2f' % avg_hours_per_funded_bounty)
total_earned_eth = float('%.2f' % total_earned_eth)
total_earned_usd = float('%.2f' % total_earned_usd)

Expand All @@ -1184,14 +1286,25 @@ def get_quarterly_stats(self):
return {
'user_total_earned_eth': total_earned_eth,
'user_total_earned_usd': total_earned_usd,
'user_total_funded_usd': total_funded_usd,
'user_total_funded_hours': total_funded_hours,
'user_fulfilled_bounties_count': fulfilled_bounties_count,
'user_fulfilled_bounties': user_fulfilled_bounties,
'user_funded_bounties_count': funded_bounties_count,
'user_funded_bounties': user_funded_bounties,
'user_funded_bounty_developers': user_funded_bounty_developers,
'user_avg_hours_per_funded_bounty': avg_hours_per_funded_bounty,
'user_avg_hourly_rate_per_funded_bounty': avg_hourly_rate_per_funded_bounty,
'user_avg_eth_earned_per_bounty': avg_eth_earned_per_bounty,
'user_avg_usd_earned_per_bounty': avg_usd_earned_per_bounty,
'user_num_completed_bounties': num_completed_bounties,
'user_num_funded_fulfilled_bounties': num_funded_fulfilled_bounties,
'user_bounty_completion_percentage': completetion_percent,
'user_funded_fulfilled_percentage': funded_fulfilled_percent,
'user_active_in_last_quarter': user_active_in_last_quarter,
'user_no_of_languages': user_no_of_languages,
'user_languages': user_languages
'user_languages': user_languages,
'relevant_bounties': relevant_bounties
}

@property
Expand Down
49 changes: 48 additions & 1 deletion app/retail/templates/emails/quarterly_stats.html
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@
<h1>GITCOIN QUARTERLY</h1>

{% if user_active_in_last_quarter %}
{% if user_fulfilled_bounties %}
<h2>You've Been BUILDing</h2>
<h4>An overview of your stats from the last 3 months.</h4>
<div class="centered-contents stat-box row">
Expand Down Expand Up @@ -237,8 +238,54 @@ <h4>An overview of your stats from the last 3 months.</h4>
<div class="button-action">
<a class="button" href="{% url 'dashboard' %}">{% trans "Share my stats" %}</a>
</div>
{% endif %}
{% endif %}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This indent doesn't match the starting {% if %} block


{% if user_funded_bounties_count %}
<h2>You've Been Making Money Moves</h2>
<h4>An overview of your OSS project's stats from the last 3 months.</h4>
<div class="centered-contents stat-box row">
<div class="stat-img full-bordered-circle percent-content col-sm-12">
<p class="content">{{ user_funded_fulfilled_percentage}} %</p>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we switch this to: {{ user_funded_fulfilled_percentage }}

</div>
<div class="stat-contents col-sm-12">
<p>Of the {{ user_funded_bounties_count | intcomma }} issue{%if user_funded_bounties_count > 1 %}s{% endif %} that you funded a bounty for, {{ user_num_funded_fulfilled_bounties | intcomma }} were fulfilled -- that's a {{ user_funded_fulfilled_percentage}}% success rate!</p>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use django's template tag pluralize here

</div>
</div>
<div class="centered-contents stat-box row">
<div class="stat-img total-transaction">
<p class="content">{{ user_funded_bounty_developers | length }} contributors</p>
</div>
<div class="stat-contents col-sm-12">
<p>The following {{ user_funded_bounty_developers | length }} developers have made contributions to your OSS projects this quarter:
{% for developer in user_funded_bounty_developers %}{% if forloop.counter == user_num_funded_bounty_developers %} &amp; {% elif forloop.first %}{% else %}, {% endif %}<a href="https://github.com/{{ developer }}">@{{ developer }}</a>{% endfor %}</p>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we drop these fors and ifs to their own lines for readability?

</div>
</div>
{% if user_total_funded_hours %}
<div class="centered-contents stat-box row">
<div class="stat-img">
<p class="content bounty-img">{{ user_total_funded_hours | intcomma }} hours</p>
<p class="content bounty-fund-img">{{ user_total_funded_usd | intcomma }} USD</p>
</div>
<div class="stat-contents">
<p>Developers worked an average of {{ user_avg_hours_per_funded_bounty }} hours per issue on your projects (only applies to bounties where users entered their hours). You funded a total of ${{ user_total_funded_usd | intcomma }} USD of work at an average rate of ${{ user_avg_hourly_rate_per_funded_bounty }} USD / hour..</p>
</div>
</div>
{% endif %}

<div class="button-action">
<a class="button" href="{% url 'dashboard' %}">{% trans "Share my stats" %}</a>
</div>
{% endif %}
{% endif %}
{% if not user_active_last_quarter and relevant_bounties %}
<h2>You could have been BUILDing</h2>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This and remaining strings throughout this file need wrapped in trans or blocktrans (if a variable exists in the string)

{% if relevant_bounties|length == 1 %} This bounty {% endif %}{% if relevant_bounties|length > 1 %} These bounties {% endif %} from the issue explorer match your skillset:
<br>
{% for bounty in relevant_bounties %}
<br>
{% include 'emails/bounty.html' with bounty=bounty %}
{% endfor %}
{% endif %}
<hr>

<h2>Platform-wide Stats</h2>
Expand Down