Skip to content

[WIP][WOC] feedback implementation (continuation of #1464) #3424

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

Closed
wants to merge 18 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions app/app/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@
# dashboard views
re_path(r'^onboard/(?P<flow>\w+)/$', dashboard.views.onboard, name='onboard'),
re_path(r'^onboard/contributor/avatar/?$', dashboard.views.onboard_avatar, name='onboard_avatar'),
url(r'^postComment/', dashboard.views.postComment, name='postComment'),
Copy link
Contributor

Choose a reason for hiding this comment

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

What do you think about changing the naming here to just comment since the post is implied as we're creating a comment resource?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

sure, can do.

url(r'^dashboard/?', dashboard.views.dashboard, name='dashboard'),
url(r'^explorer/?', dashboard.views.dashboard, name='explorer'),
path('revenue/attestations/new', revenue.views.new_attestation, name='revenue_new_attestation'),
Expand Down
43 changes: 43 additions & 0 deletions app/assets/v2/css/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -1006,6 +1006,49 @@ input.is-invalid {
width: auto;
}

.rating {
width:160px;
}
.rating span { float:right; position:relative; }
Copy link
Contributor

Choose a reason for hiding this comment

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

Empty line between css rules :P

.rating span input {
position:absolute;
top:0px;
left:0px;
opacity:0;
}
.rating span label {
display:inline-block;
width:30px;
height:30px;
text-align:center;
color:#FFF;
font-size:25px;
margin-right:2px;
line-height:30px;
border-radius:50%;
-webkit-border-radius:50%;
}
.rating span label i {
color:#ccc;
}

.rating span:hover ~ span label i,
.rating span:hover label i {
opacity: 0.5;
}
.rating span.checked label i,
.rating span.checked ~ span label i
{
opacity: 1;
}

.rating span:hover ~ span label i,
.rating span:hover label i,
.rating span.checked label i,
.rating span.checked ~ span label i {
color: rgb(18,199,113);
}

.bounty_box {
border: 5px solid white;
width: 100%;
Expand Down
21 changes: 20 additions & 1 deletion app/assets/v2/js/pages/fulfill_bounty.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
/* eslint-disable no-console */
window.onload = function() {

// Check Radio-box
$('.rating input:radio').attr('checked', false);

$('.rating input').click(function() {
$('.rating span').removeClass('checked');
$(this).parent().addClass('checked');
});
Copy link
Contributor

Choose a reason for hiding this comment

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

Arrow functions :P


$('input:radio').change(
function() {
var userRating = this.value;
});
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this variable used anywhere?


// a little time for web3 injection
setTimeout(function() {
waitforWeb3(actions_page_warn_if_not_on_same_network);
Expand Down Expand Up @@ -73,7 +87,12 @@ window.onload = function() {
platform: 'gitcoin',
schemaVersion: '0.1',
schemaName: 'gitcoinFulfillment'
}
},
review: {
rating: data.rating?data.rating:-1,

Choose a reason for hiding this comment

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

Infix operators must be spaced. (space-infix-ops)

comment: data.review?data.review:"No comment given.",

Choose a reason for hiding this comment

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

Infix operators must be spaced. (space-infix-ops)
Strings must use singlequote. (quotes)

reviewType: "worker"

Choose a reason for hiding this comment

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

Strings must use singlequote. (quotes)

}
};

var _callback = function(error, result) {
Expand Down
25 changes: 21 additions & 4 deletions app/assets/v2/js/pages/process_bounty.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,10 +205,27 @@ window.onload = function() {
'txid': result
});

_alert({ message: gettext('Submitted transaction to web3.') }, 'info');
setTimeout(() => {
document.location.href = '/funding/details?url=' + issueURL;
}, 1000);
_alert({ message: gettext('Submitted transaction to web3, saving comment(s)...') }, 'info');

var submitCommentUrl = '/postComment';
var finishedComment = function() {
_alert({ message: gettext('Submitted transaction to web3.') }, 'info');
setTimeout(() => {
document.location.href = '/funding/details?url=' + issueURL;
}, 1000);
}
var ratVal = $('#rating').val();
var revVal = $('#review').val();
$.post(submitCommentUrl, {
"github_url": issueURL,
"network": $('input[name=network]').val(),
"standard_bounties_id": $('input[name=standard_bounties_id]').val(),
"review": {
"rating": ratVal?ratVal:-1,
"comment": revVal?revVal"No comment given.",
"reviewType": "approver"
}
}, finishedComment, 'json');

};

Expand Down
5 changes: 5 additions & 0 deletions app/dashboard/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from .models import (
Activity, BlockedUser, Bounty, BountyFulfillment, BountySyncRequest, CoinRedemption, CoinRedemptionRequest,
Interest, LabsResearch, Profile, SearchHistory, Subscription, Tip, TokenApproval, Tool, ToolVote, UserAction,
FeedbackEntry,
)


Expand Down Expand Up @@ -68,6 +69,9 @@ class UserActionAdmin(admin.ModelAdmin):
search_fields = ['action', 'ip_address', 'metadata', 'profile__handle']
ordering = ['-id']

class FeedbackAdmin(admin.ModelAdmin):

Choose a reason for hiding this comment

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

E302 expected 2 blank lines, found 1

search_fields = ['sender_profile','receiver_profile','bounty','feedbackType']

Choose a reason for hiding this comment

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

E231 missing whitespace after ','

ordering = ['-id']

class ProfileAdmin(admin.ModelAdmin):
raw_id_fields = ['user', 'preferred_kudos_wallet']
Expand Down Expand Up @@ -168,4 +172,5 @@ def network_link(self, instance):
admin.site.register(CoinRedemptionRequest, GeneralAdmin)
admin.site.register(Tool, GeneralAdmin)
admin.site.register(ToolVote, ToolVoteAdmin)
admin.site.register(FeedbackEntry, FeedbackAdmin)
admin.site.register(LabsResearch)
42 changes: 38 additions & 4 deletions app/dashboard/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
from ratelimit.decorators import ratelimit
from redis_semaphore import NotAvailable as SemaphoreExists

from .models import Profile
from .models import Profile, FeedbackEntry

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -333,7 +333,9 @@ def handle_bounty_fulfillments(fulfillments, new_bounty, old_bounty):
except Exception as e:
logger.error(f'{e} during new fulfillment creation for {new_bounty}')
continue
return new_bounty.fulfillments.all()

if new_bounty:
Copy link
Contributor

Choose a reason for hiding this comment

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

What is the goal of this check on new_bounty?

return new_bounty.fulfillments.all()


def create_new_bounty(old_bounties, bounty_payload, bounty_details, bounty_id):
Expand Down Expand Up @@ -522,12 +524,13 @@ def process_bounty_details(bounty_details):
bounty_data = bounty_details.get('data') or {}
bounty_payload = bounty_data.get('payload', {})
meta = bounty_data.get('meta', {})
bounty_review = bounty_data.get('review', {})

# what schema are we working with?
schema_name = meta.get('schemaName')
schema_name = meta.get('schemaName', 'Unknown')
schema_version = meta.get('schemaVersion', 'Unknown')
if not schema_name or schema_name != 'gitcoinBounty':
logger.info('Unknown Schema: Unknown - Version: %s', schema_version)
logger.info('Unknown Schema: %s - Version: %s', schema_name, schema_version)
return (False, None, None)

# Create new bounty (but only if things have changed)
Expand All @@ -545,11 +548,33 @@ def process_bounty_details(bounty_details):
new_bounty = create_new_bounty(old_bounties, bounty_payload, bounty_details, bounty_id)

if new_bounty:
create_new_feedback(new_bounty, bounty_review)
return (did_change, latest_old_bounty, new_bounty)
return (did_change, latest_old_bounty, latest_old_bounty)
except SemaphoreExists:
return (did_change, latest_old_bounty, latest_old_bounty)

def create_new_feedback(new_bounty, feedback_dict):

Choose a reason for hiding this comment

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

E302 expected 2 blank lines, found 1

Copy link
Contributor

Choose a reason for hiding this comment

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

ever considered putting this logic into model ? models looks anemic and you create 'create' method in helpers while it can belong to Feedback itself

@classmethod
worker_feedback(cls,bounty,...)
  return cls()

@classmethod
other_worker_feedback(cls,bounty,...) 
  return cls()

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes, considered that already. The main thing was to get it working, since we are having a IPFS bridge here and it wasn't working very well. especially because we do not simply provide the IPFS later on to the create functions, but actually parse them and return a DIFFERENT json structure than the one we pass into IPFS. Hence those methods are still from the "trying to get this to work" era.

feedbackType = feedback_dict.get('feedbackType','???')

Choose a reason for hiding this comment

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

E231 missing whitespace after ','

if feedbackType == 'worker':
sender = new_bounty.bounty_owner_profile
receiver = new_bounty.activities.last().profile
else:
receiver = new_bounty.bounty_owner_profile
sender = new_bounty.activities.last().profile

kwargs = {
'bounty': new_bounty,
'sender_profile': sender,
'receiver_profile': receiver,
'rating': feedback_dict.get('rating', '-1'),
'comment': feedback_dict.get('comment', 'No comment in IPFS entry.'),
'feedbackType': feedbackType
}

e = FeedbackEntry.objects.create(**kwargs)
e.save()
pass

def get_bounty_data_for_activity(bounty):
"""Get data from bounty to be saved in activity records.
Expand Down Expand Up @@ -625,18 +650,25 @@ def record_bounty_activity(event_name, old_bounty, new_bounty, _fulfillment=None
'increased_bounty',
'worker_rejected',
'bounty_changed']
logger.info(f"record_bounty_activity > profile at start: {user_profile}")
Copy link
Contributor

Choose a reason for hiding this comment

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

Lets remove these extra logging statements before we merge this PR 👍

if event_name not in funder_actions:
if not fulfillment:
logger.info(f"record_bounty_activity > not fulfillment, event_name: {event_name}")
fulfillment = new_bounty.fulfillments.order_by('-pk').first()
if event_name == 'work_done':
fulfillment = new_bounty.fulfillments.filter(accepted=True).latest('fulfillment_id')
if fulfillment:
logger.info(f"record_bounty_activity > fulfillment! {fulfillment}")
user_profile = Profile.objects.filter(handle__iexact=fulfillment.fulfiller_github_username).first()
if not user_profile:
logger.info(f"record_bounty_activity > not user_profile :-(")
user_profile = sync_profile(fulfillment.fulfiller_github_username)

Choose a reason for hiding this comment

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

W293 blank line contains whitespace

except Exception as e:
logger.error(f'{e} during record_bounty_activity for {new_bounty}')

logger.info(f"record_bounty_activity > user_profile: {user_profile}")

if user_profile:
return Activity.objects.create(
profile=user_profile,
Expand Down Expand Up @@ -729,6 +761,8 @@ def process_bounty_changes(old_bounty, new_bounty):
# record a useraction for this
record_user_action(event_name, old_bounty, new_bounty)
record_bounty_activity(event_name, old_bounty, new_bounty)
# check for review{} in here, create review object, wheeee.
Copy link
Contributor

Choose a reason for hiding this comment

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

What is this comment referring to?



# Build profile pairs list
if new_bounty.fulfillments.exists():
Expand Down
4 changes: 4 additions & 0 deletions app/dashboard/management/commands/create_activity_records.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
from dashboard.models import Activity, Bounty, Interest
from dashboard.views import record_bounty_activity as record_bounty_activity_interest

import logging
logger = logging.getLogger(__name__)

def set_created(activity, date):
if activity and date:
Expand All @@ -41,9 +43,11 @@ def create_activities(bounty):
act = record_bounty_activity_interest(bounty, interest.profile.user, 'worker_approved', interest)
set_created(act, interest.acceptance_date)
done_recorded = False
logger.info(f"create_activities started!")
for fulfillment in bounty.fulfillments.all():
act = record_bounty_activity('work_submitted', bounty.prev_bounty, bounty, fulfillment)
set_created(act, fulfillment.created_on)
logger.info(f"fulfillment accepted? {fulfillment.accepted}")
if fulfillment.accepted:
act = record_bounty_activity('work_done', bounty.prev_bounty, bounty, fulfillment)
set_created(act, fulfillment.accepted_on)
Expand Down
48 changes: 48 additions & 0 deletions app/dashboard/migrations/0007_bountycomment_feedbackentry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Generated by Django 2.1.4 on 2019-01-11 00:48

from django.db import migrations, models
import django.db.models.deletion
import economy.models


class Migration(migrations.Migration):

dependencies = [
('dashboard', '0006_bounty_estimated_hours'),
]

operations = [
migrations.CreateModel(
name='BountyComment',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_on', models.DateTimeField(db_index=True, default=economy.models.get_time)),
('modified_on', models.DateTimeField(default=economy.models.get_time)),
('comment', models.TextField(blank=True)),
('rating', models.IntegerField(blank=True, null=True)),
('commentreceiver', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments_received', to='dashboard.Profile')),
('commentsender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments_given', to='dashboard.Profile')),
('fulfillment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='dashboard.BountyFulfillment')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='FeedbackEntry',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_on', models.DateTimeField(db_index=True, default=economy.models.get_time)),
('modified_on', models.DateTimeField(default=economy.models.get_time)),
('rating', models.SmallIntegerField(blank=True, default=0)),
('comment', models.TextField(blank=True, default='')),
('feedbackType', models.TextField(blank=True, default='', max_length=20)),
('bounty', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='feedbacks', to='dashboard.Bounty')),
('receiver_profile', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='feedbacks_got', to='dashboard.Profile')),
('sender_profile', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='feedbacks_sent', to='dashboard.Profile')),
],
options={
'abstract': False,
},
),
]
28 changes: 28 additions & 0 deletions app/dashboard/migrations/0008_auto_20190111_0049.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Generated by Django 2.1.4 on 2019-01-11 00:49

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('dashboard', '0007_bountycomment_feedbackentry'),
]

operations = [
migrations.RemoveField(
model_name='bountycomment',
name='commentreceiver',
),
migrations.RemoveField(
model_name='bountycomment',
name='commentsender',
),
migrations.RemoveField(
model_name='bountycomment',
name='fulfillment',
),
migrations.DeleteModel(
name='BountyComment',
),
]
32 changes: 31 additions & 1 deletion app/dashboard/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1025,7 +1025,6 @@ def submitted(self):
"""Exclude results that have not been submitted."""
return self.exclude(fulfiller_address='0x0000000000000000000000000000000000000000')


class BountyFulfillment(SuperModel):
"""The structure of a fulfillment on a Bounty."""

Expand Down Expand Up @@ -2802,3 +2801,34 @@ class BlockedUser(SuperModel):
def __str__(self):
"""Return the string representation of a Bounty."""
return f'<BlockedUser: {self.handle}>'


class FeedbackEntry(SuperModel):

Choose a reason for hiding this comment

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

E302 expected 2 blank lines, found 1

bounty = models.ForeignKey(
'dashboard.Bounty',
related_name='feedbacks',
on_delete=models.CASCADE,
blank=True,
null=True
)
sender_profile = models.ForeignKey(
'dashboard.Profile',
related_name='feedbacks_sent',
on_delete=models.CASCADE,
blank=True,
null=True
)
receiver_profile = models.ForeignKey(
'dashboard.Profile',
related_name='feedbacks_got',
on_delete=models.CASCADE,
blank=True,
null=True
)
rating = models.SmallIntegerField(blank=True, default=0)
comment = models.TextField(default='', blank=True)
feedbackType = models.TextField(default='', blank=True, max_length=20)
Copy link
Contributor

Choose a reason for hiding this comment

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

It looks like there are only two types of feedback: approver and worker. If thats the case, what do you think about using the choices argument here to add a bit of validation and make the admin interface that much nicer? :)


def __str__(self):
"""Return the string representation of a Bounty."""
return f'<Feedback Bounty #{self.bounty} - from: {self.sender_profile} to: {self.receiver_profile}>'
Loading