From b3d0400e98c059482b03ff1da1b583b4e1d6cca0 Mon Sep 17 00:00:00 2001 From: Durim Morina Date: Tue, 9 Jan 2018 13:41:58 -0800 Subject: [PATCH 1/4] added backend for web hooks --- .../migrations/0014_auto_20180109_2115.py | 38 +++++++++++++++ .../migrations/0015_webhook_owner.py | 23 +++++++++ crowdsourcing/models.py | 47 +++++++++++++++++++ crowdsourcing/serializers/webhooks.py | 13 +++++ crowdsourcing/viewsets/webhooks.py | 46 ++++++++++++++++++ csp/urls.py | 2 + 6 files changed, 169 insertions(+) create mode 100644 crowdsourcing/migrations/0014_auto_20180109_2115.py create mode 100644 crowdsourcing/migrations/0015_webhook_owner.py create mode 100644 crowdsourcing/serializers/webhooks.py create mode 100644 crowdsourcing/viewsets/webhooks.py diff --git a/crowdsourcing/migrations/0014_auto_20180109_2115.py b/crowdsourcing/migrations/0014_auto_20180109_2115.py new file mode 100644 index 000000000..5da3f3533 --- /dev/null +++ b/crowdsourcing/migrations/0014_auto_20180109_2115.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.3 on 2018-01-09 21:15 +from __future__ import unicode_literals + +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('crowdsourcing', '0013_auto_20171212_0049'), + ] + + operations = [ + migrations.CreateModel( + name='WebHook', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('name', models.CharField(blank=True, max_length=128, null=True)), + ('url', models.CharField(max_length=256)), + ('content_type', models.CharField(default=b'application/json', max_length=64)), + ('event', models.CharField(db_index=True, max_length=16)), + ('object', models.CharField(blank=True, db_index=True, max_length=16, null=True)), + ('payload', django.contrib.postgres.fields.jsonb.JSONField(default={}, null=True)), + ('filters', django.contrib.postgres.fields.jsonb.JSONField(default={}, null=True)), + ('retry_count', models.SmallIntegerField(default=1)), + ('is_active', models.BooleanField(default=True)), + ('secret', models.CharField(blank=True, max_length=128, null=True)), + ], + ), + migrations.AlterIndexTogether( + name='webhook', + index_together=set([('event', 'object')]), + ), + ] diff --git a/crowdsourcing/migrations/0015_webhook_owner.py b/crowdsourcing/migrations/0015_webhook_owner.py new file mode 100644 index 000000000..e2255ffb9 --- /dev/null +++ b/crowdsourcing/migrations/0015_webhook_owner.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.3 on 2018-01-09 21:35 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('crowdsourcing', '0014_auto_20180109_2115'), + ] + + operations = [ + migrations.AddField( + model_name='webhook', + name='owner', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='web_hooks', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/crowdsourcing/models.py b/crowdsourcing/models.py index 29a8ef78e..38af5122e 100644 --- a/crowdsourcing/models.py +++ b/crowdsourcing/models.py @@ -981,3 +981,50 @@ class WorkerBonus(TimeStampable): class ProjectPreview(TimeStampable): project = models.ForeignKey('Project') user = models.ForeignKey(User) + + +class WebHook(TimeStampable): + SPEC = [ + { + "event": "completed", + "object": "project", + "description": "Project picked", + "fields": ["project_id", "project_hash_id", "project_name"], + "is_active": True + }, + { + "event": "submitted", + "object": "assignment", + "description": "Assignment submitted", + "fields": ["assignment_id", "task_id", "project_id", "worker_handle"], + "is_active": True + }, + { + "event": "accepted", + "object": "assignment", + "description": "Assignment accepted", + "fields": ["assignment_id", "task_id", "project_id", "worker_handle"], + "is_active": False + }, + { + "event": "skipped", + "object": "assignment", + "description": "Assignment skipped", + "fields": ["assignment_id", "task_id", "project_id", "worker_handle"], + "is_active": False + }, + ] + name = models.CharField(max_length=128, null=True, blank=True) + url = models.CharField(max_length=256) + content_type = models.CharField(max_length=64, default='application/json') + event = models.CharField(max_length=16, db_index=True) + object = models.CharField(max_length=16, null=True, blank=True, db_index=True) + payload = JSONField(null=True, default={}) + filters = JSONField(null=True, default={}) + retry_count = models.SmallIntegerField(default=1) + is_active = models.BooleanField(default=True) + secret = models.CharField(max_length=128, null=True, blank=True) + owner = models.ForeignKey(User, related_name='web_hooks', null=True) + + class Meta: + index_together = (('event', 'object'),) diff --git a/crowdsourcing/serializers/webhooks.py b/crowdsourcing/serializers/webhooks.py new file mode 100644 index 000000000..2951b2ebf --- /dev/null +++ b/crowdsourcing/serializers/webhooks.py @@ -0,0 +1,13 @@ +from crowdsourcing import models +from crowdsourcing.serializers.dynamic import DynamicFieldsModelSerializer + + +class WebHookSerializer(DynamicFieldsModelSerializer): + class Meta: + model = models.WebHook + fields = ('id', 'payload', 'retry_count', 'url', 'event', 'filters', + 'name', 'object', 'content_type', 'secret') + + def create(self, validated_data, owner=None): + hook = self.Meta.model.objects.create(owner=owner, **validated_data) + return hook diff --git a/crowdsourcing/viewsets/webhooks.py b/crowdsourcing/viewsets/webhooks.py new file mode 100644 index 000000000..3fcb1e665 --- /dev/null +++ b/crowdsourcing/viewsets/webhooks.py @@ -0,0 +1,46 @@ +from django.db import transaction +from rest_framework import mixins, viewsets, status +from rest_framework.decorators import list_route +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response + +from crowdsourcing.models import WebHook +from crowdsourcing.permissions.util import IsOwnerOrReadOnly +from crowdsourcing.serializers.webhooks import WebHookSerializer + + +class WebHookViewSet(mixins.CreateModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin, mixins.ListModelMixin, + mixins.RetrieveModelMixin, viewsets.GenericViewSet): + queryset = WebHook.objects.all() + serializer_class = WebHookSerializer + permission_classes = [IsAuthenticated, IsOwnerOrReadOnly] + + @list_route(methods=['get'], url_path='spec') + def spec(self, request, *args, **kwargs): + return Response([s for s in self.queryset.model.SPEC if s['is_active']]) + + def list(self, request, *args, **kwargs): + return Response(self.serializer_class(instance=request.user.web_hooks.all(), many=True).data) + + def create(self, request, *args, **kwargs): + serializer = self.serializer_class(data=request.data) + if serializer.is_valid(): + instance = serializer.create(serializer.validated_data, owner=request.user) + return Response({"id": instance.id}, status=status.HTTP_201_CREATED) + else: + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def update(self, request, *args, **kwargs): + instance = self.get_object() + serializer = self.serializer_class(instance=instance, data=request.data, partial=True) + if serializer.is_valid(): + with transaction.atomic(): + serializer.update(instance, serializer.validated_data) + return Response({"id": instance.id}, status=status.HTTP_200_OK) + else: + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def destroy(self, request, *args, **kwargs): + instance = self.get_object() + instance.delete() + return Response({}, status=status.HTTP_204_NO_CONTENT) diff --git a/csp/urls.py b/csp/urls.py index 46d9ed2c2..63558b285 100644 --- a/csp/urls.py +++ b/csp/urls.py @@ -18,10 +18,12 @@ from crowdsourcing.viewsets.template import TemplateViewSet, TemplateItemViewSet, TemplateItemPropertiesViewSet from crowdsourcing.viewsets.user import UserViewSet, UserProfileViewSet, UserPreferencesViewSet, CountryViewSet, \ CityViewSet +from crowdsourcing.viewsets.webhooks import WebHookViewSet from mturk import views as mturk_views from mturk.viewsets import MTurkAssignmentViewSet, MTurkConfig, MTurkAccountViewSet router = SimpleRouter(trailing_slash=True) +router.register(r'web-hooks', WebHookViewSet) router.register(r'projects', ProjectViewSet) router.register(r'tasks', TaskViewSet) router.register(r'assignments', TaskWorkerViewSet) From 85e4a5f4d21f987356d691c90b99ae15082e0f2e Mon Sep 17 00:00:00 2001 From: Durim Morina Date: Tue, 9 Jan 2018 15:36:38 -0800 Subject: [PATCH 2/4] added web hook processor --- crowdsourcing/tasks.py | 72 +++++++++++++++++++++++++++++++++- crowdsourcing/viewsets/task.py | 5 ++- 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/crowdsourcing/tasks.py b/crowdsourcing/tasks.py index de95376ed..19cdcb55b 100644 --- a/crowdsourcing/tasks.py +++ b/crowdsourcing/tasks.py @@ -1,11 +1,13 @@ from __future__ import division import json +import time from collections import OrderedDict -from datetime import timedelta +from datetime import timedelta, datetime from decimal import Decimal, ROUND_UP import numpy as np +import requests from django.conf import settings from django.contrib.auth.models import User from django.db import connection, transaction @@ -624,10 +626,10 @@ def check_project_completed(project_id): cursor = connection.cursor() cursor.execute(query, params) remaining_count = cursor.fetchall()[0][0] if cursor.rowcount > 0 else 0 - print(remaining_count) if remaining_count == 0: with transaction.atomic(): project = models.Project.objects.select_for_update().get(id=project_id) + on_project_completed(project.id) if project.is_prototype: feedback = project.comments.all() if feedback.count() > 0 and feedback.filter(ready_for_launch=True).count() / feedback.count() < 0.66: @@ -733,3 +735,69 @@ def post_to_discourse(project_id): except Exception as e: print(e) print 'failed to update post' + + +@celery_app.task(ignore_result=True) +def on_assignment_submitted(pk): + return on_assignment_event(pk, "submitted") + + +@celery_app.task(ignore_result=True) +def on_assignment_skipped(pk): + return on_assignment_event(pk, "skipped") + + +@celery_app.task(ignore_result=True) +def on_assignment_accepted(pk): + return on_assignment_event(pk, "accepted") + + +def on_assignment_event(pk, event): + task_worker = models.TaskWorker.objects.prefetch_related('task__project').filter(id=pk).first() + if task_worker is not None: + object_name = "assignment" + hooks = task_worker.task.project.owner.web_hooks.filter(event=event, object=object_name, is_active=True) + data = { + "at": datetime.utcnow().isoformat(), + "worker_handle": task_worker.worker.profile.handle, + "assignment_id": task_worker.id, + "project_id": task_worker.task.project.id + } + for h in hooks: + post_webhook(hook=h, data=data, event=event, object_name=object_name) + + +@celery_app.task(ignore_result=True) +def on_project_completed(pk): + project = models.Project.objects.filter(id=pk).first() + if project is not None: + event = "completed" + object_name = "project" + hooks = project.owner.web_hooks.filter(event=event, object=object_name, is_active=True) + data = { + "at": datetime.utcnow().isoformat(), + "project_id": project.id, + "project_name": project.name + } + for h in hooks: + post_webhook(hook=h, data=data, event=event, object_name=object_name) + return 'SUCCESS' + + +def post_webhook(hook, data, event, object_name, attempt=1): + headers = { + "X-Daemo-Event": "{}.{}".format(object_name, event), + "Content-Type": "application/json" + } + try: + response = requests.post(url=hook.get('url'), + data=json.dumps(data), + headers=headers) + print(response.content) + return 'SUCCESS' + except Exception as e: + print(e) + if attempt < hook.get('retry_count', 1): + time.sleep(1) + post_webhook(hook, data, event, object_name, attempt + 1) + return 'FAILURE' diff --git a/crowdsourcing/viewsets/task.py b/crowdsourcing/viewsets/task.py index b95965298..7428c6287 100644 --- a/crowdsourcing/viewsets/task.py +++ b/crowdsourcing/viewsets/task.py @@ -26,7 +26,8 @@ from crowdsourcing.permissions.util import IsSandbox from crowdsourcing.serializers.project import ProjectSerializer from crowdsourcing.serializers.task import * -from crowdsourcing.tasks import update_worker_cache, refund_task, send_return_notification_email +from crowdsourcing.tasks import update_worker_cache, refund_task, send_return_notification_email, \ + on_assignment_submitted from crowdsourcing.utils import get_model_or_none, hash_as_set, \ get_review_redis_message, hash_task from crowdsourcing.validators.project import validate_account_balance @@ -917,6 +918,7 @@ def submit_results(self, request, mock=False, *args, **kwargs): task_worker.submitted_at = timezone.now() task_worker.save() task_worker.sessions.all().filter(ended_at__isnull=True).update(ended_at=timezone.now()) + on_assignment_submitted.delay(task_worker.id) # check_project_completed.delay(project_id=task_worker.task.project_id) # #send_project_completed_email.delay(project_id=task_worker.task.project_id) if task_status == TaskWorker.STATUS_SUBMITTED: @@ -1057,6 +1059,7 @@ def post(self, request, *args, **kwargs): task_worker_result.result = request.data task_worker_result.save() update_worker_cache.delay([task_worker.worker_id], constants.TASK_SUBMITTED) + on_assignment_submitted.delay(task_worker.id) # check_project_completed.delay(project_id=task_worker.task.project_id) return Response(request.data, status=status.HTTP_200_OK) else: From 2fbe251911aa0f794629459322d910b0e8a31d5f Mon Sep 17 00:00:00 2001 From: Durim Morina Date: Thu, 11 Jan 2018 15:55:24 -0800 Subject: [PATCH 3/4] added webhooks frontend and api docs --- crowdsourcing/serializers/webhooks.py | 2 +- crowdsourcing/tasks.py | 9 +- static/django_templates/index.html | 349 +++++++++--------- static/js/crowdsource.js | 3 +- static/js/crowdsource.routes.js | 32 ++ .../controllers/preferences.controller.js | 22 +- .../controllers/web-hook.controller.js | 71 ++++ .../js/web-hooks/services/web-hook.service.js | 63 ++++ static/js/web-hooks/web-hook.module.js | 14 + static/templates/docs/navigation.html | 1 + static/templates/docs/web-hooks.html | 175 +++++++++ static/templates/project/preferences.html | 213 ++++++----- static/templates/web-hooks/detail.html | 69 ++++ 13 files changed, 760 insertions(+), 263 deletions(-) create mode 100644 static/js/web-hooks/controllers/web-hook.controller.js create mode 100644 static/js/web-hooks/services/web-hook.service.js create mode 100644 static/js/web-hooks/web-hook.module.js create mode 100644 static/templates/docs/web-hooks.html create mode 100644 static/templates/web-hooks/detail.html diff --git a/crowdsourcing/serializers/webhooks.py b/crowdsourcing/serializers/webhooks.py index 2951b2ebf..76608175d 100644 --- a/crowdsourcing/serializers/webhooks.py +++ b/crowdsourcing/serializers/webhooks.py @@ -6,7 +6,7 @@ class WebHookSerializer(DynamicFieldsModelSerializer): class Meta: model = models.WebHook fields = ('id', 'payload', 'retry_count', 'url', 'event', 'filters', - 'name', 'object', 'content_type', 'secret') + 'name', 'object', 'content_type', 'secret', 'is_active') def create(self, validated_data, owner=None): hook = self.Meta.model.objects.create(owner=owner, **validated_data) diff --git a/crowdsourcing/tasks.py b/crowdsourcing/tasks.py index 19cdcb55b..33baafad8 100644 --- a/crowdsourcing/tasks.py +++ b/crowdsourcing/tasks.py @@ -790,14 +790,13 @@ def post_webhook(hook, data, event, object_name, attempt=1): "Content-Type": "application/json" } try: - response = requests.post(url=hook.get('url'), - data=json.dumps(data), - headers=headers) - print(response.content) + requests.post(url=hook.url, + data=json.dumps(data), + headers=headers) return 'SUCCESS' except Exception as e: print(e) - if attempt < hook.get('retry_count', 1): + if attempt < hook.retry_count: time.sleep(1) post_webhook(hook, data, event, object_name, attempt + 1) return 'FAILURE' diff --git a/static/django_templates/index.html b/static/django_templates/index.html index e22722b90..42bc10beb 100644 --- a/static/django_templates/index.html +++ b/static/django_templates/index.html @@ -1,204 +1,211 @@ {% load staticfiles compress %} - - - - - - - Daemo - - - - - - - - - - + + + + + + + Daemo + + + + + + + + + +
-
-
+
+
-
-
- -
+
+
+
+
{% compress js %} - - - - - + + + + + {% endcompress %} - + {% compress js %} - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + {% endcompress %} {% compress js %} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% endcompress %} diff --git a/static/js/crowdsource.js b/static/js/crowdsource.js index 0fe3c608e..8111ef01b 100644 --- a/static/js/crowdsource.js +++ b/static/js/crowdsource.js @@ -35,7 +35,8 @@ angular 'crowdsource.user', 'crowdsource.helpers', 'crowdsource.message', - 'crowdsource.docs' + 'crowdsource.docs', + 'crowdsource.web-hooks' ]); diff --git a/static/js/crowdsource.routes.js b/static/js/crowdsource.routes.js index dbf38b669..80ec94693 100644 --- a/static/js/crowdsource.routes.js +++ b/static/js/crowdsource.routes.js @@ -250,6 +250,12 @@ controllerAs: 'taskfeed' }; + var webHookDetail = { + templateUrl: '/static/templates/web-hooks/detail.html', + controller: 'WebHookController', + controllerAs: 'webhook' + }; + var projectPreview = { templateUrl: '/static/templates/project/preview.html', controller: 'TaskFeedController', @@ -308,6 +314,13 @@ templateUrl: '/static/templates/docs/template-items.html' }; + + var docsWebHooks = { + controller: 'DocsController', + controllerAs: 'docs', + templateUrl: '/static/templates/docs/web-hooks.html' + }; + var docsOAuth2 = { controller: 'DocsController', controllerAs: 'docs', @@ -466,6 +479,13 @@ }, authenticate: false }) + .state('docs.web_hooks', { + url: '/web-hooks', + views: { + 'content': docsWebHooks + }, + authenticate: false + }) .state('docs.oauth2', { url: '/oauth2-authentication', views: { @@ -754,6 +774,18 @@ authenticate: true }) + .state('web_hook', { + url: '/web-hooks/:pk', + title: 'Web Hooks', + views: { + 'navbar': navbar, + 'content': webHookDetail, + 'chat': overlay + }, + authenticate: true + }) + + .state('root', { url: '/', views: { diff --git a/static/js/user/controllers/preferences.controller.js b/static/js/user/controllers/preferences.controller.js index bf5f22d6e..d45ec4af9 100644 --- a/static/js/user/controllers/preferences.controller.js +++ b/static/js/user/controllers/preferences.controller.js @@ -10,12 +10,12 @@ .controller('PreferencesController', PreferencesController); PreferencesController.$inject = ['$state', '$scope', '$window', '$mdToast', 'User', '$filter', - 'Authentication', '$mdDialog']; + 'Authentication', '$mdDialog', 'WebHook']; /** * @namespace PreferencesController */ - function PreferencesController($state, $scope, $window, $mdToast, User, $filter, Authentication, $mdDialog) { + function PreferencesController($state, $scope, $window, $mdToast, User, $filter, Authentication, $mdDialog, WebHook) { var self = this; self.searchTextChange = searchTextChange; self.selectedItemChange = selectedItemChange; @@ -28,7 +28,9 @@ self.black_list_entries = []; self.workerGroups = []; self.groupMembers = []; + self.webHooks = []; self.workers = []; + self.editHook = editHook; self.newWorkerGroup = {}; self.black_list = null; self.selectedGroupMember = null; @@ -37,6 +39,7 @@ self.addWorkerGroup = addWorkerGroup; self.addWorkerToGroup = addWorkerToGroup; self.removeWorkerFromGroup = removeWorkerFromGroup; + self.createWebHook = createWebHook; activate(); $scope.$watch('preferences.workerGroup', function (newValue, oldValue) { @@ -59,6 +62,7 @@ else { listWorkerGroups(); retrieveBlackList(); + listWebHooks(); } } @@ -226,5 +230,19 @@ } ); } + + function createWebHook() { + $state.go('web_hook', {"pk": "new"}); + } + + function listWebHooks() { + WebHook.list().then(function success(response) { + self.webHooks = response[0]; + }); + } + + function editHook(pk) { + $state.go('web_hook', {"pk": pk}); + } } })(); diff --git a/static/js/web-hooks/controllers/web-hook.controller.js b/static/js/web-hooks/controllers/web-hook.controller.js new file mode 100644 index 000000000..0bcfc3c71 --- /dev/null +++ b/static/js/web-hooks/controllers/web-hook.controller.js @@ -0,0 +1,71 @@ +(function () { + 'use strict'; + + angular + .module('crowdsource.web-hooks.controllers') + .controller('WebHookController', WebHookController); + + WebHookController.$inject = ['$scope', '$rootScope', '$state', 'WebHook', '$stateParams', '$mdToast']; + + function WebHookController($scope, $rootScope, $state, WebHook, $stateParams, $mdToast) { + var self = this; + self.create = create; + self.obj = {}; + self.eventObj = null; + activate(); + + function activate() { + var hookId = $stateParams.pk; + if (hookId === 'new') { + self.obj.name = 'Unnamed Webhook'; + self.obj.is_active = true; + self.obj.content_type = 'application/json'; + } + else if (hookId) { + WebHook.retrieve(hookId).then( + function success(data) { + self.obj = data[0]; + self.eventObj = self.obj.object + "." + self.obj.event; + }, + function error(response) { + + }); + } + } + + function create() { + if(!self.eventObj){ + $mdToast.showSimple('Please select an event to subscribe to!'); + return; + } + var eventObjArr = self.eventObj.split('.'); + self.obj.event = eventObjArr[1]; + self.obj.object = eventObjArr[0]; + if(!self.obj.url || !self.obj.name || !self.obj.event || !self.obj.object){ + $mdToast.showSimple('All fields are required!'); + return; + } + if (self.obj.id) { + return update(); + } + WebHook.create(self.obj).then( + function success(data) { + $state.go('requester_settings') + }, + function error(response) { + + }); + } + + function update() { + WebHook.update(self.obj.id, self.obj).then( + function success(data) { + $state.go('requester_settings') + }, + function error(response) { + + }); + } + + } +})(); diff --git a/static/js/web-hooks/services/web-hook.service.js b/static/js/web-hooks/services/web-hook.service.js new file mode 100644 index 000000000..263217f5d --- /dev/null +++ b/static/js/web-hooks/services/web-hook.service.js @@ -0,0 +1,63 @@ +(function () { + 'use strict'; + + angular + .module('crowdsource.web-hooks.services') + .factory('WebHook', WebHook); + + WebHook.$inject = ['$cookies', '$http', '$q', '$sce', 'HttpService']; + + + function WebHook($cookies, $http, $q, $sce, HttpService) { + var baseUrl = HttpService.apiPrefix + '/web-hooks/'; + return { + create: create, + update: update, + delete: deleteInstance, + retrieve: retrieve, + list: list + }; + + function create(data) { + var settings = { + url: baseUrl, + method: 'POST', + data: data + }; + return HttpService.doRequest(settings); + } + + function update(pk, data) { + var settings = { + url: baseUrl + pk + '/', + method: 'PUT', + data: data + }; + return HttpService.doRequest(settings); + } + function deleteInstance(pk) { + var settings = { + url: baseUrl + pk + '/', + method: 'DELETE' + }; + return HttpService.doRequest(settings); + } + + function retrieve(pk) { + var settings = { + url: baseUrl + pk + '/', + method: 'GET' + }; + return HttpService.doRequest(settings); + } + + function list() { + var settings = { + url: baseUrl, + method: 'GET' + }; + return HttpService.doRequest(settings); + } + + } +})(); diff --git a/static/js/web-hooks/web-hook.module.js b/static/js/web-hooks/web-hook.module.js new file mode 100644 index 000000000..696b9c378 --- /dev/null +++ b/static/js/web-hooks/web-hook.module.js @@ -0,0 +1,14 @@ +(function () { + 'use strict'; + + angular + .module('crowdsource.web-hooks', [ + 'crowdsource.web-hooks.controllers', + 'crowdsource.web-hooks.services' + ]); + + angular + .module('crowdsource.web-hooks.controllers', []); + angular + .module('crowdsource.web-hooks.services', []); +})(); diff --git a/static/templates/docs/navigation.html b/static/templates/docs/navigation.html index 4628f9f43..197fff97c 100644 --- a/static/templates/docs/navigation.html +++ b/static/templates/docs/navigation.html @@ -12,6 +12,7 @@ Ratings Qualifications/Filters Payments + Web Hooks
diff --git a/static/templates/docs/web-hooks.html b/static/templates/docs/web-hooks.html new file mode 100644 index 000000000..1e4ea327b --- /dev/null +++ b/static/templates/docs/web-hooks.html @@ -0,0 +1,175 @@ +
+
+
+

