diff --git a/static/images/integrations/greenhouse/000.png b/static/images/integrations/greenhouse/000.png new file mode 100644 index 0000000000..3fe5070730 Binary files /dev/null and b/static/images/integrations/greenhouse/000.png differ diff --git a/static/images/integrations/logos/greenhouse.png b/static/images/integrations/logos/greenhouse.png new file mode 100644 index 0000000000..2e87ecdc2f Binary files /dev/null and b/static/images/integrations/logos/greenhouse.png differ diff --git a/zerver/fixtures/greenhouse/greenhouse_candidate_created.json b/zerver/fixtures/greenhouse/greenhouse_candidate_created.json new file mode 100644 index 0000000000..a5a87373ba --- /dev/null +++ b/zerver/fixtures/greenhouse/greenhouse_candidate_created.json @@ -0,0 +1,112 @@ +{ + "action": "new_candidate_application", + "payload": { + "application": { + "id": 265293, + "rejected_at": null, + "prospect": false, + "status": "active", + "applied_at": "2013-03-22T00:00:00Z", + "last_activity_at": "2015-03-18T20:28:09Z", + "source": { + "id": 27, + "public_name": "LinkedIn" + }, + "credited_to": null, + "rejection_reason": null, + "current_stage": { + "id": 678901, + "name": "Application Review", + "interviews": [ + { + "id": 989099, + "name":"Application Review", + "status": "collect_feedback", + "interview_kit": { + "url": "http://app.greenhouse.io/guides/67656/people/265788", + "content": "", + "questions": [] + }, + "interviewers": [] + } + ] + }, + "candidate": { + "id": 265788, + "created_at": "2013-10-04T01:24:48Z", + "first_name": "Hector", + "last_name": "Porter", + "title": null, + "company": null, + "phone_numbers": [ + { + "value": "330-281-8004", + "type": "home" + } + ], + "email_addresses": [ + { + "value": "hector.porter.265788@example.com", + "type": "personal" + } + ], + "addresses": [], + "website_addresses": [], + "social_media_addresses": [], + "recruiter": null, + "coordinator": null, + "photo_url": "https://prod-heroku.s3.amazonaws.com/...", + "attachments": [ + { + "filename": "resume.pdf", + "url": "https://prod-heroku.s3.amazonaws.com/...", + "type": "resume" + } + ], + "tags": [ + "Import from Previous ATS" + ], + "custom_fields": { + "favorite_color": { + "name": "Favorite Color", + "type": "short_text", + "value": "Blue" + } + } + }, + "jobs": [ + { + "id": 3485, + "name": "Designer", + "requisition_id": null, + "notes": "Digital and print", + "job_post_id": 54321, + "status": "open", + "created_at": "2013-10-02T22:59:29Z", + "opened_at": "2015-01-23T00:25:04Z", + "closed_at": null, + "departments": [ + { + "id": 237, + "name": "Community" + } + ], + "offices": [ + { + "id": 54, + "name": "New York", + "location": "New York, NY" + } + ], + "custom_fields": { + "employment_type": { + "name": "Employment Type", + "type": "single_select", + "value": "Full Time" + } + } + } + ] + } + } +} diff --git a/zerver/fixtures/greenhouse/greenhouse_candidate_hired.json b/zerver/fixtures/greenhouse/greenhouse_candidate_hired.json new file mode 100644 index 0000000000..9827d72d73 --- /dev/null +++ b/zerver/fixtures/greenhouse/greenhouse_candidate_hired.json @@ -0,0 +1,184 @@ +{ + "action": "hire_candidate", + "payload": { + "application": { + "id": 20, + "opening": { + "opening_id": "1234-56" + }, + "credited_to": { + "id": 57, + "email": "bob_johnson1@localhost.com", + "name": "Robert Johnson" + }, + "source": { + "id": 25, + "public_name": "Monster" + }, + "candidate": { + "id": 19, + "first_name": "Johnny", + "last_name": "Smith", + "title": "Previous Title", + "external_id": "00000", + "phone_numbers": [{ + "value": "518-555-1212", + "type": "work" + }, { + "value": "212-555-1212", + "type": "home" + }], + "email_addresses": [{ + "value": "personal@example.com", + "type": "personal" + }, { + "value": "work@example.com", + "type": "work" + }], + "addresses": [{ + "value": "455 Broadway New York, NY 10280", + "type": "home" + }], + "recruiter": { + "id": 55, + "email": "bob_johnson@localhost.com", + "name": "Bob Johnson" + }, + "coordinator": { + "id": 56, + "email": "bob_johnson_approver1@localhost.com", + "name": "Robert J Approver" + }, + "attachments": [{ + "filename": "resume.pdf", + "url": "https://prod-heroku.s3.amazonaws.com/...", + "type": "resume" + }], + "custom_fields": { + "desired_level": { + "name": "Desired Level", + "type": "short_text", + "value": "Senior" + }, + "favorite_programming_language": { + "name": "Favorite Programming Language", + "type": "short_text", + "value": "Rails" + } + } + }, + "job": { + "id": 20, + "name": "Developer", + "open_date": "2014-11-20T22:49:14Z", + "close_date": "2014-11-25T22:49:14Z", + "requisition_id": null, + "departments": [{ + "id": 7, + "name": "Technology" + }], + "offices": [{ + "id": 13, + "name": "New York City", + "location": { + "name": "New York, NY" + } + }, { + "id": 14, + "name": "St. Louis", + "location": null + }], + "custom_fields": { + "approved": { + "name": "Approved", + "type": "boolean", + "value": true + }, + "employment_type": { + "name": "Employment Type", + "type": "single_select", + "value": "Full-time" + }, + "salary_range": { + "name": "SalaryRange", + "type": "currency_range", + "value": { + "unit": "USD", + "min_value": "10000.0", + "max_value": "20000.0" + } + } + } + }, + "jobs": [{ + "id": 20, + "name": "Developer", + "opened_at": "2014-11-20T22:49:14Z", + "closed_at": "2014-11-25T22:49:14Z", + "requisition_id": null, + "departments": [{ + "id": 7, + "name": "Technology" + }], + "offices": [{ + "id": 13, + "name": "New York City", + "location": { + "name": "New York, NY" + } + }, { + "id": 14, + "name": "St. Louis", + "location": null + }], + "custom_fields": { + "approved": { + "name": "Approved", + "type": "boolean", + "value": true + }, + "employment_type": { + "name": "Employment Type", + "type": "single_select", + "value": "Full-time" + }, + "salary_range": { + "name": "SalaryRange", + "type": "currency_range", + "value": { + "unit": "USD", + "min_value": "10000.0", + "max_value": "20000.0" + } + } + } + }], + "offer": { + "id": 13, + "version": 2, + "created_at": "2014-11-20T22:49:14Z", + "sent_at": "2014-11-10", + "resolved_at": "2014-11-20T22:49:14Z", + "starts_at": "2015-01-23", + "custom_fields": { + "salary": { + "name": "Salary", + "type": "currency", + "value": { + "amount": 80000, + "unit": "USD" + } + }, + "seasons": { + "name": "Seasons", + "type": "multi_select", + "value": [ + "Season 1", + "Season 2" + ] + } + } + } + } + } +} diff --git a/zerver/fixtures/greenhouse/greenhouse_candidate_rejected.json b/zerver/fixtures/greenhouse/greenhouse_candidate_rejected.json new file mode 100644 index 0000000000..2e45451447 --- /dev/null +++ b/zerver/fixtures/greenhouse/greenhouse_candidate_rejected.json @@ -0,0 +1,90 @@ +{ + "action": "reject_candidate", + "payload": { + "application": { + "id": 265293, + "rejected_at": "2015-02-11T15:50:41Z", + "prospect": false, + "status": "rejected", + "applied_at": "2013-03-22T00:00:00Z", + "last_activity_at": "2015-02-11T15:50:41Z", + "source": { + "id": 27, + "public_name": "LinkedIn" + }, + "credited_to": null, + "rejection_reason": { + "id": 14, + "name": "Too Junior", + "type": { + "id": 3, + "name": "Wrong skill set" + } + }, + "candidate": { + "id": 265788, + "created_at": "2013-10-04T01:24:48Z", + "first_name": "Hector", + "last_name": "Porter", + "title": null, + "company": null, + "phone_numbers": [{ + "value": "330-281-8004", + "type": "home" + }], + "email_addresses": [{ + "value": "hector.porter.265788@example.com", + "type": "personal" + }], + "addresses": [], + "website_addresses": [], + "social_media_addresses": [], + "recruiter": null, + "coordinator": null, + "photo_url": "www.example.com/photo.png", + "attachments": [{ + "filename": "resume.pdf", + "url": "https://prod-heroku.s3.amazonaws.com/...", + "type": "resume" + }], + "tags": [ + "Imported" + ], + "custom_fields": { + "favorite_color": { + "name": "Favorite Color", + "type": "short_text", + "value": "Blue" + } + } + }, + "jobs": [{ + "id": 3485, + "name": "Designer", + "requisition_id": null, + "notes": "Digital and print", + "job_post_id": 54321, + "status": "open", + "created_at": "2013-10-02T22:59:29Z", + "opened_at": "2015-01-23T00:25:04Z", + "closed_at": null, + "departments": [{ + "id": 237, + "name": "Community" + }], + "offices": [{ + "id": 54, + "name": "New York", + "location": "New York, NY" + }], + "custom_fields": { + "employment_type": { + "name": "Employment Type", + "type": "single_select", + "value": "Full Time" + } + } + }] + } + } +} diff --git a/zerver/fixtures/greenhouse/greenhouse_candidate_stage_change.json b/zerver/fixtures/greenhouse/greenhouse_candidate_stage_change.json new file mode 100644 index 0000000000..14f243d3d6 --- /dev/null +++ b/zerver/fixtures/greenhouse/greenhouse_candidate_stage_change.json @@ -0,0 +1,130 @@ +{ + "action": "candidate_stage_change", + "payload": { + "application": { + "id": 265277, + "rejected_at": null, + "prospect": false, + "status": "active", + "applied_at": "2013-03-22T00:00:00Z", + "last_activity_at": "2015-02-09T16:38:36Z", + "source": { + "id": 31, + "name": "Agency" + }, + "credited_to": { + "id": 15, + "email": "ada@example.com", + "name": "Ada Lacey" + }, + "rejection_reason": null, + "current_stage": { + "id": 71416, + "name": "Assessment", + "interviews": [{ + "id": 113101, + "name": "Assessment", + "status": "to_be_scheduled", + "interview_kit": { + "url": "https://app.greenhouse.io/guides/113153/people/265772", + "content": "Assess their skills", + "questions": [] + }, + "interviewers": [{ + "id": 2622, + "display_name": "Carl Buddha", + "status": "tentative" + }] + }] + }, + "candidate": { + "id": 265772, + "created_at": "2013-10-04T01:24:44Z", + "first_name": "Giuseppe", + "last_name": "Hurley", + "external_id": "241b399ce4b0fd1c84e5528d", + "title": "Great Person", + "company": null, + "photo_url": "https://prod-heroku.s3.amazonaws.com/...", + "phone_numbers": [{ + "value": "330-281-8004", + "type": "home" + }], + "email_addresses": [{ + "value": "giuseppe.hurley@example.com", + "type": "personal" + }], + "addresses": [{ + "value": "123 Fake St.", + "type": "home" + }], + "website_addresses": [{ + "value": "ghurley.example.com", + "type": "personal" + }], + "social_media_addresses": [{ + "value": "linkedin.example.com/ghurley" + }], + "recruiter": { + "id": 3128, + "email": "alicia.flopple.3128@example.com", + "name": "Alicia Flopple" + }, + "coordinator": { + "id": 3128, + "email": "alicia.flopple.3128@example.com", + "name": "Alicia Flopple" + }, + "attachments": [{ + "filename": "resume.pdf", + "url": "https://prod-heroku.s3.amazonaws.com/...", + "type": "resume" + }, { + "filename": "cover_letter.pdf", + "url": "https://prod-heroku.s3.amazonaws.com/...", + "type": "cover_letter" + }, { + "filename": "portfolio.pdf", + "url": "https://prod-heroku.s3.amazonaws.com/...", + "type": "attachment" + }], + "tags": [ + "Import from Previous ATS" + ], + "custom_fields": { + "favorite_color": { + "name": "Favorite Color", + "type": "short_text", + "value": "Blue" + } + } + }, + "jobs": [{ + "id": 3485, + "name": "Designer", + "requisition_id": null, + "notes": "Digital and print", + "status": "open", + "created_at": "2013-10-02T22:59:29Z", + "opened_at": "2015-01-23T00:25:04Z", + "closed_at": null, + "departments": [{ + "id": 237, + "name": "Community" + }], + "offices": [{ + "id": 54, + "name": "New York", + "location": "New York, NY" + }], + "custom_fields": { + "employment_type": { + "name": "Employment Type", + "type": "single_select", + "value": "Full Time" + } + } + }] + } + } +} diff --git a/zerver/fixtures/greenhouse/greenhouse_prospect_created.json b/zerver/fixtures/greenhouse/greenhouse_prospect_created.json new file mode 100644 index 0000000000..eaedc82a31 --- /dev/null +++ b/zerver/fixtures/greenhouse/greenhouse_prospect_created.json @@ -0,0 +1,104 @@ +{ + "action": "new_prospect_application", + "payload": { + "application": { + "id": 979554, + "rejected_at": null, + "prospect": true, + "status": "active", + "applied_at": "2014-12-02T23:10:16Z", + "last_activity_at": "2014-12-02T23:10:16Z", + "current_stage": null, + "source": { + "id": 13, + "public_name": "Referral" + }, + "credited_to": { + "id": 2622, + "email": "carl.buddha.2622@example.com", + "name": "Carl Buddha" + }, + "rejection_reason": null, + "candidate": { + "id": 968190, + "created_at": "2014-12-02T23:10:16Z", + "first_name": "Trisha", + "last_name": "Troy", + "title": null, + "company": null, + "phone_numbers": [ + { + "value": "123456", + "type": "other" + } + ], + "email_addresses": [ + { + "value": "t.troy@example.com", + "type": "personal" + } + ], + "addresses": [], + "website_addresses": [], + "social_media_addresses": [], + "recruiter": { + "id": 3128, + "email": "alicia.flopple.3128@example.com", + "name": "Alicia Flopple" + }, + "coordinator": null, + "photo_url": "https://prod-heroku.s3.amazonaws.com/...", + "attachments": [ + { + "filename": "resume.pdf", + "url": "https://prod-heroku.s3.amazonaws.com/...", + "type": "resume" + } + ], + "tags": [ + "Import from Previous ATS" + ], + "custom_fields": { + "favorite_color": { + "name": "Favorite Color", + "type": "short_text", + "value": "Blue" + } + } + }, + "jobs": [ + { + "id": 3485, + "name": "Designer", + "requisition_id": null, + "notes": "Digital and print", + "job_post_id": 54321, + "status": "open", + "created_at": "2013-10-02T22:59:29Z", + "opened_at": "2015-01-23T00:25:04Z", + "closed_at": null, + "departments": [ + { + "id": 237, + "name": "Community" + } + ], + "offices": [ + { + "id": 54, + "name": "New York", + "location": "New York, NY" + } + ], + "custom_fields": { + "employment_type": { + "name": "Employment Type", + "type": "single_select", + "value": "Full Time" + } + } + } + ] + } + } +} diff --git a/zerver/lib/integrations.py b/zerver/lib/integrations.py index e6efc8af58..0715ecd097 100644 --- a/zerver/lib/integrations.py +++ b/zerver/lib/integrations.py @@ -133,6 +133,7 @@ WEBHOOK_INTEGRATIONS = [ ), WebhookIntegration('gitlab', display_name='GitLab'), WebhookIntegration('gosquared', display_name='GoSquared'), + WebhookIntegration('greenhouse', display_name='Greenhouse'), WebhookIntegration('hellosign', display_name='HelloSign'), WebhookIntegration('helloworld', display_name='Hello World'), WebhookIntegration('heroku', display_name='Heroku'), diff --git a/zerver/webhooks/greenhouse/__init__.py b/zerver/webhooks/greenhouse/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/zerver/webhooks/greenhouse/doc.html b/zerver/webhooks/greenhouse/doc.html new file mode 100644 index 0000000000..8735ba1137 --- /dev/null +++ b/zerver/webhooks/greenhouse/doc.html @@ -0,0 +1,32 @@ +

