+
+
+
${element.description}
+
View ${element.source_type}
+
+
`;
+
+ return markup;
+ }
+
+ $(selectItem).on('select2:unselecting', function(e) {
+ $(this).val(null).trigger('change');
+ document.selected_element = null;
+ e.preventDefault();
+ });
+ $(selectItem).on('select2:select', function(e) {
+ console.log(e);
+ console.log($('#search').val());
+ var data = e.params.data;
+
+ console.log(data);
+ });
+ });
+}
+
+$('document').ready(function() {
+ $('#search_container').html('
');
+ search();
+
+ $('body').on('click', '.search_autocomplete', function(e) {
+ var search_term = $(this).text();
+
+ e.preventDefault();
+ });
+
+ $(document).on ('click', '.select2-container--gc-search .element-search-result', function() {
+ document.location.href = $(this).data('url');
+ });
+
+ $('.select2-nosearch').select2({
+ minimumResultsForSearch: 20
+ });
+
+ // $('.select2-search').select2({});
+
+ // listen for keyups in both input widget AND dropdown
+ $('body').on('keyup', '.select2-container--gc-search', function(e) {
+ var KEYS = { UP: 38, DOWN: 40, ENTER: 13 };
+ var $sel = $('.select2-container--gc-search.select2-container--open');
+
+ if ($sel.length) {
+ var target;
+
+ if (e.keyCode === KEYS.DOWN && !e.altKey) {
+ target = $('.select2-container--gc-search .select2-results__option.selected');
+ if (!target.length) {
+ target = $('.select2-container--gc-search .select2-results__option:first-child');
+ } else if (target.next().length) {
+ target.removeClass('selected');
+ target = target.next();
+ }
+ target.addClass('selected');
+ } else if (e.keyCode === KEYS.UP) {
+ target = $('.select2-container--gc-search .select2-results__option.selected');
+ if (!target.length) {
+ target = $('.select2-container--gc-search .select2-results__option:first-child');
+ } else if (target.prev().length) {
+ target.removeClass('selected');
+ target = target.prev();
+ }
+ target.addClass('selected');
+ } else if (e.keyCode === KEYS.ENTER) {
+ target = $('.select2-container--gc-search .select2-results__option.selected');
+ var url = target.find('.search-result').data('url');
+
+ if (target && url) {
+ document.location.href = url;
+ }
+ }
+ }
+
+ });
+
+
+});
+
diff --git a/app/assets/v2/js/shared.js b/app/assets/v2/js/shared.js
index 0cdfddb9323..861dccd11b3 100644
--- a/app/assets/v2/js/shared.js
+++ b/app/assets/v2/js/shared.js
@@ -695,7 +695,6 @@ const randomElement = array => {
var currentNetwork = function(network) {
$('.navbar-network').removeClass('hidden');
- let tooltip_info;
document.web3network = network;
if (document.location.href.startsWith('https://gitcoin.co')) { // Live
@@ -1024,9 +1023,11 @@ var actions_page_warn_if_not_on_same_network = function() {
attach_change_element_type();
-window.addEventListener('load', function() {
- setInterval(listen_for_web3_changes, 5000);
-});
+if (typeof is_bounties_network == 'undefined' || is_bounties_network) {
+ window.addEventListener('load', function() {
+ setInterval(listen_for_web3_changes, 1000);
+ });
+}
var setUsdAmount = function() {
const amount = $('input[name=amount]').val();
@@ -1536,3 +1537,31 @@ function check_balance_and_alert_user_if_not_enough(
});
}
+
+/**
+ * fetches github issue details of the issue_url
+ * @param {string} issue_url
+ */
+const fetchIssueDetailsFromGithub = issue_url => {
+ return new Promise((resolve, reject) => {
+ if (!issue_url || issue_url.length < 5 || issue_url.indexOf('github') == -1) {
+ reject('error: issue_url needs to be a valid github URL');
+ }
+
+ const github_token = currentProfile.githubToken;
+
+ if (!github_token) {
+ reject('error: API calls needs user to be logged in');
+ }
+
+ const request_url = '/sync/get_issue_details?url=' + encodeURIComponent(issue_url) + '&token=' + github_token;
+
+ $.get(request_url, function(result) {
+ result = sanitizeAPIResults(result);
+ resolve(result);
+ }).fail(err => {
+ console.log(err);
+ reject(error);
+ });
+ });
+};
diff --git a/app/assets/v2/js/status.js b/app/assets/v2/js/status.js
index 0d19ea066c2..e1901c9026c 100644
--- a/app/assets/v2/js/status.js
+++ b/app/assets/v2/js/status.js
@@ -84,7 +84,7 @@ $(document).ready(function() {
// enforce a max length
var max_len = $(this).data('maxlen');
-
+
if ($(this).val().trim().length > max_len) {
e.preventDefault();
$(this).addClass('red');
@@ -153,3 +153,19 @@ $(document).ready(function() {
.catch(err => console.log('Error ', err));
}
});
+window.addEventListener('DOMContentLoaded', function() {
+ var button = document.querySelector('#emoji-button');
+ var picker = new EmojiButton({
+ position: 'left-end'
+ });
+
+ if (button && picker) {
+ picker.on('emoji', function(emoji) {
+ document.querySelector('textarea').value += emoji;
+ });
+
+ button.addEventListener('click', function() {
+ picker.pickerVisible ? picker.hidePicker() : picker.showPicker(button);
+ });
+ }
+});
diff --git a/app/assets/v2/js/users.js b/app/assets/v2/js/users.js
index 164c5a13412..a4fe3f2e2ad 100644
--- a/app/assets/v2/js/users.js
+++ b/app/assets/v2/js/users.js
@@ -205,6 +205,18 @@ Vue.mixin({
}
}
}
+ },
+ joinTribe: function(user, event) {
+ event.target.disabled = true;
+ const url = `/tribe/${user.handle}/join/`;
+ const sendJoin = fetchData (url, 'POST', {}, {'X-CSRFToken': csrftoken});
+
+ $.when(sendJoin).then(function(response) {
+ event.target.disabled = false;
+ response.is_member ? event.target.innerText = 'Leave Tribe' : event.target.innerText = 'Join Tribe';
+ }).fail(function(error) {
+ event.target.disabled = false;
+ });
}
}
});
diff --git a/app/avatar/views_3d.py b/app/avatar/views_3d.py
index eb578da2eea..0c770b4d730 100644
--- a/app/avatar/views_3d.py
+++ b/app/avatar/views_3d.py
@@ -32,24 +32,80 @@
from .models import BaseAvatar, CustomAvatar, SocialAvatar
logger = logging.getLogger(__name__)
-avatar_3d_base_path = 'assets/v2/images/avatar3d/avatar.svg'
-
-preview_viewbox = {
- 'background': '0 0 350 350',
- 'clothing': '60 80 260 300',
- 'ears': '100 70 50 50',
- 'head': '80 10 170 170',
- 'mouth': '130 90 70 70',
- 'nose': '140 80 50 50',
- 'eyes': '120 40 80 80',
- 'hair': '110 0 110 110',
-}
-
-skin_tones = [
- 'FFFFF6', 'FEF7EB', 'F8D5C2', 'EEE3C1', 'D8BF82', 'D2946B', 'AE7242', '88563B', '715031', '593D26', '392D16'
-]
-hair_tones = ['000000', '4E3521', '8C3B28', 'B28E28', 'F4EA6E', 'F0E6FF', '4D22D2', '8E2ABE', '3596EC', '0ECF7C']
-tone_maps = ['skin', 'blonde_hair', 'brown_hair', 'brown_hair2', 'dark_hair', 'grey_hair']
+
+
+def get_avatar_attrs(theme, key):
+ avatar_attrs = {
+ 'bufficorn': {
+ 'preview_viewbox': {
+ #section: x_pos y_pox x_size y_size
+ 'background': '0 0 200 200',
+ 'facial': '80 180 220 220',
+ 'glasses': '80 80 220 220',
+ 'hat': '20 30 300 300',
+ 'shirt': '130 200 200 200',
+ 'accessory': '50 180 150 200',
+ 'horn': '120 80 150 150',
+ },
+ 'skin_tones': [
+ 'D7723B', 'FFFFF6', 'FEF7EB', 'F8D5C2', 'EEE3C1', 'D8BF82', 'D2946B', 'AE7242', '88563B', '715031',
+ '593D26', '392D16'
+ ],
+ 'hair_tones': [
+ 'F495A8', '000000', '4E3521', '8C3B28', 'B28E28', 'F4EA6E', 'F0E6FF', '4D22D2', '8E2ABE', '3596EC',
+ '0ECF7C'
+ ],
+ 'tone_maps': ['skin', 'blonde_hair', 'brown_hair', 'brown_hair2', 'dark_hair', 'grey_hair'],
+ 'path': 'assets/v2/images/avatar3d/avatar_bufficorn.svg',
+ },
+ '3d': {
+ 'preview_viewbox': {
+ #section: x_pos y_pox x_size y_size
+ 'background': '0 0 350 350',
+ 'clothing': '60 80 260 300',
+ 'ears': '100 70 50 50',
+ 'head': '80 10 170 170',
+ 'mouth': '130 90 70 70',
+ 'nose': '140 80 50 50',
+ 'eyes': '120 40 80 80',
+ 'hair': '110 0 110 110',
+ },
+ 'skin_tones': [
+ 'FFFFF6', 'FEF7EB', 'F8D5C2', 'EEE3C1', 'D8BF82', 'D2946B', 'AE7242', '88563B', '715031', '593D26',
+ '392D16'
+ ],
+ 'hair_tones': [
+ '000000', '4E3521', '8C3B28', 'B28E28', 'F4EA6E', 'F0E6FF', '4D22D2', '8E2ABE', '3596EC', '0ECF7C'
+ ],
+ 'tone_maps': ['skin', 'blonde_hair', 'brown_hair', 'brown_hair2', 'dark_hair', 'grey_hair'],
+ 'path': 'assets/v2/images/avatar3d/avatar.svg',
+ },
+ 'female': {
+ 'preview_viewbox': {
+ #section: x_pos y_pox x_size y_size
+ 'background': '0 0 350 350',
+ 'body': '60 80 220 220',
+ 'ears': '100 70 50 50',
+ 'head': '80 10 170 170',
+ 'mouth': '130 90 70 70',
+ 'nose': '130 80 30 30',
+ 'lips': '120 80 50 50',
+ 'eyes': '110 40 70 70',
+ 'hair': '90 0 110 110',
+ 'accessories': '100 50 100 100',
+ },
+ 'skin_tones': [
+ 'FFFFF6', 'FEF7EB', 'F8D5C2', 'EEE3C1', 'D8BF82', 'D2946B', 'AE7242', '88563B', '715031', '593D26',
+ '392D16'
+ ],
+ 'hair_tones': [
+ '000000', '4E3521', '8C3B28', 'B28E28', 'F4EA6E', 'F0E6FF', '4D22D2', '8E2ABE', '3596EC', '0ECF7C'
+ ],
+ 'tone_maps': ['skin', 'blonde_hair', 'brown_hair', 'brown_hair2', 'dark_hair', 'grey_hair'],
+ 'path': 'assets/v2/images/avatar3d/avatar_female.svg',
+ },
+ }
+ return avatar_attrs.get(theme, {}).get(key, {})
def get_avatar_tone_map(tone='skin', skinTone=''):
@@ -60,11 +116,11 @@ def get_avatar_tone_map(tone='skin', skinTone=''):
'FFCAA6': 0,
'D68876': 0,
'FFDBC2': 0,
- 'F4B990': 0, #base
+ 'D7723B': 0, #base
}
- base_3d_tone = 'F4B990'
+ base_3d_tone = 'D7723B'
if tone == 'blonde_hair':
- tones = {'CEA578': 0, 'BA7056': 0, 'F4C495': 0, }
+ tones = {'F495A8': 0, 'C6526D': 0, 'F4C495': 0, }
base_3d_tone = 'CEA578'
if tone == 'brown_hair':
tones = {'775246': 0, '563532': 0, 'A3766A': 0, }
@@ -101,6 +157,7 @@ def get_avatar_tone_map(tone='skin', skinTone=''):
def avatar3d(request):
"""Serve an 3d avatar."""
+ theme = request.GET.get('theme', '3d')
#get request
accept_ids = request.GET.getlist('ids')
if not accept_ids:
@@ -119,7 +176,7 @@ def avatar3d(request):
height = 30
width = 30
_type = accept_ids[0].split('_')[0]
- viewBox = preview_viewbox.get(_type, '0 0 600 600')
+ viewBox = get_avatar_attrs(theme, 'preview_viewbox').get(_type, '0 0 600 600')
force_show_whole_body = False
else:
accept_ids.append('frame')
@@ -137,49 +194,56 @@ def avatar3d(request):
#ensure at least one per category
if bool(int(force_show_whole_body)):
- categories = avatar3dids_helper()['by_category']
+ categories = avatar3dids_helper(theme)['by_category']
for category_name, ids in categories.items():
has_ids_in_category = any([ele in accept_ids for ele in ids])
if not has_ids_in_category:
accept_ids.append(ids[0])
# asseble response
- with open(avatar_3d_base_path) as file:
- elements = []
- tree = ET.parse(file)
- for item in tree.getroot():
- include_item = item.attrib.get('id') in accept_ids or item.tag in tags
- if include_item:
- elements.append(ET.tostring(item).decode('utf-8'))
- output = prepend + "".join(elements) + postpend
- for _type in tone_maps:
- base_tone = skinTone if 'hair' not in _type else hairTone
- if base_tone:
- for _from, to in get_avatar_tone_map(_type, base_tone).items():
- output = output.replace(_from, to)
- if request.method == 'POST':
- return save_custom_avatar(request, output)
- response = HttpResponse(output, content_type='image/svg+xml')
+ avatar_3d_base_path = get_avatar_attrs(theme, 'path')
+ if avatar_3d_base_path:
+ with open(avatar_3d_base_path) as file:
+ elements = []
+ tree = ET.parse(file)
+ for item in tree.getroot():
+ include_item = item.attrib.get('id') in accept_ids or item.tag in tags
+ if include_item:
+ elements.append(ET.tostring(item).decode('utf-8'))
+ output = prepend + "".join(elements) + postpend
+ tone_maps = get_avatar_attrs(theme, 'tone_maps')
+ for _type in tone_maps:
+ base_tone = skinTone if 'hair' not in _type else hairTone
+ if base_tone:
+ for _from, to in get_avatar_tone_map(_type, base_tone).items():
+ output = output.replace(_from, to)
+ if request.method == 'POST':
+ return save_custom_avatar(request, output)
+ response = HttpResponse(output, content_type='image/svg+xml')
return response
-def avatar3dids_helper():
- with open(avatar_3d_base_path) as file:
- tree = ET.parse(file)
- ids = [item.attrib.get('id') for item in tree.getroot()]
- ids = [ele for ele in ids if ele]
- category_list = {ele.split("_")[0]: [] for ele in ids}
- for ele in ids:
- category = ele.split("_")[0]
- category_list[category].append(ele)
+def avatar3dids_helper(theme):
+ avatar_3d_base_path = get_avatar_attrs(theme, 'path')
+ if avatar_3d_base_path:
+ with open(avatar_3d_base_path) as file:
+ tree = ET.parse(file)
+ ids = [item.attrib.get('id') for item in tree.getroot()]
+ ids = [ele for ele in ids if ele and ele != 'base']
+ category_list = {ele.split("_")[0]: [] for ele in ids}
+ for ele in ids:
+ category = ele.split("_")[0]
+ category_list[category].append(ele)
- response = {'ids': ids, 'by_category': category_list, }
- return response
+ response = {'ids': ids, 'by_category': category_list, }
+ return response
def avatar3dids(request):
"""Serve an 3d avatar id list."""
- response = JsonResponse(avatar3dids_helper())
+
+ theme = request.GET.get('theme', '3d')
+ response = JsonResponse(avatar3dids_helper(theme))
return response
diff --git a/app/bounty_requests/admin.py b/app/bounty_requests/admin.py
index d6da9caf0f8..45a0c5d3ef7 100644
--- a/app/bounty_requests/admin.py
+++ b/app/bounty_requests/admin.py
@@ -28,12 +28,12 @@ class BountyRequestAdmin(admin.ModelAdmin):
"""Setup the BountyRequest admin results display."""
ordering = ['-created_on']
list_display = [
- 'created_on', 'status', 'github_url', 'amount', 'requested_by',
- 'comment_admin'
+ 'created_on', 'status','github_url', 'tribe', 'amount', 'token_name',
+ 'requested_by'
]
search_fields = [
'created_on', 'status', 'github_url', 'amount', 'requested_by__handle',
- 'eth_address', 'comment', 'comment_admin'
+ 'comment',
]
raw_id_fields = ['requested_by']
diff --git a/app/bounty_requests/migrations/0004_auto_20200204_0629.py b/app/bounty_requests/migrations/0004_auto_20200204_0629.py
new file mode 100644
index 00000000000..6cc3b4082a0
--- /dev/null
+++ b/app/bounty_requests/migrations/0004_auto_20200204_0629.py
@@ -0,0 +1,66 @@
+# Generated by Django 2.2.4 on 2020-02-04 06:29
+
+import django.core.validators
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('dashboard', '0079_auto_20200204_0629'),
+ ('bounty_requests', '0003_auto_20191001_0748'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='bountyrequest',
+ name='title',
+ field=models.CharField(default='', max_length=1000),
+ ),
+ migrations.AddField(
+ model_name='bountyrequest',
+ name='token_name',
+ field=models.CharField(default='ETH', help_text='token in which the requested bounty is to be funded', max_length=50),
+ ),
+ migrations.AddField(
+ model_name='bountyrequest',
+ name='tribe',
+ field=models.ForeignKey(help_text='tribe which is being requested to fund the issue', null=True, on_delete=django.db.models.deletion.SET_NULL, to='dashboard.Profile'),
+ ),
+ migrations.AlterField(
+ model_name='bountyrequest',
+ name='amount',
+ field=models.FloatField(help_text='amount for which the requested bounty is to be funded', validators=[django.core.validators.MinValueValidator(1.0)]),
+ ),
+ migrations.AlterField(
+ model_name='bountyrequest',
+ name='comment',
+ field=models.TextField(help_text='description from the requestor to justify the bounty request', max_length=500),
+ ),
+ migrations.AlterField(
+ model_name='bountyrequest',
+ name='github_org_email',
+ field=models.CharField(blank=True, max_length=255),
+ ),
+ migrations.AlterField(
+ model_name='bountyrequest',
+ name='github_org_name',
+ field=models.CharField(blank=True, max_length=50),
+ ),
+ migrations.AlterField(
+ model_name='bountyrequest',
+ name='github_url',
+ field=models.CharField(help_text='github url of suggested bounty', max_length=255),
+ ),
+ migrations.AlterField(
+ model_name='bountyrequest',
+ name='requested_by',
+ field=models.ForeignKey(help_text='profile submitting the bounty request', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bounty_requests', to='dashboard.Profile'),
+ ),
+ migrations.AlterField(
+ model_name='bountyrequest',
+ name='status',
+ field=models.CharField(choices=[('o', 'open'), ('c', 'closed'), ('f', 'funded')], db_index=True, default='o', help_text='status of the bounty request', max_length=1),
+ ),
+ ]
diff --git a/app/bounty_requests/models.py b/app/bounty_requests/models.py
index d66de423afa..8097a137e58 100644
--- a/app/bounty_requests/models.py
+++ b/app/bounty_requests/models.py
@@ -43,24 +43,40 @@ class BountyRequest(SuperModel):
(STATUS_FUNDED, 'funded')
)
- status = models.CharField(max_length=1,
- choices=STATUS_CHOICES,
- default=STATUS_OPEN,
- db_index=True)
+ status = models.CharField(
+ max_length=1,
+ choices=STATUS_CHOICES,
+ default=STATUS_OPEN,
+ db_index=True,
+ help_text='status of the bounty request'
+ )
+
requested_by = models.ForeignKey(
'dashboard.Profile',
null=True,
on_delete=models.SET_NULL,
related_name='bounty_requests',
+ help_text='profile submitting the bounty request'
)
- github_url = models.CharField(max_length=255, default='')
- github_org_name = models.CharField(max_length=50, default='')
- eth_address = models.CharField(max_length=50, blank=True)
- comment = models.TextField(max_length=500, default='')
- comment_admin = models.TextField(max_length=500, blank=True)
- amount = models.FloatField(blank=False, validators=[MinValueValidator(1.0)])
-
- github_org_email = models.CharField(max_length=255, default='')
+
+ tribe = models.ForeignKey(
+ 'dashboard.Profile',
+ null=True,
+ on_delete=models.SET_NULL,
+ db_index=True,
+ help_text='tribe which is being requested to fund the issue'
+ )
+
+ title = models.CharField(max_length=1000, default='')
+ github_url = models.CharField(max_length=255, help_text='github url of suggested bounty')
+ comment = models.TextField(max_length=500, help_text='description from the requestor to justify the bounty request')
+ amount = models.FloatField(blank=False, help_text='amount for which the requested bounty is to be funded', validators=[MinValueValidator(1.0)])
+ token_name = models.CharField(max_length=50, default='ETH', help_text='token in which the requested bounty is to be funded')
+
+ github_org_email = models.CharField(max_length=255, blank=True) # TODO: REMOVE
+ github_org_name = models.CharField(max_length=50, blank=True) # TODO: REMOVE
+ eth_address = models.CharField(max_length=50, blank=True) # TODO: REMOVE
+ comment_admin = models.TextField(max_length=500, blank=True) # TODO: REMOVE
objects = BountyQuerySet.as_manager()
diff --git a/app/bounty_requests/views.py b/app/bounty_requests/views.py
index 1295c655d31..b96b95811ad 100644
--- a/app/bounty_requests/views.py
+++ b/app/bounty_requests/views.py
@@ -19,12 +19,15 @@
"""
import json
+from django.core.exceptions import MultipleObjectsReturned
from django.http import JsonResponse
from django.template.response import TemplateResponse
from django.utils.translation import gettext_lazy as _
from django.views.decorators.csrf import csrf_exempt
+from django.views.decorators.http import require_POST
import requests
+from dashboard.models import Bounty, Profile, TribeMember
from linkshortener.models import Link
from marketing.mails import new_bounty_request
from ratelimit.decorators import ratelimit
@@ -86,3 +89,179 @@ def bounty_request(request):
}
return TemplateResponse(request, 'bounty_request_form.html', params)
+
+
+
+@require_POST
+@csrf_exempt
+@ratelimit(key='ip', rate='5/s', method=ratelimit.UNSAFE, block=True)
+def create_bounty_request_v1(request):
+ response = {
+ 'status': 400,
+ 'message': 'error: Bad Request. Unable to create bounty request'
+ }
+
+ user = request.user if request.user.is_authenticated else None
+
+ if not user:
+ response['message'] = 'error: user needs to be authenticated to submit bounty request'
+ return JsonResponse(response)
+
+ profile = request.user.profile if hasattr(request.user, 'profile') else None
+
+ if not profile:
+ response['message'] = 'error: no matching profile found'
+ return JsonResponse(response)
+
+ if not request.method == 'POST':
+ response['message'] = 'error: create bounty request is a POST operation'
+ return JsonResponse(response)
+
+ github_url = request.POST.get("github_url", None)
+
+ if not github_url:
+ response['message'] = 'error: missing github_url parameter'
+ return JsonResponse(response)
+
+ if Bounty.objects.filter(github_url=github_url).exists():
+ response = {
+ 'status': 303,
+ 'message': 'bounty already exists for this github issue'
+ }
+ return JsonResponse(response)
+
+ title = request.POST.get("title", None)
+ if not title:
+ response['title'] = 'error: missing title parameter'
+ return JsonResponse(response)
+
+ comment = request.POST.get("comment", None)
+ if not comment:
+ response['message'] = 'error: missing comment parameter'
+ return JsonResponse(response)
+
+ amount = request.POST.get("amount", None)
+ if not amount:
+ response['message'] = 'error: missing amount parameter'
+ return JsonResponse(response)
+
+ tribe = request.POST.get("tribe", None)
+ if not tribe:
+ response['message'] = 'error: missing tribe parameter'
+ return JsonResponse(response)
+
+ try:
+ tribe_profile = Profile.objects.get(handle=tribe)
+ except Profile.DoesNotExist:
+ response = {
+ 'status': 400,
+ 'message': 'error: could not find tribe'
+ }
+ return JsonResponse(response)
+ except Profile.MultipleObjectsReturned:
+ response = {
+ 'status': 500,
+ 'message': 'error: found multiple tribes'
+ }
+ return JsonResponse(response)
+
+
+ token_name = request.POST.get("token_name", 'ETH')
+ if token_name == '':
+ token_name = 'ETH'
+
+ bounty_request = BountyRequest()
+ bounty_request.requested_by = profile
+ bounty_request.github_url = github_url
+ bounty_request.amount = amount
+ bounty_request.token_name = token_name
+ bounty_request.comment = comment
+ bounty_request.title = title
+ bounty_request.tribe = tribe_profile
+
+ bounty_request.save()
+
+ try:
+ TribeMember.objects.get(profile=profile, org=tribe_profile)
+ except TribeMember.DoesNotExist:
+ kwargs = {
+ 'org': tribe_profile,
+ 'profile': profile
+ }
+ tribemember = TribeMember.objects.create(**kwargs)
+ tribemember.save()
+
+
+ response = {
+ 'status': 204,
+ 'message': 'bounty request successfully created'
+ }
+
+ return JsonResponse(response)
+
+
+@require_POST
+@csrf_exempt
+@ratelimit(key='ip', rate='5/s', method=ratelimit.UNSAFE, block=True)
+def update_bounty_request_v1(request):
+ response = {
+ 'status': 400,
+ 'message': 'error: Bad Request. Unable to edit bounty request'
+ }
+
+ user = request.user if request.user.is_authenticated else None
+
+ if not user:
+ response['message'] = 'error: user needs to be authenticated to edit bounty request'
+ return JsonResponse(response)
+
+ profile = request.user.profile if hasattr(request.user, 'profile') else None
+
+ if not profile:
+ response['message'] = 'error: no matching profile found'
+ return JsonResponse(response)
+
+ if not request.method == 'POST':
+ response['message'] = 'error: create bounty edit is a POST operation'
+ return JsonResponse(response)
+
+ bounty_request_id = request.POST.get("bounty_request_id", None)
+
+ if not bounty_request_id:
+ response['message'] = 'error: missing bounty_request_id parameter'
+ return JsonResponse(response)
+
+ try:
+ bounty_request = BountyRequest.objects.get(pk=bounty_request_id)
+ except BountyRequest.DoesNotExist:
+ response = {
+ 'status': 404,
+ 'message': 'request bounty request does not exsist'
+ }
+ return JsonResponse(response)
+ except BountyRequest.MultipleObjectsReturned:
+ response = {
+ 'status': 500,
+ 'message': 'error: found multiple bounty requests'
+ }
+ return JsonResponse(response)
+
+ is_my_org = any([bounty_request.tribe.handle.lower() == org.lower() for org in request.user.profile.organizations ])
+ if not is_my_org:
+ response = {
+ 'status': 405,
+ 'message': 'operation on bounty request can be performed by tribe owner'
+ }
+ return JsonResponse(response)
+
+ request_status = request.POST.get("request_status", None)
+ if request_status == 'c' and bounty_request.status != 'c':
+ bounty_request.status = 'c'
+ bounty_request.save()
+
+ response = {
+ 'status': 200,
+ 'message': 'bounty request updated successfully'
+ }
+
+ return JsonResponse(response)
diff --git a/app/chat/templates/chat.html b/app/chat/templates/chat.html
index ef181fa495d..eeb01279363 100644
--- a/app/chat/templates/chat.html
+++ b/app/chat/templates/chat.html
@@ -130,7 +130,7 @@
{% trans "Mac" %}
-
+
{% trans "Windows" %}
diff --git a/app/compliance/admin.py b/app/compliance/admin.py
index 2a8d4ca20f5..efe769567f8 100644
--- a/app/compliance/admin.py
+++ b/app/compliance/admin.py
@@ -7,5 +7,11 @@ class GeneralAdmin(admin.ModelAdmin):
ordering = ['-id']
list_display = ['created_on', '__str__']
+
+class EntityAdmin(admin.ModelAdmin):
+ ordering = ['-id']
+ list_display = ['created_on', '__str__']
+ search_fields = ['firstName', 'lastName', 'fullName', 'city', 'country']
+
admin.site.register(Country, GeneralAdmin)
-admin.site.register(Entity, GeneralAdmin)
+admin.site.register(Entity, EntityAdmin)
diff --git a/app/dashboard/admin.py b/app/dashboard/admin.py
index 7738026d4b7..8896f8e3075 100644
--- a/app/dashboard/admin.py
+++ b/app/dashboard/admin.py
@@ -19,6 +19,7 @@
from __future__ import unicode_literals
from django.contrib import admin
+from django.utils import timezone
from django.utils.html import format_html
from django.utils.safestring import mark_safe
@@ -89,9 +90,27 @@ class ToolAdmin(admin.ModelAdmin):
class ActivityAdmin(admin.ModelAdmin):
ordering = ['-id']
- raw_id_fields = ['bounty', 'profile', 'tip', 'kudos', 'grant', 'subscription', 'other_profile']
+ raw_id_fields = ['bounty', 'profile', 'tip', 'kudos', 'grant', 'subscription', 'other_profile', 'kudos_transfer']
search_fields = ['metadata', 'activity_type', 'profile__handle']
+ def response_change(self, request, obj):
+ from django.shortcuts import redirect
+ if "_make_nano_bounty" in request.POST:
+ from townsquare.models import Offer
+ obj = Offer.objects.create(
+ created_by=obj.profile,
+ title='Offer for x ETH',
+ desc=obj.metadata.get('title', ''),
+ key='top',
+ url=obj.url,
+ valid_from=timezone.now(),
+ valid_to=timezone.now() + timezone.timedelta(days=1),
+ public=False,
+ )
+ self.message_user(request, "Nano bounty made - You still need to make it public + edit amounts tho.")
+ return redirect(obj.admin_url)
+ return super().response_change(request, obj)
+
class TokenApprovalAdmin(admin.ModelAdmin):
raw_id_fields = ['profile']
@@ -170,7 +189,7 @@ def response_change(self, request, obj):
obj.calculate_all()
obj.save()
self.message_user(request, "Recalc done")
- return redirect(obj.url)
+ return redirect(obj.admin_url)
if "_impersonate" in request.POST:
return redirect(f"/impersonate/{obj.user.pk}/")
return super().response_change(request, obj)
diff --git a/app/dashboard/export.py b/app/dashboard/export.py
new file mode 100644
index 00000000000..830e9e69e0a
--- /dev/null
+++ b/app/dashboard/export.py
@@ -0,0 +1,304 @@
+from grants.models import Grant
+from rest_framework import serializers
+
+from .models import Activity, Bounty, FeedbackEntry, Tip
+
+
+class ProfileExportSerializer(serializers.BaseSerializer):
+ """Handle serializing the exported Profile object."""
+
+ def to_representation(self, instance):
+ """Provide the serialized representation of the Profile.
+
+ Notice: Add understore (_) before a key to indicate it's a private key/value pair, otherwise the key/value pair will be saved publicly after exported.
+
+ Args:
+ instance (Profile): The Profile object to be serialized.
+
+ Returns:
+ dict: The serialized Profile.
+
+ """
+
+ d = instance.as_dict
+
+ return {
+ # basic info
+ 'id': instance.id,
+ 'username': instance.handle,
+ 'full_name': instance.user.get_full_name(),
+ 'gitcoin_url': instance.absolute_url,
+ 'github_url': instance.github_url,
+ 'avatar_url': instance.avatar_url,
+ 'wallpaper': instance.profile_wallpaper,
+ 'keywords': instance.keywords,
+ 'portfolio_keywords': d['portfolio_keywords'],
+ 'position': instance.get_contributor_leaderboard_index(),
+ '_locations': instance.locations,
+ 'organizations': instance.get_who_works_with(network=None),
+ '_email': instance.email,
+ '_gitcoin_discord_username': instance.gitcoin_discord_username,
+ '_pref_lang_code': instance.pref_lang_code,
+ '_preferred_payout_address': instance.preferred_payout_address,
+ 'persona': instance.selected_persona or instance.dominant_persona,
+ 'persona_is_funder': instance.persona_is_funder,
+ 'persona_is_hunter': instance.persona_is_hunter,
+
+ # job info
+ # 'linkedin_url': instance.linkedin_url,
+ # '_job_search_status': instance.job_search_status,
+ # '_job_type': instance.job_type,
+ # '_job_salary': instance.job_salary,
+ # '_job_location': instance.job_location,
+ # '_resume': instance.resume,
+
+ # stats
+ 'last_visit': instance.last_visit,
+ 'longest_streak': instance.longest_streak,
+ 'activity_level': instance.activity_level,
+ 'avg_hourly_rate': instance.avg_hourly_rate,
+ 'success_rate': instance.success_rate,
+ 'reliability': instance.reliability,
+ 'rank_funder': instance.rank_funder,
+ 'rank_org': instance.rank_org,
+ 'rank_coder': instance.rank_coder,
+ # contribution v.s. funding
+ 'completed_bounties_count': d['count_bounties_completed'],
+ 'funded_bounties_count': d['funded_bounties_count'],
+ 'earnings_total': d['earnings_total'],
+ 'earnings_count': d['earnings_count'],
+ 'spent_total': d['spent_total'],
+ 'spent_count': d['spent_count'],
+ 'sum_eth_collected': d['sum_eth_collected'],
+ 'sum_eth_funded': d['sum_eth_funded'],
+ 'hackathons_participated_in': d['hackathons_participated_in'],
+ 'hackathons_funded': d['hackathons_funded'],
+ 'total_tips_sent': d['total_tips_sent'],
+ 'total_tips_received': d['total_tips_received'],
+ # rating
+ 'overall_average_rating': d['avg_rating']['overall'],
+ 'code_quality_average_rating': d['avg_rating']['code_quality_rating'],
+ 'communication_rating': d['avg_rating']['communication_rating'],
+ 'recommendation_rating': d['avg_rating']['recommendation_rating'],
+ 'satisfaction_rating': d['avg_rating']['satisfaction_rating'],
+ 'speed_rating': d['avg_rating']['speed_rating'],
+ 'total_rating_count': d['avg_rating']['total_rating'],
+ }
+
+
+class GrantExportSerializer(serializers.ModelSerializer):
+ """Handle serializing the exported Grant object."""
+ org = serializers.SerializerMethodField()
+ created_at = serializers.SerializerMethodField()
+ url = serializers.SerializerMethodField()
+ contribution_count = serializers.SerializerMethodField()
+ contributor_count = serializers.SerializerMethodField()
+
+ class Meta:
+
+ model = Grant
+ fields = ('id', 'active', 'grant_type', 'title', 'slug',
+ 'description', 'description_rich', 'reference_url', 'logo',
+ 'admin_address', 'contract_owner_address', 'amount_goal',
+ 'monthly_amount_subscribed', 'amount_received', 'token_address',
+ 'token_symbol', 'contract_address', 'network',
+ 'org', 'created_at', 'url', ''
+ )
+
+ def get_created_at(self, instance):
+ return instance.created_on.isoformat()
+
+ def get_org(self, instance):
+ return instance.org_name()
+
+ def get_url(self, instance):
+ return instance.get_absolute_url()
+
+ def get_contributor_count(self, instance):
+ return instance.get_contributor_count()
+
+ def get_contribution_count(self, instance):
+ return instance.get_contribution_count()
+
+
+class BountyExportSerializer(serializers.ModelSerializer):
+ """Handle serializing the exported Bounty object."""
+ gitcoin_link = serializers.CharField(source='get_absolute_url')
+ status = serializers.CharField(source='idx_status')
+ gitcoin_provider = serializers.CharField(default='gitcoin')
+ github_provider = serializers.CharField(default='github')
+ created_at = serializers.SerializerMethodField()
+ expires_at = serializers.SerializerMethodField()
+
+ class Meta:
+
+ model = Bounty
+ fields = ('id', 'title', 'gitcoin_link', 'github_url', 'token_name', 'token_address',
+ 'bounty_type', 'project_type', 'bounty_categories', 'project_length',
+ 'estimated_hours', 'experience_level', 'value_in_token', 'value_in_usdt',
+ 'bounty_reserved_for_user', 'is_open', 'standard_bounties_id', 'accepted',
+ 'funding_organisation', 'gitcoin_provider', 'github_provider',
+ 'canceled_bounty_reason', 'submissions_comment', 'fulfillment_accepted_on',
+ 'fulfillment_submitted_on', 'fulfillment_started_on', 'canceled_on',
+ 'created_at', 'expires_at', 'status'
+ )
+
+ def get_created_at(self, instance):
+ return instance.created_on.isoformat()
+
+ def get_expires_at(self, instance):
+ return instance.expires_date.isoformat()
+
+
+class ActivityExportSerializer(serializers.ModelSerializer):
+ """Handle serializing the exported Activity object."""
+
+ created_at = serializers.SerializerMethodField()
+ url = serializers.SerializerMethodField()
+ category = serializers.CharField(source='activity_type')
+ action = serializers.SerializerMethodField()
+
+ class Meta:
+ model = Activity
+ fields = ('id', 'created_at', 'category', 'action', 'url', )
+
+ def get_created_at(self, instance):
+ return instance.created.isoformat()
+
+ def get_url(self, instance):
+ action = self.get_action(instance)
+ if action in ('bounty', ):
+ return instance.bounty.get_absolute_url()
+
+ if action in ('kudos', ):
+ return instance.kudos.kudos_token.get_absolute_url()
+
+ if action in ('profile', ):
+ return instance.profile.absolute_url
+
+ return ''
+
+ def get_action(self, instance):
+ action = ''
+ t = instance.activity_type
+ if t in ('joined', 'updated_avatar'):
+ action = 'profile'
+ elif t in ('bounty_abandonment_warning', 'bounty_removed_by_funder',
+ 'bounty_removed_slashed_by_staff', 'bounty_removed_by_staff','new_bounty',
+ 'start_work', 'stop_work', 'work_done', 'worker_approved', 'worker_rejected',
+ 'worker_applied', 'increased_bounty', 'killed_bounty',
+ 'bounty_abandonment_escalation_to_mods', 'new_crowdfund', 'work_submitted'
+ ):
+ action = 'bounty'
+ elif t in ('new_kudos',):
+ action = 'kudos'
+ elif t in ('new_tip', 'receive_tip'):
+ action = 'tip'
+
+ return action
+
+class TipExportSerializer(serializers.ModelSerializer):
+ bounty_url = serializers.SerializerMethodField()
+ created_at = serializers.SerializerMethodField()
+ expires_at = serializers.SerializerMethodField()
+ sender = serializers.SerializerMethodField()
+ recipient = serializers.SerializerMethodField()
+ status = serializers.CharField(source='tx_status')
+ token = serializers.CharField(source='tokenName')
+ token_address = serializers.CharField(source='tokenAddress')
+ transaction_address = serializers.CharField(source='receive_address')
+ public_comments = serializers.CharField(source='comments_public')
+ private_comments = serializers.CharField(source='comments_priv')
+
+ class Meta:
+ model = Tip
+ fields = ('id', 'status', 'sender', 'recipient', 'amount', 'token', 'token_address',
+ 'transaction_address', 'bounty_url', 'value_in_usdt', 'public_comments',
+ 'private_comments', 'created_at', 'expires_at')
+
+ def get_bounty_url(self, instance):
+ bounty = instance.bounty
+ if bounty:
+ return bounty.get_absolute_url()
+
+ return ''
+
+ def get_created_at(self, instance):
+ return instance.created_on.isoformat()
+
+ def get_expires_at(self, instance):
+ return instance.expires_date.isoformat()
+
+ def get_sender(self, instance):
+ return instance.sender_profile.handle
+
+ def get_recipient(self, instance):
+ return instance.recipient_profile.handle
+
+class FeedbackExportSerializer(serializers.ModelSerializer):
+ bounty_url = serializers.SerializerMethodField()
+ created_at = serializers.SerializerMethodField()
+ sender = serializers.SerializerMethodField()
+ recipient = serializers.SerializerMethodField()
+ feedback_type = serializers.CharField(source='feedbackType')
+ comment = serializers.SerializerMethodField()
+
+ class Meta:
+ model = FeedbackEntry
+ fields = ('id', 'sender', 'recipient', 'bounty_url', 'rating', 'satisfaction_rating',
+ 'communication_rating', 'speed_rating', 'code_quality_rating',
+ 'recommendation_rating', 'feedback_type', 'comment')
+
+ def get_bounty_url(self, instance):
+ bounty = instance.bounty
+ if bounty:
+ return bounty.get_absolute_url()
+
+ return ''
+
+ def get_created_at(self, instance):
+ return instance.created_on.isoformat()
+
+ def get_sender(self, instance):
+ return instance.sender_profile.handle
+
+ def get_recipient(self, instance):
+ return instance.recipient_profile.handle
+
+ def get_comment(self, instance):
+ return instance.anonymized_comment
+
+privacy_fields = {
+ "tip": ["private_comments"],
+ "feedback": []
+}
+
+exporters = {
+ "tip": TipExportSerializer,
+ "feedback": FeedbackExportSerializer
+}
+
+def filter_items(model, data, private):
+ private_keys = privacy_fields[model]
+ if private:
+ private_keys.append("id")
+ return [{k:item[k] for k in private_keys} for item in data]
+ else:
+ public_keys = list(set(data[0].keys()) - set(private_keys))
+ return [{k:item[k] for k in public_keys} for item in data]
+
+def filtered_list_data(model, items, private_items, private_fields):
+ if private_items is not None:
+ items = items.filter(private=private_items)
+ exporter = exporters[model]
+ data = exporter(items, many=True).data
+
+ if private_items:
+ return data
+ else:
+ if private_fields is None:
+ return data
+ elif private_fields:
+ return filter_items(model, data, private=True)
+ else:
+ return filter_items(model, data, private=False)
diff --git a/app/dashboard/helpers.py b/app/dashboard/helpers.py
index 19b2348e9f7..1aeea7e3a47 100644
--- a/app/dashboard/helpers.py
+++ b/app/dashboard/helpers.py
@@ -25,13 +25,14 @@
from django.conf import settings
from django.conf.urls.static import static
-from django.core.exceptions import ValidationError
+from django.core.exceptions import MultipleObjectsReturned, ValidationError
from django.core.validators import URLValidator
from django.db import transaction
from django.http import Http404, HttpResponseBadRequest, JsonResponse
from django.utils import timezone
from app.utils import get_semaphore, sync_profile
+from bounty_requests.models import BountyRequest
from dashboard.models import (
Activity, BlockedURLFilter, Bounty, BountyDocuments, BountyEvent, BountyFulfillment, BountyInvites,
BountySyncRequest, Coupon, HackathonEvent, UserAction,
@@ -303,60 +304,91 @@ def handle_bounty_fulfillments(fulfillments, new_bounty, old_bounty):
"""
from dashboard.utils import is_blocked
+
for fulfillment in fulfillments:
- kwargs = {}
- accepted_on = None
- github_username = fulfillment.get('data', {}).get(
- 'payload', {}).get('fulfiller', {}).get(
- 'githubUsername', '')
- if github_username:
- if is_blocked(github_username):
- continue
- try:
- kwargs['profile_id'] = Profile.objects.get(handle__iexact=github_username).pk
- except Profile.MultipleObjectsReturned:
- kwargs['profile_id'] = Profile.objects.filter(handle__iexact=github_username).first().pk
- except Profile.DoesNotExist:
- pass
- if fulfillment.get('accepted'):
- kwargs['accepted'] = True
- accepted_on = timezone.now()
+ fulfillment_id = fulfillment.get('id')
+ old_fulfillment = None
try:
- created_on = timezone.now()
- modified_on = timezone.now()
- if old_bounty:
- old_fulfillments = old_bounty.fulfillments.filter(fulfillment_id=fulfillment.get('id')).nocache()
- if old_fulfillments.exists():
- old_fulfillment = old_fulfillments.first()
- created_on = old_fulfillment.created_on
- modified_on = old_fulfillment.modified_on
- if old_fulfillment.accepted:
- accepted_on = old_fulfillment.accepted_on
- hours_worked = fulfillment.get('data', {}).get(
+ old_fulfillment = BountyFulfillment.objects.get(bounty=old_bounty, fulfillment_id=fulfillment_id)
+
+ except MultipleObjectsReturned as error:
+ logger.warning(f'error: found duplicate fulfillments for bounty {old_bounty} {error}')
+ old_bounty_fulfillments = BountyFulfillment.objects.filter(fulfillment_id=fulfillment_id, bounty=old_bounty).nocache()
+ if old_bounty_fulfillments.exists():
+ old_fulfillment = old_bounty_fulfillments.first()
+
+ except BountyFulfillment.DoesNotExist as error:
+ logger.warning(f'info: bounty {old_bounty} has no fulfillments in db {error}')
+
+ if old_fulfillment:
+ if not old_fulfillment.accepted and fulfillment.get('accepted'):
+ # update fulfillment to accepted + reference to new bounty
+ now = timezone.now()
+ old_fulfillment.modified_on = now
+ old_fulfillment.accepted_on = now
+ old_fulfillment.accepted = True
+ old_fulfillment.bounty = new_bounty
+ old_fulfillment.save()
+ else:
+ # create new fulfillment object
+ kwargs = {}
+ accepted_on = None
+ github_username = fulfillment.get('data', {}).get(
+ 'payload', {}).get('fulfiller', {}).get('githubUsername', '')
+ if github_username:
+ if is_blocked(github_username):
+ continue
+ try:
+ kwargs['profile_id'] = Profile.objects.get(handle__iexact=github_username).pk
+ except Profile.MultipleObjectsReturned:
+ kwargs['profile_id'] = Profile.objects.filter(handle__iexact=github_username).first().pk
+ except Profile.DoesNotExist:
+ pass
+ if fulfillment.get('accepted'):
+ kwargs['accepted'] = True
+ accepted_on = timezone.now()
+ try:
+ created_on = timezone.now()
+ modified_on = timezone.now()
+ fulfiller_email = fulfillment.get('data', {}).get(
+ 'payload', {}).get('fulfiller', {}).get('email', '')
+ fulfiller_name = fulfillment.get('data', {}).get(
+ 'payload', {}).get('fulfiller', {}).get('name', '')
+ fulfiller_github_url = fulfillment.get('data', {}).get(
+ 'payload', {}).get('fulfiller', {}).get('githubPRLink', '')
+ hours_worked = fulfillment.get('data', {}).get(
'payload', {}).get('fulfiller', {}).get('hoursWorked', None)
- if not hours_worked or not hours_worked.isdigit():
- hours_worked = None
- new_bounty.fulfillments.create(
- fulfiller_address=fulfillment.get(
+ fulfiller_address = fulfillment.get(
'fulfiller',
- '0x0000000000000000000000000000000000000000'),
- fulfiller_email=fulfillment.get('data', {}).get(
- 'payload', {}).get('fulfiller', {}).get('email', ''),
- fulfiller_github_username=github_username,
- fulfiller_name=fulfillment.get('data', {}).get(
- 'payload', {}).get('fulfiller', {}).get('name', ''),
- fulfiller_metadata=fulfillment,
- fulfillment_id=fulfillment.get('id'),
- fulfiller_github_url=fulfillment.get('data', {}).get(
- 'payload', {}).get('fulfiller', {}).get('githubPRLink', ''),
- fulfiller_hours_worked=hours_worked,
- created_on=created_on,
- modified_on=modified_on,
- accepted_on=accepted_on,
- **kwargs)
- except Exception as e:
- logger.error(f'{e} during new fulfillment creation for {new_bounty}')
- continue
+ '0x0000000000000000000000000000000000000000'
+ )
+ if not hours_worked.isdigit():
+ hours_worked = None
+
+ new_bounty.fulfillments.create(
+ fulfiller_address=fulfiller_address,
+ fulfiller_email=fulfiller_email,
+ fulfiller_github_username=github_username,
+ fulfiller_name=fulfiller_name,
+ fulfiller_metadata=fulfillment,
+ fulfillment_id=fulfillment.get('id'),
+ fulfiller_github_url=fulfiller_github_url,
+ fulfiller_hours_worked=hours_worked,
+ created_on=created_on,
+ modified_on=modified_on,
+ accepted_on=accepted_on,
+ **kwargs
+ )
+ except Exception as e:
+ logger.error(f'{e} during new fulfillment creation for {new_bounty}')
+ continue
+
+ old_bounty_fulfillments = BountyFulfillment.objects.filter(bounty=old_bounty)
+ if old_bounty_fulfillments:
+ # fail safe to ensure all fulfillments are migrated over
+ for fulfillment in old_bounty_fulfillments:
+ fulfillment.bounty = new_bounty.id
+ fulfillment.save()
if new_bounty:
return new_bounty.fulfillments.all()
@@ -389,6 +421,13 @@ def create_new_bounty(old_bounties, bounty_payload, bounty_details, bounty_id):
else:
raise UnsupportedSchemaException('No webReferenceURL found. Cannot continue!')
+ try:
+ bounty_request = BountyRequest.objects.get(github_url=url, status='o')
+ bounty_request.status = 'f'
+ bounty_request.save()
+ except BountyRequest.DoesNotExist:
+ pass
+
# check conditions for private repos
if metadata.get('repo_type', None) == 'private' and \
bounty_payload.get('schemes', {}).get('permission_type', 'permissionless') != 'approval' and \
@@ -628,8 +667,6 @@ def merge_bounty(latest_old_bounty, new_bounty, metadata, bounty_details, verbos
new_bounty.canceled_on = canceled_on
new_bounty.save()
- # migrate fulfillments, and only take the ones from
- # fulfillments metadata will be empty when bounty is first created
fulfillments = bounty_details.get('fulfillments', {})
if fulfillments:
handle_bounty_fulfillments(fulfillments, new_bounty, latest_old_bounty)
@@ -637,6 +674,7 @@ def merge_bounty(latest_old_bounty, new_bounty, metadata, bounty_details, verbos
for inactive in Bounty.objects.filter(
current_bounty=False, github_url=url
).nocache().order_by('-created_on'):
+ # TODO: evalute if this can be removed
BountyFulfillment.objects.filter(bounty_id=inactive.id).nocache().delete()
# preserve featured status for bounties where it was set manually
diff --git a/app/dashboard/management/commands/create_search_results.py b/app/dashboard/management/commands/create_search_results.py
new file mode 100644
index 00000000000..570c7414549
--- /dev/null
+++ b/app/dashboard/management/commands/create_search_results.py
@@ -0,0 +1,121 @@
+'''
+ Copyright (C) 2018 Gitcoin Core
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+
+'''
+
+from django.conf import settings
+from django.contrib.contenttypes.models import ContentType
+from django.core.management.base import BaseCommand
+from django.urls import reverse
+from django.utils import timezone
+
+import requests
+from app.sitemaps import StaticViewSitemap
+from bs4 import BeautifulSoup
+from dashboard.models import Bounty, HackathonEvent, Profile
+from grants.models import Grant
+from kudos.models import Token
+from quests.models import Quest
+from retail.utils import strip_html
+from search.models import Page, ProgrammingLanguage, SearchResult
+
+
+class Command(BaseCommand):
+
+ help = 'creates earnings records for deploy of https://github.com/gitcoinco/web/pull/5093'
+
+ def handle(self, *args, **options):
+
+ # regular URLs
+ svs = StaticViewSitemap()
+ for item in svs.items():
+ try:
+ uri = reverse(item)
+ url = f"{settings.BASE_URL}{uri}".replace(f"/{uri}", f"{uri}")
+
+ html_response = requests.get(url)
+ soup = BeautifulSoup(html_response.text, 'html.parser')
+ title = soup.findAll("title")[0].text
+ try:
+ description = soup.findAll("meta", {"name": 'description'})[0].text
+ except:
+ description = ''
+ try:
+ img_url = soup.findAll("meta", {"name": 'twitter:image'})[0].get('content')
+ print(img_url)
+ except:
+ img_url = ''
+ valid_title ='Grow Open Source' not in title and 'GitHub' not in title
+ title = title if valid_title else item.capitalize() + " Page"
+ print(title, item, url, img_url)
+ obj, created = Page.objects.update_or_create(
+ key=item,
+ defaults={
+ "title":title,
+ "description":description,
+ }
+ )
+ if obj.pk:
+ SearchResult.objects.update_or_create(
+ source_type=ContentType.objects.get(app_label='search', model='page'),
+ source_id=obj.pk,
+ defaults={
+ "title":title,
+ "description":description,
+ "url":url,
+ "visible_to":None,
+ 'img_url': img_url,
+ }
+ )
+ except Exception as e:
+ print(item, e)
+
+
+ # prog languages
+ from retail.utils import programming_languages_full
+ for pl in programming_languages_full:
+ obj, created = ProgrammingLanguage.objects.update_or_create(val=pl)
+ urls = [f"/explorer?q={pl}", f"/users?q={pl}"]
+ for url in urls:
+ title = f"View {pl} Bounties"
+ if 'users' in url:
+ title = f"View {pl} Coders"
+ description = title
+ if obj.pk:
+ SearchResult.objects.update_or_create(
+ source_type=ContentType.objects.get(app_label='search', model='programminglanguage'),
+ source_id=obj.pk,
+ title=title,
+ defaults={
+ "description":description,
+ "url":url,
+ "visible_to":None,
+ }
+ )
+ # objects
+ qses = [
+ Grant.objects.all(),
+ Token.objects.filter(num_clones_allowed__gt=0),
+ Bounty.objects.current(),
+ Quest.objects.filter(visible=True),
+ Profile.objects.filter(hide_profile=False),
+ HackathonEvent.objects.all(),
+ ]
+ for qs in qses:
+ print(qs)
+ for obj in qs:
+ print(obj.pk)
+ obj.save()
diff --git a/app/dashboard/migrations/0077_remove_activity_kudos.py b/app/dashboard/migrations/0077_remove_activity_kudos.py
new file mode 100644
index 00000000000..5eaba8c56e9
--- /dev/null
+++ b/app/dashboard/migrations/0077_remove_activity_kudos.py
@@ -0,0 +1,17 @@
+# Generated by Django 2.2.4 on 2020-01-17 17:55
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('dashboard', '0076_merge_20200117_1700'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='activity',
+ name='kudos',
+ ),
+ ]
diff --git a/app/dashboard/migrations/0078_auto_20200117_1755.py b/app/dashboard/migrations/0078_auto_20200117_1755.py
new file mode 100644
index 00000000000..954e368d8ac
--- /dev/null
+++ b/app/dashboard/migrations/0078_auto_20200117_1755.py
@@ -0,0 +1,25 @@
+# Generated by Django 2.2.4 on 2020-01-17 17:55
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('kudos', '0011_auto_20191106_0237'),
+ ('dashboard', '0077_remove_activity_kudos'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='activity',
+ name='kudos',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='activities', to='kudos.Token'),
+ ),
+ migrations.AddField(
+ model_name='activity',
+ name='kudos_transfer',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='activities', to='kudos.KudosTransfer'),
+ ),
+ ]
diff --git a/app/dashboard/migrations/0079_auto_20200204_0629.py b/app/dashboard/migrations/0079_auto_20200204_0629.py
new file mode 100644
index 00000000000..167703d7ed6
--- /dev/null
+++ b/app/dashboard/migrations/0079_auto_20200204_0629.py
@@ -0,0 +1,23 @@
+# Generated by Django 2.2.4 on 2020-02-04 06:29
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('dashboard', '0078_profile_as_representation'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='profile',
+ name='tribe_priority',
+ field=models.TextField(blank=True, default='', help_text='HTML rich description for what tribe priorities.'),
+ ),
+ migrations.AlterField(
+ model_name='profile',
+ name='tribe_description',
+ field=models.TextField(blank=True, default='', help_text='HTML rich description describing tribe.'),
+ ),
+ ]
diff --git a/app/dashboard/migrations/0079_tribemember_title.py b/app/dashboard/migrations/0079_tribemember_title.py
new file mode 100644
index 00000000000..ea93a137886
--- /dev/null
+++ b/app/dashboard/migrations/0079_tribemember_title.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2.4 on 2020-01-29 12:14
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('dashboard', '0078_profile_as_representation'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='tribemember',
+ name='title',
+ field=models.CharField(blank=True, default='', max_length=255),
+ ),
+ ]
diff --git a/app/dashboard/migrations/0080_merge_20200205_1511.py b/app/dashboard/migrations/0080_merge_20200205_1511.py
new file mode 100644
index 00000000000..8f8e5101523
--- /dev/null
+++ b/app/dashboard/migrations/0080_merge_20200205_1511.py
@@ -0,0 +1,15 @@
+# Generated by Django 2.2.4 on 2020-02-05 15:11
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('dashboard', '0078_auto_20200117_1755'),
+ ('dashboard', '0079_auto_20200204_0629'),
+ ('dashboard', '0079_tribemember_title'),
+ ]
+
+ operations = [
+ ]
diff --git a/app/dashboard/migrations/0081_auto_20200205_1521.py b/app/dashboard/migrations/0081_auto_20200205_1521.py
new file mode 100644
index 00000000000..27d0f60c3c2
--- /dev/null
+++ b/app/dashboard/migrations/0081_auto_20200205_1521.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2.4 on 2020-02-05 15:21
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('dashboard', '0080_merge_20200205_1511'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='profile',
+ name='automatic_backup',
+ field=models.BooleanField(default=False, help_text='automatic backup profile to cloud storage such as 3Box if the flag is true'),
+ ),
+ ]
diff --git a/app/dashboard/models.py b/app/dashboard/models.py
index 95328a717ac..4b2118b712d 100644
--- a/app/dashboard/models.py
+++ b/app/dashboard/models.py
@@ -836,7 +836,7 @@ def value_in_usdt_then(self):
def value_in_usdt_at_time(self, at_time):
decimals = 10 ** 18
if self.token_name == 'USDT':
- return float(self.value_in_token)
+ return float(self.value_in_token / 10 ** 6)
if self.token_name in settings.STABLE_COINS:
return float(self.value_in_token / 10 ** 18)
try:
@@ -1666,6 +1666,26 @@ class Tip(SendCryptoAsset):
'dashboard.Profile', related_name='sent_tips', on_delete=models.SET_NULL, null=True, blank=True
)
+ @property
+ def is_programmatic_comment(self):
+ if 'activity:' in self.comments_priv:
+ return True
+ if 'comment:' in self.comments_priv:
+ return True
+
+ @property
+ def attached_object(self):
+ if 'activity:' in self.comments_priv:
+ pk = self.comments_priv.split(":")[1]
+ obj = Activity.objects.get(pk=pk)
+ return obj
+ if 'comment:' in self.comments_priv:
+ pk = self.comments_priv.split(":")[1]
+ from townsquare.models import Comment
+ obj = Comment.objects.get(pk=pk)
+ return obj
+
+
@property
def receive_url(self):
if self.web3_type == 'yge':
@@ -1729,9 +1749,9 @@ def psave_tip(sender, instance, **kwargs):
@receiver(post_save, sender=Tip, dispatch_uid="post_save_tip")
-def postsave_tip(sender, instance, **kwargs):
+def postsave_tip(sender, instance, created, **kwargs):
is_valid = instance.sender_profile != instance.recipient_profile and instance.txid
- if is_valid:
+ if instance.pk and is_valid:
Earning.objects.update_or_create(
source_type=ContentType.objects.get(app_label='dashboard', model='tip'),
source_id=instance.pk,
@@ -1745,6 +1765,19 @@ def postsave_tip(sender, instance, **kwargs):
"network":instance.network,
}
)
+ if created:
+ if instance.network == 'mainnet' or settings.DEBUG:
+ from townsquare.models import Comment
+ if 'activity:' in instance.comments_priv:
+ activity=instance.attached_object
+ comment = f"Just sent a tip of {instance.amount} ETH to @{instance.username}"
+ comment = Comment.objects.create(profile=instance.sender_profile, activity=activity, comment=comment)
+ if 'comment:' in instance.comments_priv:
+ _comment=instance.attached_object
+ comment = f"Just sent a tip of {instance.amount} ETH to @{instance.username}"
+ comment = Comment.objects.create(profile=instance.sender_profile, activity=_comment.activity, comment=comment)
+
+
# method for updating
@@ -1786,10 +1819,31 @@ def psave_bounty(sender, instance, **kwargs):
if profiles.exists():
instance.bounty_owner_profile = profiles.first()
+ from django.contrib.contenttypes.models import ContentType
+ from search.models import SearchResult
+ ct = ContentType.objects.get(app_label='dashboard', model='bounty')
+ if instance.current_bounty and instance.pk:
+ SearchResult.objects.update_or_create(
+ source_type=ct,
+ source_id=instance.pk,
+ defaults={
+ "created_on":instance.web3_created,
+ "title":instance.title,
+ "description":instance.issue_description,
+ "url":instance.url,
+ "visible_to":None,
+ 'img_url': instance.get_avatar_url(True),
+ }
+ )
+ # delete any old bounties
+ if instance.prev_bounty and instance.prev_bounty.pk:
+ for sr in SearchResult.objects.filter(source_type=ct, source_id=instance.prev_bounty.pk):
+ sr.delete()
+
@receiver(post_save, sender=BountyFulfillment, dispatch_uid="psave_bounty_fulfill")
def psave_bounty_fulfilll(sender, instance, **kwargs):
- if instance.accepted:
+ if instance.pk and instance.accepted:
Earning.objects.update_or_create(
source_type=ContentType.objects.get(app_label='dashboard', model='bountyfulfillment'),
source_id=instance.pk,
@@ -1989,12 +2043,18 @@ class Activity(SuperModel):
blank=True,
null=True
)
- kudos = models.ForeignKey(
+ kudos_transfer = models.ForeignKey(
'kudos.KudosTransfer',
related_name='activities',
on_delete=models.CASCADE,
blank=True, null=True
)
+ kudos = models.ForeignKey(
+ 'kudos.Token',
+ related_name='activities',
+ on_delete=models.CASCADE,
+ blank=True, null=True
+ )
grant = models.ForeignKey(
'grants.Grant',
related_name='activities',
@@ -2142,7 +2202,7 @@ def view_props(self):
# in a later release, it couild be refactored such that its just contained in the above code block ^^.
activity['icon'] = icons.get(self.activity_type, 'fa-check-circle')
if activity.get('kudos'):
- activity['kudos_data'] = Token.objects.get(pk=self.kudos.kudos_token_cloned_from_id)
+ activity['kudos_data'] = self.kudos
obj = self.metadata
if 'new_bounty' in self.metadata:
obj = self.metadata['new_bounty']
@@ -2164,6 +2224,8 @@ def view_props(self):
10 ** activity['token']['decimals']) * 1000) / 1000
activity['view_count'] = self.view_count
+ activity['tip_count_usd'] = self.tip_count_usd
+ activity['tip_count_eth'] = self.tip_count_eth
# finally done!
@@ -2177,6 +2239,18 @@ def view_props_for(self, user):
vp['liked'] = self.likes.filter(profile=user.profile).exists()
return vp
+ @property
+ def tip_count_usd(self):
+ network = 'rinkeby' if settings.DEBUG else 'mainnet'
+ tips = Tip.objects.filter(comments_priv=f"activity:{self.pk}", network=network)
+ return sum([tip.value_in_usdt for tip in tips])
+
+ @property
+ def tip_count_eth(self):
+ network = 'rinkeby' if settings.DEBUG else 'mainnet'
+ tips = Tip.objects.filter(comments_priv=f"activity:{self.pk}", network=network)
+ return sum([tip.value_in_eth for tip in tips])
+
@property
def secondary_avatar_url(self):
if self.metadata.get('to_username'):
@@ -2474,9 +2548,10 @@ class Profile(SuperModel):
rank_org = models.IntegerField(default=0)
rank_coder = models.IntegerField(default=0)
referrer = models.ForeignKey('dashboard.Profile', related_name='referred', on_delete=models.CASCADE, null=True, db_index=True, blank=True)
- tribe_description = models.TextField(default='', blank=True, help_text=_('HTML rich description.'))
+ tribe_description = models.TextField(default='', blank=True, help_text=_('HTML rich description describing tribe.'))
+ automatic_backup = models.BooleanField(default=False, help_text=_('automatic backup profile to cloud storage such as 3Box if the flag is true'))
as_representation = JSONField(default=dict, blank=True)
-
+ tribe_priority = models.TextField(default='', blank=True, help_text=_('HTML rich description for what tribe priorities.'))
objects = ProfileQuerySet.as_manager()
@property
@@ -2864,6 +2939,18 @@ def is_moderator(self):
"""
return self.user.groups.filter(name='Moderators').exists() if self.user else False
+ @property
+ def is_alpha_tester(self):
+ """Determine whether or not the user is an alpha tester.
+
+ Returns:
+ bool: Whether or not the user is an alpha tester.
+
+ """
+ if self.user.is_staff:
+ return True
+ return self.user.groups.filter(name='Alpha_Testers').exists() if self.user else False
+
@property
def is_staff(self):
"""Determine whether or not the user is a staff member.
@@ -3860,6 +3947,22 @@ def psave_profile(sender, instance, **kwargs):
instance.handle = instance.handle.replace('@', '')
instance.handle = instance.handle.lower()
+ from django.contrib.contenttypes.models import ContentType
+ from search.models import SearchResult
+
+ if instance.pk:
+ SearchResult.objects.update_or_create(
+ source_type=ContentType.objects.get(app_label='dashboard', model='profile'),
+ source_id=instance.pk,
+ defaults={
+ "created_on":instance.created_on,
+ "title":instance.handle,
+ "description":instance.desc,
+ "url":instance.url,
+ "visible_to":None,
+ 'img_url': instance.avatar_url,
+ }
+ )
@receiver(user_logged_in)
def post_login(sender, request, user, **kwargs):
@@ -3904,7 +4007,6 @@ def to_representation(self, instance):
instance.save()
return instance.as_representation
-
@receiver(pre_save, sender=Tip, dispatch_uid="normalize_tip_usernames")
def normalize_tip_usernames(sender, instance, **kwargs):
"""Handle pre-save signals from Tips to normalize Github usernames."""
@@ -4218,6 +4320,10 @@ def get_onboard_url(self):
def get_current_bounties(self):
return Bounty.objects.filter(event=self, network='mainnet').current()
+ @property
+ def url(self):
+ return settings.BASE_URL + self.slug
+
@property
def stats(self):
stats = {
@@ -4236,6 +4342,25 @@ def save(self, *args, **kwargs):
self.slug = slugify(self.name)
super().save(*args, **kwargs)
+# method for updating
+@receiver(pre_save, sender=HackathonEvent, dispatch_uid="psave_hackathonevent")
+def psave_hackathonevent(sender, instance, **kwargs):
+
+ from django.contrib.contenttypes.models import ContentType
+ from search.models import SearchResult
+ if instance.pk:
+ SearchResult.objects.update_or_create(
+ source_type=ContentType.objects.get(app_label='dashboard', model='hackathonevent'),
+ source_id=instance.pk,
+ defaults={
+ "created_on":instance.created_on,
+ "title":instance.name,
+ "description":instance.stats['range'],
+ "url":instance.onboard_url,
+ "visible_to":None,
+ 'img_url': instance.logo.url if instance.logo else None,
+ }
+ )
class HackathonSponsor(SuperModel):
SPONSOR_TYPES = [
@@ -4480,6 +4605,7 @@ class TribeMember(SuperModel):
profile = models.ForeignKey('dashboard.Profile', related_name='follower', on_delete=models.CASCADE)
org = models.ForeignKey('dashboard.Profile', related_name='org', on_delete=models.CASCADE)
leader = models.BooleanField(default=False, help_text=_('tribe leader'))
+ title = models.CharField(max_length=255, blank=True, default='')
status = models.CharField(
max_length=20,
choices=MEMBER_STATUS,
diff --git a/app/dashboard/templates/addinterest.html b/app/dashboard/templates/addinterest.html
index 44d9615788b..f022f5bf3c4 100644
--- a/app/dashboard/templates/addinterest.html
+++ b/app/dashboard/templates/addinterest.html
@@ -73,7 +73,7 @@