Web Hooks

+
Content-Type will always be set to "application/json", additionally a header "X-Daemo-Event" will be set to + the event/object name for identification. +
+

Resource definition

+
+ {{ '{' }}
+
+ "id": integer,
+ "name": string,
+ "url": string,
+ "event": string,
+ "is_active": boolean,
+ "object": string +
+ {{ '}' }} +
+
 
+

Web Hooks: list

+
Lists the web hooks that you own.
+

Request

+
GET /v1/web-hooks/
+

Response

+
+ [
+
WebHook Resource
+ ] +
+ +
 
+

Web Hooks: retrieve

+
Retrieve a web hook by ID.
+

Request

+
GET /v1/web-hooks/webHookId/ +
+

Response

+
+ This method will return a WebHook Resource. +
+
 
+

Web Hooks: create

+
Create a new web hook.
+

Request

+
POST /v1/web-hooks/ +
+

Request Data

+
+ {{ '{' }}
+
+ "name": string,
+ "url": string,
+ "event": string,
+ "object": string,
+ "is_active": boolean
+
+ {{ '}' }} +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Field NameData TypeDescription
namestringA name for this web hook.
urlstringA URL to post the event payload to. +
eventstringEvent to be subscribed to, allowed values are "submitted", "completed". +
objectstringThe object/resource the event is occurring for, allowed values are "project", "assignment". +
is_activebooleanDaemo will post these events as long as this field is set to true. +
+
+