+ First, create a stream where you would like to receive Greenhouse + notifications and subscribe all interested parties to the + stream. The integration will automatically use the default + stream greenhouse if no stream is supplied, though you + will still need to create the stream manually even though it's the + default. +

+ +

+ Next, go to the Zulip settings page and create a bot named + Greenhouse. Go to the account settings page of your Greenhouse + account and under Webhooks, add the below URL and name the + integration, Zulip. +

+ +

{{ external_api_uri_subdomain }}/v1/external/greenhouse?api_key=abcdefgh&stream=greenhouse

+ +

+ Note: api_key must be reconfigured to be the API key of your Zulip bot.
+ If you want to change the stream that receives notifications, change stream= in the URL.
+ To change the topic displayed by the bot, simply append &topic=name to the end of the above URL. + Where name is your topic. +

+ +

+ Congratulations! You're all set
+ Your messages should look like this: +

+ +

+ diff --git a/zerver/webhooks/greenhouse/tests.py b/zerver/webhooks/greenhouse/tests.py new file mode 100644 index 0000000000..914c21361e --- /dev/null +++ b/zerver/webhooks/greenhouse/tests.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +from typing import Text +from zerver.lib.test_classes import WebhookTestCase + +class GreenhouseHookTests(WebhookTestCase): + STREAM_NAME = 'greenhouse' + URL_TEMPLATE = "/api/v1/external/greenhouse?stream={stream}&api_key={api_key}" + FIXTURE_DIR_NAME = 'greenhouse' + CONTENT_TYPE = "application/x-www-form-urlencoded" + + def test_message_candidate_hired(self): + # type: () -> None + expected_subject = "Hire Candidate - 19" + expected_message = ("Hire Candidate\n>Johnny Smith\nID: 19" + "\nApplying for role:\nDeveloper\n**Emails:**" + "\nPersonal\npersonal@example.com\nWork\nwork@example.com\n\n\n>" + "**Attachments:**\n[Resume](https://prod-heroku.s3.amazonaws.com/...)\n") + + self.send_and_test_stream_message('candidate_hired', + expected_subject, + expected_message, + content_type=self.CONTENT_TYPE) + + def test_message_candidate_rejected(self): + # type: () -> None + expected_subject = "Reject Candidate - 265788" + expected_message = ("Reject Candidate\n>Hector Porter\nID: " + "265788\nApplying for role:\nDesigner" + "\n**Emails:**\nPersonal\n" + "hector.porter.265788@example.com\n\n\n>" + "**Attachments:**\n[Resume](https://prod-heroku.s3.amazonaws.com/...)\n") + + self.send_and_test_stream_message('candidate_rejected', + expected_subject, + expected_message, + content_type=self.CONTENT_TYPE) + + def test_message_candidate_stage_change(self): + # type: () -> None + expected_subject = "Candidate Stage Change - 265772" + expected_message = ("Candidate Stage Change\n>Giuseppe Hurley" + "\nID: 265772\nApplying for role:\n" + "Designer\n**Emails:**\nPersonal" + "\ngiuseppe.hurley@example.com\n\n\n>" + "**Attachments:**\n[Resume](https://prod-heroku.s3.amazonaws.com/...)" + "\n[Cover_Letter](https://prod-heroku.s3.amazonaws.com/...)" + "\n[Attachment](https://prod-heroku.s3.amazonaws.com/...)\n") + + self.send_and_test_stream_message('candidate_stage_change', + expected_subject, + expected_message, + content_type=self.CONTENT_TYPE) + + def test_message_prospect_created(self): + # type: () -> None + expected_subject = "New Prospect Application - 968190" + expected_message = ("New Prospect Application\n>Trisha Troy" + "\nID: 968190\nApplying for role:\n" + "Designer\n**Emails:**\nPersonal" + "\nt.troy@example.com\n\n\n>**Attachments:**" + "\n[Resume](https://prod-heroku.s3.amazonaws.com/...)\n") + + self.send_and_test_stream_message('prospect_created', + expected_subject, + expected_message, + content_type=self.CONTENT_TYPE) + + def get_body(self, fixture_name): + # type: (Text) -> Text + return self.fixture_data("greenhouse", fixture_name, file_type="json") diff --git a/zerver/webhooks/greenhouse/view.py b/zerver/webhooks/greenhouse/view.py new file mode 100644 index 0000000000..ee2aa63c36 --- /dev/null +++ b/zerver/webhooks/greenhouse/view.py @@ -0,0 +1,64 @@ +from __future__ import absolute_import + +from django.utils.translation import ugettext as _ +from django.http import HttpRequest, HttpResponse +from typing import Any + +from zerver.lib.actions import check_send_message +from zerver.lib.response import json_success, json_error +from zerver.decorator import REQ, has_request_variables, api_key_only_webhook_view +from zerver.models import UserProfile, Client + +import ujson + +MESSAGE_TEMPLATE = "Applying for role:\n{}\n**Emails:**\n{}\n\n>**Attachments:**\n{}" + +def dict_list_to_string(some_list): + # type: (List[Any]) -> str + internal_template = '' + for item in some_list: + item_type = item.get('type', '').title() + item_value = item.get('value') + item_url = item.get('url') + if item_type and item_value: + internal_template += "{}\n{}\n".format(item_type, item_value) + elif item_type and item_url: + internal_template += "[{}]({})\n".format(item_type, item_url) + return internal_template + +def message_creator(action, application): + # type: (str, Dict[str, Any]) -> str + message = MESSAGE_TEMPLATE.format( + application['jobs'][0]['name'], + dict_list_to_string(application['candidate']['email_addresses']), + dict_list_to_string(application['candidate']['attachments'])) + return message + +@api_key_only_webhook_view('Greenhouse') +@has_request_variables +def api_greenhouse_webhook(request, user_profile, client, + payload=REQ(argument_type='body'), + stream=REQ(default='greenhouse'), topic=REQ(default=None)): + # type: (HttpRequest, UserProfile, Client, Dict[str, Any], str, str) -> HttpResponse + try: + if payload['action'] == 'update_candidate': + candidate = payload['payload']['candidate'] + else: + candidate = payload['payload']['application']['candidate'] + action = payload['action'].replace('_', ' ').title() + body = "{}\n>{} {}\nID: {}\n{}".format( + action, + candidate['first_name'], + candidate['last_name'], + str(candidate['id']), + message_creator(payload['action'], + payload['payload']['application'])) + + if topic is None: + topic = "{} - {}".format(action, str(candidate['id'])) + + except KeyError as e: + return json_error(_("Missing key {} in JSON").format(str(e))) + + check_send_message(user_profile, client, 'stream', [stream], topic, body) + return json_success()