Response

+
{{ '{' }}"id": integer{{ '}' }} +
+
 
+

Web Hooks: destroy

+
Delete a web hook by ID.
+

Request

+
DELETE /v1/web-hooks/webHookId/
+

Response

+
The response for this request will be empty.
+ + +
 
+

Web Hooks: Examples

+
Here's a list of possible configurations.
+

Assignment Submitted

+
+ {{ '{' }}
+
+ "name": "My Sample Web Hook",
+ "url": "http://localhost/notifications/assignments/",
+ "event": "submitted",
+ "object": "assignment",
+ "is_active": true
+
+ {{ '}' }} +
+

Project Completed

+
+ {{ '{' }}
+
+ "name": "My Sample Web Hook #2",
+ "url": "http://localhost/notifications/projects/",
+ "event": "completed",
+ "object": "project",
+ "is_active": true
+
+ {{ '}' }} +
+ +
 
+

Web Hooks: Posted payload format

+
+

Assignment Submitted

+
+ {{ '{' }}
+
+ "at": string,
+ "worker_handle": string,
+ "assignment_id": integer,
+ "project_id": integer +
+ {{ '}' }} +
+

Project Completed

+
+ {{ '{' }}
+
+ "at": string,
+ "project_name": string,
+ "project_id": integer +
+ {{ '}' }} +
+ +
+
+
+
diff --git a/static/templates/project/preferences.html b/static/templates/project/preferences.html index 34c7eff30..334fe3133 100644 --- a/static/templates/project/preferences.html +++ b/static/templates/project/preferences.html @@ -1,108 +1,155 @@
-
+
Requester Settings -
-
+
+
-
-
-
- group -
-
- Worker Groups +
+
+
+ group +
+
+ Worker Groups -
-
+
+
-
-
- - - - - {{ option.name }} - - - -
or - Create a new - group - -
-
-
- - - {{ user.handle }} - - +
+
+ + + + + {{ option.name }} + + + +
or + Create a new + group + +
+
+
+ + + {{ user.handle }} + + {{$chip.handle}} - - + + -
-
+
+
+
+
+
+
+ block
-
-
-
- block -
-
- Block workers -
-
+
+ Block workers +
+
-
Blocked workers will not be able to attempt your tasks in the - future -
-
- - - {{ user.handle }} - - +
Blocked workers will not be able to attempt your tasks in the + future +
+
+ + + {{ user.handle }} + + {{$chip.handle}} - - -
+
+
+
+ +
+ No workers found on this list. +
+ + +
+
+
+
+ http +
+
+ Web Hooks +
+
+ -
- No workers found on this list. + +
+
+
+
+
{{ hook.name }} +
+
{{ hook.url }}
- +
+ + mode_edit + Edit + +
+
+ +
+
+
diff --git a/static/templates/web-hooks/detail.html b/static/templates/web-hooks/detail.html new file mode 100644 index 000000000..6d7e13c11 --- /dev/null +++ b/static/templates/web-hooks/detail.html @@ -0,0 +1,69 @@ +
+
+ + chevron_left + +
+
{{ webhook.obj.id? 'Edit webhook' : 'Create webhook' }} +
+
+ + +
+ + +
+
+ +
+ + +
+ + + + + +
+ +
+
+
+ Select an event you want to subscribe to: +
+ + + Assignment Submitted + + Project Completed + + + +
+
+ + + + +
+
We will send the data as a POST request with Content-Type set to application/json. + More information can be found in our API Docs.
+
+ Active +
+
+ +
+
+ Save + +
+
+ +
+
From 060b014f213480ce3b50c1db4b03073cae2936cf Mon Sep 17 00:00:00 2001 From: Durim Morina Date: Thu, 11 Jan 2018 15:58:30 -0800 Subject: [PATCH 4/4] rm trailing whitespace .models --- crowdsourcing/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crowdsourcing/models.py b/crowdsourcing/models.py index 38af5122e..8e483f814 100644 --- a/crowdsourcing/models.py +++ b/crowdsourcing/models.py @@ -406,7 +406,7 @@ def filter_by_boomerang(self, worker, sort_by='-boomerang'): ELSE worker_rating + 0.1 * worker_avg_rating END worker_rating FROM get_worker_ratings(%(worker_id)s)) worker_ratings ON worker_ratings.requester_id = ratings.owner_id - AND (worker_ratings.worker_rating >= ratings.min_rating or p.enable_boomerang is FALSE + AND (worker_ratings.worker_rating >= ratings.min_rating or p.enable_boomerang is FALSE or p.owner_id = %(worker_id)s) WHERE coalesce(p.deadline, NOW() + INTERVAL '1 minute') > NOW() AND p.status = 3 AND deleted_at IS NULL AND (requester.is_denied = FALSE OR p.enable_blacklist = FALSE)