From d683dc250d6f79d6c09cbbe6cc63f12eb5e5a97c Mon Sep 17 00:00:00 2001 From: Anupam K Date: Sat, 18 Apr 2020 00:45:18 +0530 Subject: [PATCH 1/5] Linkedin, Twitter Integration --- .../crm/doctype/linkedin_settings/__init__.py | 0 .../linkedin_settings/linkedin_settings.js | 66 ++++++++ .../linkedin_settings/linkedin_settings.json | 111 +++++++++++++ .../linkedin_settings/linkedin_settings.py | 157 ++++++++++++++++++ .../test_linkedin_settings.py | 10 ++ .../crm/doctype/social_media_post/__init__.py | 0 .../social_media_post/social_media_post.js | 54 ++++++ .../social_media_post/social_media_post.json | 157 ++++++++++++++++++ .../social_media_post/social_media_post.py | 49 ++++++ .../social_media_post_list.js | 10 ++ .../test_social_media_post.py | 10 ++ .../crm/doctype/twitter_settings/__init__.py | 0 .../twitter_settings/test_twitter_settings.py | 10 ++ .../twitter_settings/twitter_settings.js | 52 ++++++ .../twitter_settings/twitter_settings.json | 101 +++++++++++ .../twitter_settings/twitter_settings.py | 90 ++++++++++ erpnext/hooks.py | 3 +- .../doctype/campaign/campaign_dashboard.py | 6 +- requirements.txt | 1 + 19 files changed, 885 insertions(+), 2 deletions(-) create mode 100644 erpnext/crm/doctype/linkedin_settings/__init__.py create mode 100644 erpnext/crm/doctype/linkedin_settings/linkedin_settings.js create mode 100644 erpnext/crm/doctype/linkedin_settings/linkedin_settings.json create mode 100644 erpnext/crm/doctype/linkedin_settings/linkedin_settings.py create mode 100644 erpnext/crm/doctype/linkedin_settings/test_linkedin_settings.py create mode 100644 erpnext/crm/doctype/social_media_post/__init__.py create mode 100644 erpnext/crm/doctype/social_media_post/social_media_post.js create mode 100644 erpnext/crm/doctype/social_media_post/social_media_post.json create mode 100644 erpnext/crm/doctype/social_media_post/social_media_post.py create mode 100644 erpnext/crm/doctype/social_media_post/social_media_post_list.js create mode 100644 erpnext/crm/doctype/social_media_post/test_social_media_post.py create mode 100644 erpnext/crm/doctype/twitter_settings/__init__.py create mode 100644 erpnext/crm/doctype/twitter_settings/test_twitter_settings.py create mode 100644 erpnext/crm/doctype/twitter_settings/twitter_settings.js create mode 100644 erpnext/crm/doctype/twitter_settings/twitter_settings.json create mode 100644 erpnext/crm/doctype/twitter_settings/twitter_settings.py diff --git a/erpnext/crm/doctype/linkedin_settings/__init__.py b/erpnext/crm/doctype/linkedin_settings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js new file mode 100644 index 0000000000..fd169f8328 --- /dev/null +++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js @@ -0,0 +1,66 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('LinkedIn Settings', { + onload: function(frm){ + if(frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret){ + frappe.confirm( + 'Session not valid, Do you want to login?', + function(){ + frm.trigger("login"); + }, + function(){ + window.close(); + } + ) + } + }, + refresh: function(frm){ + if(frm.doc.session_status=="Expired"){ + frm.dashboard.set_headline_alert( + '
' + + '
' + + ' ' + + '
' + + '
' + ); + } + if(frm.doc.session_status=="Active"){ + let d = new Date(frm.doc.modified) + d.setDate(d.getDate()+60); + let dn = new Date() + let days = d.getTime() - dn.getTime(); + days = Math.floor(days/(1000 * 3600 * 24)); + let msg,color; + if(days>0){ + msg = "Your Session will be expire in " + days + " days."; + color = "green"; + } + else{ + msg = "Session is expired. Save doc to login."; + color = "red"; + } + frm.dashboard.set_headline_alert( + '
' + + '
' + + ' ' + + '
' + + '
' + ); + } + }, + login: function(frm){ + if(frm.doc.consumer_key && frm.doc.consumer_secret){ + frappe.call({ + doc: frm.doc, + method: "get_authorization_url", + callback : function(r) { + window.location.href = r.message; + } + }); + } + }, + after_save: function(frm){ + frm.trigger("login"); + } +}); diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.json b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.json new file mode 100644 index 0000000000..9eacb0011c --- /dev/null +++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.json @@ -0,0 +1,111 @@ +{ + "actions": [], + "creation": "2020-01-30 13:36:39.492931", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "account_name", + "column_break_2", + "company_id", + "oauth_details", + "consumer_key", + "column_break_5", + "consumer_secret", + "user_details_section", + "access_token", + "person_urn", + "session_status" + ], + "fields": [ + { + "fieldname": "account_name", + "fieldtype": "Data", + "label": "Account Name", + "read_only": 1 + }, + { + "fieldname": "oauth_details", + "fieldtype": "Section Break", + "label": "OAuth Credentials" + }, + { + "fieldname": "consumer_key", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Consumer Key", + "reqd": 1 + }, + { + "fieldname": "consumer_secret", + "fieldtype": "Password", + "in_list_view": 1, + "label": "Consumer Secret", + "reqd": 1 + }, + { + "fieldname": "access_token", + "fieldtype": "Data", + "hidden": 1, + "label": "Access Token", + "read_only": 1 + }, + { + "fieldname": "person_urn", + "fieldtype": "Data", + "hidden": 1, + "label": "Person URN", + "read_only": 1 + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "fieldname": "user_details_section", + "fieldtype": "Section Break", + "label": "User Details" + }, + { + "fieldname": "session_status", + "fieldtype": "Select", + "hidden": 1, + "label": "Session Status", + "options": "Expired\nActive", + "read_only": 1 + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "fieldname": "company_id", + "fieldtype": "Data", + "label": "Company ID", + "reqd": 1 + } + ], + "issingle": 1, + "links": [], + "modified": "2020-04-16 23:22:51.966397", + "modified_by": "Administrator", + "module": "CRM", + "name": "LinkedIn Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py new file mode 100644 index 0000000000..9326e6d046 --- /dev/null +++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py @@ -0,0 +1,157 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe, requests, json +from frappe import _ +from frappe.utils import get_site_url, get_url_to_form, get_link_to_form +from frappe.model.document import Document +from frappe.utils.file_manager import get_file, get_file_path +from six.moves.urllib.parse import urlencode +class LinkedInSettings(Document): + def get_authorization_url(self): + params = urlencode({ + "response_type":"code", + "client_id": self.consumer_key, + "redirect_uri": get_site_url(frappe.local.site) + "/?cmd=erpnext.crm.doctype.linkedin_settings.linkedin_settings.callback", + "scope": "r_emailaddress w_organization_social r_basicprofile r_liteprofile r_organization_social rw_organization_admin w_member_social" + }) + + url = "https://www.linkedin.com/oauth/v2/authorization?{}".format(params) + + return url + + def get_access_token(self, code): + url = "https://www.linkedin.com/oauth/v2/accessToken" + body = { + "grant_type": "authorization_code", + "code": code, + "client_id": self.consumer_key, + "client_secret": self.get_password(fieldname="consumer_secret"), + "redirect_uri": get_site_url(frappe.local.site) + "/?cmd=erpnext.crm.doctype.linkedin_settings.linkedin_settings.callback", + } + headers = { + "Content-Type": "application/x-www-form-urlencoded" + } + + response = self.http_post(url=url, data=body, headers=headers) + response = frappe.parse_json(response.content.decode()) + self.db_set("access_token", response["access_token"]) + + def get_member_profile(self): + headers = { + "Authorization": "Bearer {}".format(self.access_token) + } + url = "https://api.linkedin.com/v2/me" + response = requests.get(url=url, headers=headers) + response = frappe.parse_json(response.content.decode()) + self.db_set("person_urn", response["id"]) + self.db_set("account_name", response["vanityName"]) + self.db_set("session_status", "Active") + frappe.local.response["type"] = "redirect" + frappe.local.response["location"] = get_url_to_form("LinkedIn Settings","LinkedIn Settings") + + def post(self, text, media=None): + if not media: + return self.post_text(text) + else: + media_id = self.upload_image(media) + if media_id: + return self.post_text(text, media_id=media_id) + else: + frappe.log_error("Failed to upload media.","LinkedIn Upload Error") + + + def upload_image(self, media): + media = get_file_path(media) + register_url = "https://api.linkedin.com/v2/assets?action=registerUpload" + + body = { + "registerUploadRequest": { + "recipes": ["urn:li:digitalmediaRecipe:feedshare-image"], + "owner": "urn:li:organization:{0}".format(self.company_id), + "serviceRelationships": [{ + "relationshipType": "OWNER", + "identifier": "urn:li:userGeneratedContent" + }] + } + } + headers = { + "Authorization": "Bearer {}".format(self.access_token) + } + response = self.http_post(url=register_url, body=body, headers=headers) + if response.status_code == 200: + response = response.json() + asset = response["value"]["asset"] + upload_url = response["value"]["uploadMechanism"]["com.linkedin.digitalmedia.uploading.MediaUploadHttpRequest"]["uploadUrl"] + headers['Content-Type']='image/jpeg' + response = self.http_post(upload_url, headers=headers, data=open(media,"rb")) + if response.status_code < 200 and response.status_code > 299: + frappe.throw(_("Error While Uploading Image"), title="{0} {1}".format(response.status_code, response.reason)) + return None + return asset + return None + + + def post_text(self, text, media_id=None): + # url = "https://api.linkedin.com/v2/ugcPosts" + url = "https://api.linkedin.com/v2/shares" + headers = { + "X-Restli-Protocol-Version": "2.0.0", + "Authorization": "Bearer {}".format(self.access_token), + "Content-Type": "application/json; charset=UTF-8" + } + body = { + "distribution": { + "linkedInDistributionTarget": {} + }, + "owner":"urn:li:organization:{0}".format(self.company_id), + "subject": "Test Share Subject", + "text": { + "text": text + } + } + if media_id: + body["content"]= { + "contentEntities": [{ + "entity": media_id + }], + "shareMediaCategory": "IMAGE" + } + response = self.http_post(url=url, headers=headers, body=body) + return response + + def http_post(self, url, headers=None, body=None, data=None): + try: + response = requests.post( + url = url, + json = body, + data = data, + headers = headers + ) + if response.status_code not in [201,200]: + raise + except Exception as e: + content = json.loads(response.content) + if response.status_code == 401: + self.db_set("session_status", "Expired") + frappe.db.commit() + frappe.throw(content["message"], title="LinkedIn Error - Unauthorized") + elif response.status_code == 403: + frappe.msgprint(_("You Didn't have permission to access this API")) + frappe.throw(content["message"], title="LinkedIn Error - Access Denied") + else: + frappe.throw(response.reason, title=response.status_code) + return response + +@frappe.whitelist() +def callback(code=None, error=None, error_description=None): + if not error: + linkedin_settings = frappe.get_doc("LinkedIn Settings") + linkedin_settings.get_access_token(code) + linkedin_settings.get_member_profile() + frappe.db.commit() + else: + frappe.local.response["type"] = "redirect" + frappe.local.response["location"] = get_url_to_form("LinkedIn Settings","LinkedIn Settings") diff --git a/erpnext/crm/doctype/linkedin_settings/test_linkedin_settings.py b/erpnext/crm/doctype/linkedin_settings/test_linkedin_settings.py new file mode 100644 index 0000000000..9c3ef3f32f --- /dev/null +++ b/erpnext/crm/doctype/linkedin_settings/test_linkedin_settings.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestLinkedInSettings(unittest.TestCase): + pass diff --git a/erpnext/crm/doctype/social_media_post/__init__.py b/erpnext/crm/doctype/social_media_post/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.js b/erpnext/crm/doctype/social_media_post/social_media_post.js new file mode 100644 index 0000000000..c858885d2b --- /dev/null +++ b/erpnext/crm/doctype/social_media_post/social_media_post.js @@ -0,0 +1,54 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +frappe.ui.form.on('Social Media Post', { + validate: function(frm){ + if(frm.doc.text.length > 280){ + frappe.throw("Length Must be less than 280.") + } + }, + refresh: function(frm){ + if(frm.doc.docstatus === 1){ + if(frm.doc.post_status != "Posted"){ + add_post_btn(frm); + } + else if(frm.doc.post_status == "Posted"){ + frm.set_df_property('sheduled_time', 'read_only', 1); + } + let html = '
'; + if(frm.doc.twitter){ + let color = frm.doc.twitter_post_id ? "green" : "red"; + let status = frm.doc.twitter_post_id ? "Posted" : "Not Posted"; + html += '
' + + ' ' + + '
' ; + } + if(frm.doc.linkedin){ + let color = frm.doc.linkedin_post_id ? "green" : "red"; + let status = frm.doc.linkedin_post_id ? "Posted" : "Not Posted"; + html += '
' + + ' ' + + '
' ; + } + html += '
'; + frm.dashboard.set_headline_alert(html); + } + } +}); +var add_post_btn = function(frm){ + frm.add_custom_button(('Post Now'), function(){ + post(frm); + }); +} +var post = function(frm){ + frappe.call({ + method: "erpnext.crm.doctype.social_media_post.social_media_post.publish", + args: { + doctype: frm.doc.doctype, + name: frm.doc.name + }, + callback: function(r) { + frm.reload_doc(); + } + }) + +} \ No newline at end of file diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.json b/erpnext/crm/doctype/social_media_post/social_media_post.json new file mode 100644 index 0000000000..e34f02b8b5 --- /dev/null +++ b/erpnext/crm/doctype/social_media_post/social_media_post.json @@ -0,0 +1,157 @@ +{ + "actions": [], + "autoname": "format: CRM-SMP-{YYYY}-{MM}-{DD}-{###}", + "creation": "2020-01-30 11:53:13.872864", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "campaign_name", + "sheduled_time", + "post_status", + "column_break_6", + "twitter", + "linkedin", + "twitter_post_id", + "linkedin_post_id", + "content", + "text", + "column_break_14", + "tweet_preview", + "linkedin_section", + "linkedin_post", + "attachments_section", + "image", + "amended_from" + ], + "fields": [ + { + "fieldname": "text", + "fieldtype": "Small Text", + "label": "Tweet", + "reqd": 1 + }, + { + "fieldname": "image", + "fieldtype": "Attach Image", + "label": "Image" + }, + { + "default": "0", + "fieldname": "twitter", + "fieldtype": "Check", + "label": "Twitter" + }, + { + "default": "0", + "fieldname": "linkedin", + "fieldtype": "Check", + "label": "LinkedIn" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Social Media Post", + "print_hide": 1, + "read_only": 1 + }, + { + "allow_on_submit": 1, + "fieldname": "sheduled_time", + "fieldtype": "Datetime", + "label": "Scheduled Time" + }, + { + "fieldname": "content", + "fieldtype": "Section Break", + "label": "Twitter" + }, + { + "allow_on_submit": 1, + "fieldname": "post_status", + "fieldtype": "Select", + "label": "Post Status", + "options": "\nScheduled\nPosted\nError", + "read_only": 1 + }, + { + "allow_on_submit": 1, + "fieldname": "twitter_post_id", + "fieldtype": "Data", + "hidden": 1, + "label": "Twitter Post Id", + "read_only": 1 + }, + { + "allow_on_submit": 1, + "fieldname": "linkedin_post_id", + "fieldtype": "Data", + "hidden": 1, + "label": "LinkedIn Post Id", + "read_only": 1 + }, + { + "fieldname": "campaign_name", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Campaign", + "options": "Campaign" + }, + { + "fieldname": "column_break_6", + "fieldtype": "Column Break", + "label": "Share On" + }, + { + "fieldname": "column_break_14", + "fieldtype": "Column Break" + }, + { + "fieldname": "tweet_preview", + "fieldtype": "HTML" + }, + { + "collapsible": 1, + "fieldname": "linkedin_section", + "fieldtype": "Section Break", + "label": "LinkedIn" + }, + { + "collapsible": 1, + "fieldname": "attachments_section", + "fieldtype": "Section Break", + "label": "Attachments" + }, + { + "fieldname": "linkedin_post", + "fieldtype": "Text", + "label": "Post" + } + ], + "is_submittable": 1, + "links": [], + "modified": "2020-04-09 22:35:23.821991", + "modified_by": "Administrator", + "module": "CRM", + "name": "Social Media Post", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.py b/erpnext/crm/doctype/social_media_post/social_media_post.py new file mode 100644 index 0000000000..8b3387ebf2 --- /dev/null +++ b/erpnext/crm/doctype/social_media_post/social_media_post.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document +from frappe import _ + +class SocialMediaPost(Document): + def submit(self): + if self.sheduled_time: + self.post_status = "Scheduled" + super(SocialMediaPost, self).submit() + + def post(self): + try: + if self.twitter and not self.twitter_post_id: + twitter = frappe.get_doc("Twitter Settings") + twitter_post = twitter.post(self.text, self.image) + self.db_set("twitter_post_id", twitter_post.id) + if self.linkedin and not self.linkedin_post_id: + linkedin = frappe.get_doc("LinkedIn Settings") + linkedin_post = linkedin.post(self.text, self.image) + self.db_set("linkedin_post_id", linkedin_post.headers['X-RestLi-Id'].split(":")[-1]) + self.db_set("post_status", "Posted") + + except Exception as e: + self.db_set("post_status", "Error") + title = _("Error while POSTING {0}").format(self.name) + traceback = frappe.get_traceback() + frappe.log_error(message=traceback , title=title) + +def process_scheduled_social_media_posts(): + import datetime + posts = frappe.get_list("Social Media Post", filters={"status": "Scheduled"}, fields= ["name", "sheduled_time"]) + start = frappe.utils.now_datetime() + end = start + datetime.timedelta(minutes=59) + for post in posts: + post_time = frappe.utils.get_datetime(post.scheduled_time) + if post_time > start and post_time <= end: + post = frappe.get_doc('Social Media Post',post['name']) + post.post() + +@frappe.whitelist() +def publish(doctype, name): + sm_post = frappe.get_doc(doctype, name) + sm_post.post() + frappe.db.commit() diff --git a/erpnext/crm/doctype/social_media_post/social_media_post_list.js b/erpnext/crm/doctype/social_media_post/social_media_post_list.js new file mode 100644 index 0000000000..c60b91a9a0 --- /dev/null +++ b/erpnext/crm/doctype/social_media_post/social_media_post_list.js @@ -0,0 +1,10 @@ +frappe.listview_settings['Social Media Post'] = { + add_fields: ["status","post_status"], + get_indicator: function(doc) { + return [__(doc.post_status), { + "Scheduled": "orange", + "Posted": "green", + "Error": "red" + }[doc.post_status]]; + } +} diff --git a/erpnext/crm/doctype/social_media_post/test_social_media_post.py b/erpnext/crm/doctype/social_media_post/test_social_media_post.py new file mode 100644 index 0000000000..ec81ee5871 --- /dev/null +++ b/erpnext/crm/doctype/social_media_post/test_social_media_post.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestSocialMediaPost(unittest.TestCase): + pass diff --git a/erpnext/crm/doctype/twitter_settings/__init__.py b/erpnext/crm/doctype/twitter_settings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/crm/doctype/twitter_settings/test_twitter_settings.py b/erpnext/crm/doctype/twitter_settings/test_twitter_settings.py new file mode 100644 index 0000000000..3f999c1af4 --- /dev/null +++ b/erpnext/crm/doctype/twitter_settings/test_twitter_settings.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestTwitterSettings(unittest.TestCase): + pass diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.js b/erpnext/crm/doctype/twitter_settings/twitter_settings.js new file mode 100644 index 0000000000..359196bcae --- /dev/null +++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.js @@ -0,0 +1,52 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Twitter Settings', { + onload: function(frm){ + if(frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret){ + frappe.confirm( + 'Session not valid, Do you want to login?', + function(){ + frm.trigger("login"); + }, + function(){ + window.close(); + } + ) + } + }, + refresh: function(frm){ + if(frm.doc.session_status=="Active"){ + frm.dashboard.set_headline_alert( + '
' + + '
' + + ' ' + + '
' + + '
' + ); + } + else if(frm.doc.session_status=="Expired"){ + frm.dashboard.set_headline_alert( + '
' + + '
' + + ' ' + + '
' + + '
' + ); + } + }, + login: function(frm){ + if(frm.doc.consumer_key && frm.doc.consumer_secret){ + frappe.call({ + doc: frm.doc, + method: "get_authorize_url", + callback : function(r) { + window.location.href = r.message; + } + }); + } + }, + after_save: function(frm){ + frm.trigger("login"); + } +}); diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.json b/erpnext/crm/doctype/twitter_settings/twitter_settings.json new file mode 100644 index 0000000000..fbdfac907e --- /dev/null +++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.json @@ -0,0 +1,101 @@ +{ + "actions": [], + "creation": "2020-01-30 10:29:08.562108", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "account_name", + "profile_pic", + "oauth_details", + "consumer_key", + "column_break_5", + "consumer_secret", + "oauth_token", + "oauth_secret", + "session_status" + ], + "fields": [ + { + "fieldname": "account_name", + "fieldtype": "Data", + "label": "Account Name", + "read_only": 1 + }, + { + "fieldname": "oauth_details", + "fieldtype": "Section Break", + "label": "OAuth Credentials" + }, + { + "fieldname": "consumer_key", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Consumer Key", + "reqd": 1 + }, + { + "fieldname": "consumer_secret", + "fieldtype": "Password", + "in_list_view": 1, + "label": "Consumer Secret Key", + "reqd": 1 + }, + { + "fieldname": "oauth_token", + "fieldtype": "Data", + "hidden": 1, + "label": "OAuth Token", + "read_only": 1 + }, + { + "fieldname": "oauth_secret", + "fieldtype": "Password", + "hidden": 1, + "label": "OAuth Token Secret", + "read_only": 1 + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "fieldname": "profile_pic", + "fieldtype": "Attach Image", + "hidden": 1, + "read_only": 1 + }, + { + "fieldname": "session_status", + "fieldtype": "Select", + "hidden": 1, + "label": "Session Status", + "options": "Expired\nActive", + "read_only": 1 + } + ], + "image_field": "profile_pic", + "issingle": 1, + "links": [], + "modified": "2020-04-08 23:56:20.621246", + "modified_by": "Administrator", + "module": "CRM", + "name": "Twitter Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.py b/erpnext/crm/doctype/twitter_settings/twitter_settings.py new file mode 100644 index 0000000000..f069a600dd --- /dev/null +++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe, os, tweepy, json +from frappe import _ +from frappe.model.document import Document +from frappe.utils.file_manager import get_file_path +from frappe.utils import get_url_to_form, get_link_to_form +from tweepy.error import TweepError + +class TwitterSettings(Document): + def get_authorize_url(self): + callback_url = "{0}/?cmd=erpnext.crm.doctype.twitter_settings.twitter_settings.callback".format(frappe.utils.get_url()) + auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret"), callback_url) + try: + redirect_url = auth.get_authorization_url() + return redirect_url + except: + frappe.msgprint(_("Error! Failed to get request token.")) + frappe.throw(_('Invalid {0} or {1}').format(frappe.bold("Consumer Key"), frappe.bold("Consumer Secret Key"))) + + + def get_access_token(self, oauth_token, oauth_verifier): + auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret")) + auth.request_token = { + 'oauth_token' : oauth_token, + 'oauth_token_secret' : oauth_verifier + } + try: + auth.get_access_token(oauth_verifier) + self.db_set("oauth_token", auth.access_token) + self.db_set("oauth_secret", auth.access_token_secret) + api = self.get_api() + user = api.me() + self.db_set("account_name", user._json["screen_name"]) + profile_pic = (user._json["profile_image_url"]).replace("_normal","") + self.db_set("profile_pic", profile_pic) + self.db_set("session_status", "Active") + frappe.local.response["type"] = "redirect" + frappe.local.response["location"] = get_url_to_form("Twitter Settings","Twitter Settings") + except TweepError as e: + frappe.msgprint(_("Error! Failed to get access token.")) + frappe.throw(_('Invalid Consumer Key or Consumer Secret Key')) + + def get_api(self): + # authentication of consumer key and secret + auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret")) + # authentication of access token and secret + auth.set_access_token(self.oauth_token, self.get_password(fieldname="oauth_secret")) + + return tweepy.API(auth) + + def post(self, text, media=None): + if not media: + return self.send_tweet(text) + + if media: + media_id = self.upload_image(media) + return self.send_tweet(text, media_id) + + def upload_image(self, media): + media = get_file_path(media) + api = self.get_api() + media = api.media_upload(media) + return media.media_id + + def send_tweet(self, text, media_id=None): + api = self.get_api() + try: + if media_id: + response = api.update_status(status = text, media_ids = [media_id]) + else: + response = api.update_status(status = text) + return response + + except TweepError as e: + content = json.loads(e.response.content) + content = content["errors"][0] + if e.response.status_code == 401: + self.db_set("session_status", "Expired") + frappe.db.commit() + frappe.throw(content["message"],title="Twitter Error {0} {1}".format(e.response.status_code, e.response.reason)) + +@frappe.whitelist() +def callback(oauth_token, oauth_verifier): + twitter_settings = frappe.get_single("Twitter Settings") + twitter_settings.get_access_token(oauth_token,oauth_verifier) + frappe.db.commit() diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 2f07e15f1e..deaf12edd6 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -270,7 +270,8 @@ auto_cancel_exempted_doctypes= [ scheduler_events = { "all": [ "erpnext.projects.doctype.project.project.project_status_update_reminder", - "erpnext.healthcare_healthcare.doctype.patient_appointment.patient_appointment.send_appointment_reminder" + "erpnext.healthcare_healthcare.doctype.patient_appointment.patient_appointment.send_appointment_reminder", + "erpnext.crm.doctype.social_media_post.social_media_post.process_scheduled_social_media_posts" ], "hourly": [ 'erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group.trigger_emails', diff --git a/erpnext/selling/doctype/campaign/campaign_dashboard.py b/erpnext/selling/doctype/campaign/campaign_dashboard.py index a9d8eca38c..3cef560c32 100644 --- a/erpnext/selling/doctype/campaign/campaign_dashboard.py +++ b/erpnext/selling/doctype/campaign/campaign_dashboard.py @@ -8,6 +8,10 @@ def get_data(): { 'label': _('Email Campaigns'), 'items': ['Email Campaign'] + }, + { + 'label': _('Social Media Campaigns'), + 'items': ['Social Media Post'] } - ], + ] } diff --git a/requirements.txt b/requirements.txt index c277545fab..9da537e493 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,3 +8,4 @@ PyGithub==1.44.1 python-stdnum==1.12 Unidecode==1.1.1 WooCommerce==2.1.1 +tweepy==3.8.0 \ No newline at end of file From 22318b2eb68cfbfc5b3f18cf906a07a07a014873 Mon Sep 17 00:00:00 2001 From: Anupam K Date: Sat, 18 Apr 2020 00:45:18 +0530 Subject: [PATCH 2/5] Linkedin, Twitter Integration --- .../crm/doctype/linkedin_settings/__init__.py | 0 .../linkedin_settings/linkedin_settings.js | 66 ++++++++ .../linkedin_settings/linkedin_settings.json | 111 +++++++++++++ .../linkedin_settings/linkedin_settings.py | 157 ++++++++++++++++++ .../test_linkedin_settings.py | 10 ++ .../crm/doctype/social_media_post/__init__.py | 0 .../social_media_post/social_media_post.js | 54 ++++++ .../social_media_post/social_media_post.json | 157 ++++++++++++++++++ .../social_media_post/social_media_post.py | 49 ++++++ .../social_media_post_list.js | 10 ++ .../test_social_media_post.py | 10 ++ .../crm/doctype/twitter_settings/__init__.py | 0 .../twitter_settings/test_twitter_settings.py | 10 ++ .../twitter_settings/twitter_settings.js | 52 ++++++ .../twitter_settings/twitter_settings.json | 101 +++++++++++ .../twitter_settings/twitter_settings.py | 90 ++++++++++ erpnext/hooks.py | 3 +- .../doctype/campaign/campaign_dashboard.py | 6 +- requirements.txt | 1 + 19 files changed, 885 insertions(+), 2 deletions(-) create mode 100644 erpnext/crm/doctype/linkedin_settings/__init__.py create mode 100644 erpnext/crm/doctype/linkedin_settings/linkedin_settings.js create mode 100644 erpnext/crm/doctype/linkedin_settings/linkedin_settings.json create mode 100644 erpnext/crm/doctype/linkedin_settings/linkedin_settings.py create mode 100644 erpnext/crm/doctype/linkedin_settings/test_linkedin_settings.py create mode 100644 erpnext/crm/doctype/social_media_post/__init__.py create mode 100644 erpnext/crm/doctype/social_media_post/social_media_post.js create mode 100644 erpnext/crm/doctype/social_media_post/social_media_post.json create mode 100644 erpnext/crm/doctype/social_media_post/social_media_post.py create mode 100644 erpnext/crm/doctype/social_media_post/social_media_post_list.js create mode 100644 erpnext/crm/doctype/social_media_post/test_social_media_post.py create mode 100644 erpnext/crm/doctype/twitter_settings/__init__.py create mode 100644 erpnext/crm/doctype/twitter_settings/test_twitter_settings.py create mode 100644 erpnext/crm/doctype/twitter_settings/twitter_settings.js create mode 100644 erpnext/crm/doctype/twitter_settings/twitter_settings.json create mode 100644 erpnext/crm/doctype/twitter_settings/twitter_settings.py diff --git a/erpnext/crm/doctype/linkedin_settings/__init__.py b/erpnext/crm/doctype/linkedin_settings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js new file mode 100644 index 0000000000..fd169f8328 --- /dev/null +++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js @@ -0,0 +1,66 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('LinkedIn Settings', { + onload: function(frm){ + if(frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret){ + frappe.confirm( + 'Session not valid, Do you want to login?', + function(){ + frm.trigger("login"); + }, + function(){ + window.close(); + } + ) + } + }, + refresh: function(frm){ + if(frm.doc.session_status=="Expired"){ + frm.dashboard.set_headline_alert( + '
' + + '
' + + ' ' + + '
' + + '
' + ); + } + if(frm.doc.session_status=="Active"){ + let d = new Date(frm.doc.modified) + d.setDate(d.getDate()+60); + let dn = new Date() + let days = d.getTime() - dn.getTime(); + days = Math.floor(days/(1000 * 3600 * 24)); + let msg,color; + if(days>0){ + msg = "Your Session will be expire in " + days + " days."; + color = "green"; + } + else{ + msg = "Session is expired. Save doc to login."; + color = "red"; + } + frm.dashboard.set_headline_alert( + '
' + + '
' + + ' ' + + '
' + + '
' + ); + } + }, + login: function(frm){ + if(frm.doc.consumer_key && frm.doc.consumer_secret){ + frappe.call({ + doc: frm.doc, + method: "get_authorization_url", + callback : function(r) { + window.location.href = r.message; + } + }); + } + }, + after_save: function(frm){ + frm.trigger("login"); + } +}); diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.json b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.json new file mode 100644 index 0000000000..9eacb0011c --- /dev/null +++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.json @@ -0,0 +1,111 @@ +{ + "actions": [], + "creation": "2020-01-30 13:36:39.492931", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "account_name", + "column_break_2", + "company_id", + "oauth_details", + "consumer_key", + "column_break_5", + "consumer_secret", + "user_details_section", + "access_token", + "person_urn", + "session_status" + ], + "fields": [ + { + "fieldname": "account_name", + "fieldtype": "Data", + "label": "Account Name", + "read_only": 1 + }, + { + "fieldname": "oauth_details", + "fieldtype": "Section Break", + "label": "OAuth Credentials" + }, + { + "fieldname": "consumer_key", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Consumer Key", + "reqd": 1 + }, + { + "fieldname": "consumer_secret", + "fieldtype": "Password", + "in_list_view": 1, + "label": "Consumer Secret", + "reqd": 1 + }, + { + "fieldname": "access_token", + "fieldtype": "Data", + "hidden": 1, + "label": "Access Token", + "read_only": 1 + }, + { + "fieldname": "person_urn", + "fieldtype": "Data", + "hidden": 1, + "label": "Person URN", + "read_only": 1 + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "fieldname": "user_details_section", + "fieldtype": "Section Break", + "label": "User Details" + }, + { + "fieldname": "session_status", + "fieldtype": "Select", + "hidden": 1, + "label": "Session Status", + "options": "Expired\nActive", + "read_only": 1 + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "fieldname": "company_id", + "fieldtype": "Data", + "label": "Company ID", + "reqd": 1 + } + ], + "issingle": 1, + "links": [], + "modified": "2020-04-16 23:22:51.966397", + "modified_by": "Administrator", + "module": "CRM", + "name": "LinkedIn Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py new file mode 100644 index 0000000000..9326e6d046 --- /dev/null +++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py @@ -0,0 +1,157 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe, requests, json +from frappe import _ +from frappe.utils import get_site_url, get_url_to_form, get_link_to_form +from frappe.model.document import Document +from frappe.utils.file_manager import get_file, get_file_path +from six.moves.urllib.parse import urlencode +class LinkedInSettings(Document): + def get_authorization_url(self): + params = urlencode({ + "response_type":"code", + "client_id": self.consumer_key, + "redirect_uri": get_site_url(frappe.local.site) + "/?cmd=erpnext.crm.doctype.linkedin_settings.linkedin_settings.callback", + "scope": "r_emailaddress w_organization_social r_basicprofile r_liteprofile r_organization_social rw_organization_admin w_member_social" + }) + + url = "https://www.linkedin.com/oauth/v2/authorization?{}".format(params) + + return url + + def get_access_token(self, code): + url = "https://www.linkedin.com/oauth/v2/accessToken" + body = { + "grant_type": "authorization_code", + "code": code, + "client_id": self.consumer_key, + "client_secret": self.get_password(fieldname="consumer_secret"), + "redirect_uri": get_site_url(frappe.local.site) + "/?cmd=erpnext.crm.doctype.linkedin_settings.linkedin_settings.callback", + } + headers = { + "Content-Type": "application/x-www-form-urlencoded" + } + + response = self.http_post(url=url, data=body, headers=headers) + response = frappe.parse_json(response.content.decode()) + self.db_set("access_token", response["access_token"]) + + def get_member_profile(self): + headers = { + "Authorization": "Bearer {}".format(self.access_token) + } + url = "https://api.linkedin.com/v2/me" + response = requests.get(url=url, headers=headers) + response = frappe.parse_json(response.content.decode()) + self.db_set("person_urn", response["id"]) + self.db_set("account_name", response["vanityName"]) + self.db_set("session_status", "Active") + frappe.local.response["type"] = "redirect" + frappe.local.response["location"] = get_url_to_form("LinkedIn Settings","LinkedIn Settings") + + def post(self, text, media=None): + if not media: + return self.post_text(text) + else: + media_id = self.upload_image(media) + if media_id: + return self.post_text(text, media_id=media_id) + else: + frappe.log_error("Failed to upload media.","LinkedIn Upload Error") + + + def upload_image(self, media): + media = get_file_path(media) + register_url = "https://api.linkedin.com/v2/assets?action=registerUpload" + + body = { + "registerUploadRequest": { + "recipes": ["urn:li:digitalmediaRecipe:feedshare-image"], + "owner": "urn:li:organization:{0}".format(self.company_id), + "serviceRelationships": [{ + "relationshipType": "OWNER", + "identifier": "urn:li:userGeneratedContent" + }] + } + } + headers = { + "Authorization": "Bearer {}".format(self.access_token) + } + response = self.http_post(url=register_url, body=body, headers=headers) + if response.status_code == 200: + response = response.json() + asset = response["value"]["asset"] + upload_url = response["value"]["uploadMechanism"]["com.linkedin.digitalmedia.uploading.MediaUploadHttpRequest"]["uploadUrl"] + headers['Content-Type']='image/jpeg' + response = self.http_post(upload_url, headers=headers, data=open(media,"rb")) + if response.status_code < 200 and response.status_code > 299: + frappe.throw(_("Error While Uploading Image"), title="{0} {1}".format(response.status_code, response.reason)) + return None + return asset + return None + + + def post_text(self, text, media_id=None): + # url = "https://api.linkedin.com/v2/ugcPosts" + url = "https://api.linkedin.com/v2/shares" + headers = { + "X-Restli-Protocol-Version": "2.0.0", + "Authorization": "Bearer {}".format(self.access_token), + "Content-Type": "application/json; charset=UTF-8" + } + body = { + "distribution": { + "linkedInDistributionTarget": {} + }, + "owner":"urn:li:organization:{0}".format(self.company_id), + "subject": "Test Share Subject", + "text": { + "text": text + } + } + if media_id: + body["content"]= { + "contentEntities": [{ + "entity": media_id + }], + "shareMediaCategory": "IMAGE" + } + response = self.http_post(url=url, headers=headers, body=body) + return response + + def http_post(self, url, headers=None, body=None, data=None): + try: + response = requests.post( + url = url, + json = body, + data = data, + headers = headers + ) + if response.status_code not in [201,200]: + raise + except Exception as e: + content = json.loads(response.content) + if response.status_code == 401: + self.db_set("session_status", "Expired") + frappe.db.commit() + frappe.throw(content["message"], title="LinkedIn Error - Unauthorized") + elif response.status_code == 403: + frappe.msgprint(_("You Didn't have permission to access this API")) + frappe.throw(content["message"], title="LinkedIn Error - Access Denied") + else: + frappe.throw(response.reason, title=response.status_code) + return response + +@frappe.whitelist() +def callback(code=None, error=None, error_description=None): + if not error: + linkedin_settings = frappe.get_doc("LinkedIn Settings") + linkedin_settings.get_access_token(code) + linkedin_settings.get_member_profile() + frappe.db.commit() + else: + frappe.local.response["type"] = "redirect" + frappe.local.response["location"] = get_url_to_form("LinkedIn Settings","LinkedIn Settings") diff --git a/erpnext/crm/doctype/linkedin_settings/test_linkedin_settings.py b/erpnext/crm/doctype/linkedin_settings/test_linkedin_settings.py new file mode 100644 index 0000000000..9c3ef3f32f --- /dev/null +++ b/erpnext/crm/doctype/linkedin_settings/test_linkedin_settings.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestLinkedInSettings(unittest.TestCase): + pass diff --git a/erpnext/crm/doctype/social_media_post/__init__.py b/erpnext/crm/doctype/social_media_post/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.js b/erpnext/crm/doctype/social_media_post/social_media_post.js new file mode 100644 index 0000000000..c858885d2b --- /dev/null +++ b/erpnext/crm/doctype/social_media_post/social_media_post.js @@ -0,0 +1,54 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +frappe.ui.form.on('Social Media Post', { + validate: function(frm){ + if(frm.doc.text.length > 280){ + frappe.throw("Length Must be less than 280.") + } + }, + refresh: function(frm){ + if(frm.doc.docstatus === 1){ + if(frm.doc.post_status != "Posted"){ + add_post_btn(frm); + } + else if(frm.doc.post_status == "Posted"){ + frm.set_df_property('sheduled_time', 'read_only', 1); + } + let html = '
'; + if(frm.doc.twitter){ + let color = frm.doc.twitter_post_id ? "green" : "red"; + let status = frm.doc.twitter_post_id ? "Posted" : "Not Posted"; + html += '
' + + ' ' + + '
' ; + } + if(frm.doc.linkedin){ + let color = frm.doc.linkedin_post_id ? "green" : "red"; + let status = frm.doc.linkedin_post_id ? "Posted" : "Not Posted"; + html += '
' + + ' ' + + '
' ; + } + html += '
'; + frm.dashboard.set_headline_alert(html); + } + } +}); +var add_post_btn = function(frm){ + frm.add_custom_button(('Post Now'), function(){ + post(frm); + }); +} +var post = function(frm){ + frappe.call({ + method: "erpnext.crm.doctype.social_media_post.social_media_post.publish", + args: { + doctype: frm.doc.doctype, + name: frm.doc.name + }, + callback: function(r) { + frm.reload_doc(); + } + }) + +} \ No newline at end of file diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.json b/erpnext/crm/doctype/social_media_post/social_media_post.json new file mode 100644 index 0000000000..e34f02b8b5 --- /dev/null +++ b/erpnext/crm/doctype/social_media_post/social_media_post.json @@ -0,0 +1,157 @@ +{ + "actions": [], + "autoname": "format: CRM-SMP-{YYYY}-{MM}-{DD}-{###}", + "creation": "2020-01-30 11:53:13.872864", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "campaign_name", + "sheduled_time", + "post_status", + "column_break_6", + "twitter", + "linkedin", + "twitter_post_id", + "linkedin_post_id", + "content", + "text", + "column_break_14", + "tweet_preview", + "linkedin_section", + "linkedin_post", + "attachments_section", + "image", + "amended_from" + ], + "fields": [ + { + "fieldname": "text", + "fieldtype": "Small Text", + "label": "Tweet", + "reqd": 1 + }, + { + "fieldname": "image", + "fieldtype": "Attach Image", + "label": "Image" + }, + { + "default": "0", + "fieldname": "twitter", + "fieldtype": "Check", + "label": "Twitter" + }, + { + "default": "0", + "fieldname": "linkedin", + "fieldtype": "Check", + "label": "LinkedIn" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Social Media Post", + "print_hide": 1, + "read_only": 1 + }, + { + "allow_on_submit": 1, + "fieldname": "sheduled_time", + "fieldtype": "Datetime", + "label": "Scheduled Time" + }, + { + "fieldname": "content", + "fieldtype": "Section Break", + "label": "Twitter" + }, + { + "allow_on_submit": 1, + "fieldname": "post_status", + "fieldtype": "Select", + "label": "Post Status", + "options": "\nScheduled\nPosted\nError", + "read_only": 1 + }, + { + "allow_on_submit": 1, + "fieldname": "twitter_post_id", + "fieldtype": "Data", + "hidden": 1, + "label": "Twitter Post Id", + "read_only": 1 + }, + { + "allow_on_submit": 1, + "fieldname": "linkedin_post_id", + "fieldtype": "Data", + "hidden": 1, + "label": "LinkedIn Post Id", + "read_only": 1 + }, + { + "fieldname": "campaign_name", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Campaign", + "options": "Campaign" + }, + { + "fieldname": "column_break_6", + "fieldtype": "Column Break", + "label": "Share On" + }, + { + "fieldname": "column_break_14", + "fieldtype": "Column Break" + }, + { + "fieldname": "tweet_preview", + "fieldtype": "HTML" + }, + { + "collapsible": 1, + "fieldname": "linkedin_section", + "fieldtype": "Section Break", + "label": "LinkedIn" + }, + { + "collapsible": 1, + "fieldname": "attachments_section", + "fieldtype": "Section Break", + "label": "Attachments" + }, + { + "fieldname": "linkedin_post", + "fieldtype": "Text", + "label": "Post" + } + ], + "is_submittable": 1, + "links": [], + "modified": "2020-04-09 22:35:23.821991", + "modified_by": "Administrator", + "module": "CRM", + "name": "Social Media Post", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.py b/erpnext/crm/doctype/social_media_post/social_media_post.py new file mode 100644 index 0000000000..8b3387ebf2 --- /dev/null +++ b/erpnext/crm/doctype/social_media_post/social_media_post.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document +from frappe import _ + +class SocialMediaPost(Document): + def submit(self): + if self.sheduled_time: + self.post_status = "Scheduled" + super(SocialMediaPost, self).submit() + + def post(self): + try: + if self.twitter and not self.twitter_post_id: + twitter = frappe.get_doc("Twitter Settings") + twitter_post = twitter.post(self.text, self.image) + self.db_set("twitter_post_id", twitter_post.id) + if self.linkedin and not self.linkedin_post_id: + linkedin = frappe.get_doc("LinkedIn Settings") + linkedin_post = linkedin.post(self.text, self.image) + self.db_set("linkedin_post_id", linkedin_post.headers['X-RestLi-Id'].split(":")[-1]) + self.db_set("post_status", "Posted") + + except Exception as e: + self.db_set("post_status", "Error") + title = _("Error while POSTING {0}").format(self.name) + traceback = frappe.get_traceback() + frappe.log_error(message=traceback , title=title) + +def process_scheduled_social_media_posts(): + import datetime + posts = frappe.get_list("Social Media Post", filters={"status": "Scheduled"}, fields= ["name", "sheduled_time"]) + start = frappe.utils.now_datetime() + end = start + datetime.timedelta(minutes=59) + for post in posts: + post_time = frappe.utils.get_datetime(post.scheduled_time) + if post_time > start and post_time <= end: + post = frappe.get_doc('Social Media Post',post['name']) + post.post() + +@frappe.whitelist() +def publish(doctype, name): + sm_post = frappe.get_doc(doctype, name) + sm_post.post() + frappe.db.commit() diff --git a/erpnext/crm/doctype/social_media_post/social_media_post_list.js b/erpnext/crm/doctype/social_media_post/social_media_post_list.js new file mode 100644 index 0000000000..c60b91a9a0 --- /dev/null +++ b/erpnext/crm/doctype/social_media_post/social_media_post_list.js @@ -0,0 +1,10 @@ +frappe.listview_settings['Social Media Post'] = { + add_fields: ["status","post_status"], + get_indicator: function(doc) { + return [__(doc.post_status), { + "Scheduled": "orange", + "Posted": "green", + "Error": "red" + }[doc.post_status]]; + } +} diff --git a/erpnext/crm/doctype/social_media_post/test_social_media_post.py b/erpnext/crm/doctype/social_media_post/test_social_media_post.py new file mode 100644 index 0000000000..ec81ee5871 --- /dev/null +++ b/erpnext/crm/doctype/social_media_post/test_social_media_post.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestSocialMediaPost(unittest.TestCase): + pass diff --git a/erpnext/crm/doctype/twitter_settings/__init__.py b/erpnext/crm/doctype/twitter_settings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/crm/doctype/twitter_settings/test_twitter_settings.py b/erpnext/crm/doctype/twitter_settings/test_twitter_settings.py new file mode 100644 index 0000000000..3f999c1af4 --- /dev/null +++ b/erpnext/crm/doctype/twitter_settings/test_twitter_settings.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestTwitterSettings(unittest.TestCase): + pass diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.js b/erpnext/crm/doctype/twitter_settings/twitter_settings.js new file mode 100644 index 0000000000..359196bcae --- /dev/null +++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.js @@ -0,0 +1,52 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Twitter Settings', { + onload: function(frm){ + if(frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret){ + frappe.confirm( + 'Session not valid, Do you want to login?', + function(){ + frm.trigger("login"); + }, + function(){ + window.close(); + } + ) + } + }, + refresh: function(frm){ + if(frm.doc.session_status=="Active"){ + frm.dashboard.set_headline_alert( + '
' + + '
' + + ' ' + + '
' + + '
' + ); + } + else if(frm.doc.session_status=="Expired"){ + frm.dashboard.set_headline_alert( + '
' + + '
' + + ' ' + + '
' + + '
' + ); + } + }, + login: function(frm){ + if(frm.doc.consumer_key && frm.doc.consumer_secret){ + frappe.call({ + doc: frm.doc, + method: "get_authorize_url", + callback : function(r) { + window.location.href = r.message; + } + }); + } + }, + after_save: function(frm){ + frm.trigger("login"); + } +}); diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.json b/erpnext/crm/doctype/twitter_settings/twitter_settings.json new file mode 100644 index 0000000000..fbdfac907e --- /dev/null +++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.json @@ -0,0 +1,101 @@ +{ + "actions": [], + "creation": "2020-01-30 10:29:08.562108", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "account_name", + "profile_pic", + "oauth_details", + "consumer_key", + "column_break_5", + "consumer_secret", + "oauth_token", + "oauth_secret", + "session_status" + ], + "fields": [ + { + "fieldname": "account_name", + "fieldtype": "Data", + "label": "Account Name", + "read_only": 1 + }, + { + "fieldname": "oauth_details", + "fieldtype": "Section Break", + "label": "OAuth Credentials" + }, + { + "fieldname": "consumer_key", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Consumer Key", + "reqd": 1 + }, + { + "fieldname": "consumer_secret", + "fieldtype": "Password", + "in_list_view": 1, + "label": "Consumer Secret Key", + "reqd": 1 + }, + { + "fieldname": "oauth_token", + "fieldtype": "Data", + "hidden": 1, + "label": "OAuth Token", + "read_only": 1 + }, + { + "fieldname": "oauth_secret", + "fieldtype": "Password", + "hidden": 1, + "label": "OAuth Token Secret", + "read_only": 1 + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "fieldname": "profile_pic", + "fieldtype": "Attach Image", + "hidden": 1, + "read_only": 1 + }, + { + "fieldname": "session_status", + "fieldtype": "Select", + "hidden": 1, + "label": "Session Status", + "options": "Expired\nActive", + "read_only": 1 + } + ], + "image_field": "profile_pic", + "issingle": 1, + "links": [], + "modified": "2020-04-08 23:56:20.621246", + "modified_by": "Administrator", + "module": "CRM", + "name": "Twitter Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.py b/erpnext/crm/doctype/twitter_settings/twitter_settings.py new file mode 100644 index 0000000000..f069a600dd --- /dev/null +++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe, os, tweepy, json +from frappe import _ +from frappe.model.document import Document +from frappe.utils.file_manager import get_file_path +from frappe.utils import get_url_to_form, get_link_to_form +from tweepy.error import TweepError + +class TwitterSettings(Document): + def get_authorize_url(self): + callback_url = "{0}/?cmd=erpnext.crm.doctype.twitter_settings.twitter_settings.callback".format(frappe.utils.get_url()) + auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret"), callback_url) + try: + redirect_url = auth.get_authorization_url() + return redirect_url + except: + frappe.msgprint(_("Error! Failed to get request token.")) + frappe.throw(_('Invalid {0} or {1}').format(frappe.bold("Consumer Key"), frappe.bold("Consumer Secret Key"))) + + + def get_access_token(self, oauth_token, oauth_verifier): + auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret")) + auth.request_token = { + 'oauth_token' : oauth_token, + 'oauth_token_secret' : oauth_verifier + } + try: + auth.get_access_token(oauth_verifier) + self.db_set("oauth_token", auth.access_token) + self.db_set("oauth_secret", auth.access_token_secret) + api = self.get_api() + user = api.me() + self.db_set("account_name", user._json["screen_name"]) + profile_pic = (user._json["profile_image_url"]).replace("_normal","") + self.db_set("profile_pic", profile_pic) + self.db_set("session_status", "Active") + frappe.local.response["type"] = "redirect" + frappe.local.response["location"] = get_url_to_form("Twitter Settings","Twitter Settings") + except TweepError as e: + frappe.msgprint(_("Error! Failed to get access token.")) + frappe.throw(_('Invalid Consumer Key or Consumer Secret Key')) + + def get_api(self): + # authentication of consumer key and secret + auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret")) + # authentication of access token and secret + auth.set_access_token(self.oauth_token, self.get_password(fieldname="oauth_secret")) + + return tweepy.API(auth) + + def post(self, text, media=None): + if not media: + return self.send_tweet(text) + + if media: + media_id = self.upload_image(media) + return self.send_tweet(text, media_id) + + def upload_image(self, media): + media = get_file_path(media) + api = self.get_api() + media = api.media_upload(media) + return media.media_id + + def send_tweet(self, text, media_id=None): + api = self.get_api() + try: + if media_id: + response = api.update_status(status = text, media_ids = [media_id]) + else: + response = api.update_status(status = text) + return response + + except TweepError as e: + content = json.loads(e.response.content) + content = content["errors"][0] + if e.response.status_code == 401: + self.db_set("session_status", "Expired") + frappe.db.commit() + frappe.throw(content["message"],title="Twitter Error {0} {1}".format(e.response.status_code, e.response.reason)) + +@frappe.whitelist() +def callback(oauth_token, oauth_verifier): + twitter_settings = frappe.get_single("Twitter Settings") + twitter_settings.get_access_token(oauth_token,oauth_verifier) + frappe.db.commit() diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 2f07e15f1e..deaf12edd6 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -270,7 +270,8 @@ auto_cancel_exempted_doctypes= [ scheduler_events = { "all": [ "erpnext.projects.doctype.project.project.project_status_update_reminder", - "erpnext.healthcare_healthcare.doctype.patient_appointment.patient_appointment.send_appointment_reminder" + "erpnext.healthcare_healthcare.doctype.patient_appointment.patient_appointment.send_appointment_reminder", + "erpnext.crm.doctype.social_media_post.social_media_post.process_scheduled_social_media_posts" ], "hourly": [ 'erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group.trigger_emails', diff --git a/erpnext/selling/doctype/campaign/campaign_dashboard.py b/erpnext/selling/doctype/campaign/campaign_dashboard.py index a9d8eca38c..3cef560c32 100644 --- a/erpnext/selling/doctype/campaign/campaign_dashboard.py +++ b/erpnext/selling/doctype/campaign/campaign_dashboard.py @@ -8,6 +8,10 @@ def get_data(): { 'label': _('Email Campaigns'), 'items': ['Email Campaign'] + }, + { + 'label': _('Social Media Campaigns'), + 'items': ['Social Media Post'] } - ], + ] } diff --git a/requirements.txt b/requirements.txt index c277545fab..9da537e493 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,3 +8,4 @@ PyGithub==1.44.1 python-stdnum==1.12 Unidecode==1.1.1 WooCommerce==2.1.1 +tweepy==3.8.0 \ No newline at end of file From c12a40c923585bc6660afc73e330da63ebbae437 Mon Sep 17 00:00:00 2001 From: Anupam K Date: Sat, 18 Apr 2020 01:50:18 +0530 Subject: [PATCH 3/5] Linkedin, Twitter Integration --- .../doctype/social_media_post/social_media_post.js | 2 +- .../social_media_post/social_media_post.json | 14 +++++++++++--- .../doctype/social_media_post/social_media_post.py | 2 +- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.js b/erpnext/crm/doctype/social_media_post/social_media_post.js index c858885d2b..690e27c79b 100644 --- a/erpnext/crm/doctype/social_media_post/social_media_post.js +++ b/erpnext/crm/doctype/social_media_post/social_media_post.js @@ -3,7 +3,7 @@ frappe.ui.form.on('Social Media Post', { validate: function(frm){ if(frm.doc.text.length > 280){ - frappe.throw("Length Must be less than 280.") + frappe.throw(__("Length Must be less than 280.")) } }, refresh: function(frm){ diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.json b/erpnext/crm/doctype/social_media_post/social_media_post.json index e34f02b8b5..f8c23a7e46 100644 --- a/erpnext/crm/doctype/social_media_post/social_media_post.json +++ b/erpnext/crm/doctype/social_media_post/social_media_post.json @@ -20,6 +20,7 @@ "tweet_preview", "linkedin_section", "linkedin_post", + "column_break_15", "attachments_section", "image", "amended_from" @@ -29,7 +30,7 @@ "fieldname": "text", "fieldtype": "Small Text", "label": "Tweet", - "reqd": 1 + "mandatory_depends_on": "eval:doc.twitter ==1" }, { "fieldname": "image", @@ -64,6 +65,7 @@ "label": "Scheduled Time" }, { + "depends_on": "eval:doc.twitter ==1", "fieldname": "content", "fieldtype": "Section Break", "label": "Twitter" @@ -114,6 +116,7 @@ }, { "collapsible": 1, + "depends_on": "eval:doc.linkedin==1", "fieldname": "linkedin_section", "fieldtype": "Section Break", "label": "LinkedIn" @@ -127,12 +130,17 @@ { "fieldname": "linkedin_post", "fieldtype": "Text", - "label": "Post" + "label": "Post", + "mandatory_depends_on": "eval:doc.linkedin ==1" + }, + { + "fieldname": "column_break_15", + "fieldtype": "Column Break" } ], "is_submittable": 1, "links": [], - "modified": "2020-04-09 22:35:23.821991", + "modified": "2020-04-18 01:28:35.995490", "modified_by": "Administrator", "module": "CRM", "name": "Social Media Post", diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.py b/erpnext/crm/doctype/social_media_post/social_media_post.py index 8b3387ebf2..672486deac 100644 --- a/erpnext/crm/doctype/social_media_post/social_media_post.py +++ b/erpnext/crm/doctype/social_media_post/social_media_post.py @@ -21,7 +21,7 @@ class SocialMediaPost(Document): self.db_set("twitter_post_id", twitter_post.id) if self.linkedin and not self.linkedin_post_id: linkedin = frappe.get_doc("LinkedIn Settings") - linkedin_post = linkedin.post(self.text, self.image) + linkedin_post = linkedin.post(self.linkedin_post, self.image) self.db_set("linkedin_post_id", linkedin_post.headers['X-RestLi-Id'].split(":")[-1]) self.db_set("post_status", "Posted") From e01723e360f0d7141b13ad0b7d5390bee02b14f5 Mon Sep 17 00:00:00 2001 From: Anupam K Date: Thu, 23 Apr 2020 22:49:28 +0530 Subject: [PATCH 4/5] Review changes --- .../linkedin_settings/linkedin_settings.js | 42 ++++++++++--------- .../linkedin_settings/linkedin_settings.py | 23 ++++++---- .../social_media_post/social_media_post.js | 35 ++++++++-------- .../social_media_post/social_media_post.py | 2 +- .../twitter_settings/twitter_settings.js | 37 ++++++++-------- .../twitter_settings/twitter_settings.py | 20 ++++++--- 6 files changed, 90 insertions(+), 69 deletions(-) diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js index b888e721bb..b05b6021af 100644 --- a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js +++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js @@ -3,7 +3,7 @@ frappe.ui.form.on('LinkedIn Settings', { onload: function(frm){ - if(frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret){ + if (frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret){ frappe.confirm( __('Session not valid, Do you want to login?'), function(){ @@ -12,45 +12,49 @@ frappe.ui.form.on('LinkedIn Settings', { function(){ window.close(); } - ) + ); } }, refresh: function(frm){ - if(frm.doc.session_status=="Expired"){ + if (frm.doc.session_status=="Expired"){ + let msg = __("Session Not Active. Save doc to login."); frm.dashboard.set_headline_alert( - '
' + - '
' + - ' ' + - '
' + - '
' + `
+
+ +
+
` ); } - if(frm.doc.session_status=="Active"){ - let d = new Date(frm.doc.modified) + + if (frm.doc.session_status=="Active"){ + let d = new Date(frm.doc.modified); d.setDate(d.getDate()+60); - let dn = new Date() + let dn = new Date(); let days = d.getTime() - dn.getTime(); days = Math.floor(days/(1000 * 3600 * 24)); let msg,color; - if(days>0){ + + if (days>0){ msg = __("Your Session will be expire in ") + days + __(" days."); color = "green"; } - else{ + else { msg = __("Session is expired. Save doc to login."); color = "red"; } + frm.dashboard.set_headline_alert( - '
' + - '
' + - ' ' + - '
' + - '
' + `
+
+ +
+
` ); } }, login: function(frm){ - if(frm.doc.consumer_key && frm.doc.consumer_secret){ + if (frm.doc.consumer_key && frm.doc.consumer_secret){ frappe.call({ doc: frm.doc, method: "get_authorization_url", diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py index 9326e6d046..5df35df3dd 100644 --- a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py +++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py @@ -9,6 +9,7 @@ from frappe.utils import get_site_url, get_url_to_form, get_link_to_form from frappe.model.document import Document from frappe.utils.file_manager import get_file, get_file_path from six.moves.urllib.parse import urlencode + class LinkedInSettings(Document): def get_authorization_url(self): params = urlencode({ @@ -46,9 +47,12 @@ class LinkedInSettings(Document): url = "https://api.linkedin.com/v2/me" response = requests.get(url=url, headers=headers) response = frappe.parse_json(response.content.decode()) - self.db_set("person_urn", response["id"]) - self.db_set("account_name", response["vanityName"]) - self.db_set("session_status", "Active") + + frappe.db.set_value(self.doctype, self.name, { + "person_urn": response["id"], + "account_name": response["vanityName"], + "session_status": "Active" + }) frappe.local.response["type"] = "redirect" frappe.local.response["location"] = get_url_to_form("LinkedIn Settings","LinkedIn Settings") @@ -57,6 +61,7 @@ class LinkedInSettings(Document): return self.post_text(text) else: media_id = self.upload_image(media) + if media_id: return self.post_text(text, media_id=media_id) else: @@ -66,7 +71,6 @@ class LinkedInSettings(Document): def upload_image(self, media): media = get_file_path(media) register_url = "https://api.linkedin.com/v2/assets?action=registerUpload" - body = { "registerUploadRequest": { "recipes": ["urn:li:digitalmediaRecipe:feedshare-image"], @@ -81,6 +85,7 @@ class LinkedInSettings(Document): "Authorization": "Bearer {}".format(self.access_token) } response = self.http_post(url=register_url, body=body, headers=headers) + if response.status_code == 200: response = response.json() asset = response["value"]["asset"] @@ -91,11 +96,10 @@ class LinkedInSettings(Document): frappe.throw(_("Error While Uploading Image"), title="{0} {1}".format(response.status_code, response.reason)) return None return asset + return None - def post_text(self, text, media_id=None): - # url = "https://api.linkedin.com/v2/ugcPosts" url = "https://api.linkedin.com/v2/shares" headers = { "X-Restli-Protocol-Version": "2.0.0", @@ -112,6 +116,7 @@ class LinkedInSettings(Document): "text": text } } + if media_id: body["content"]= { "contentEntities": [{ @@ -119,6 +124,7 @@ class LinkedInSettings(Document): }], "shareMediaCategory": "IMAGE" } + response = self.http_post(url=url, headers=headers, body=body) return response @@ -132,17 +138,20 @@ class LinkedInSettings(Document): ) if response.status_code not in [201,200]: raise + except Exception as e: content = json.loads(response.content) + if response.status_code == 401: self.db_set("session_status", "Expired") frappe.db.commit() - frappe.throw(content["message"], title="LinkedIn Error - Unauthorized") + frappe.throw(content["message"], title="LinkedIn Error - Unauthorized") elif response.status_code == 403: frappe.msgprint(_("You Didn't have permission to access this API")) frappe.throw(content["message"], title="LinkedIn Error - Access Denied") else: frappe.throw(response.reason, title=response.status_code) + return response @frappe.whitelist() diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.js b/erpnext/crm/doctype/social_media_post/social_media_post.js index 517b3b4614..c2a17d8bec 100644 --- a/erpnext/crm/doctype/social_media_post/social_media_post.js +++ b/erpnext/crm/doctype/social_media_post/social_media_post.js @@ -2,44 +2,45 @@ // For license information, please see license.txt frappe.ui.form.on('Social Media Post', { validate: function(frm){ - if(frm.doc.twitter==0 && frm.doc.linkedin==0){ + if (frm.doc.twitter === 0 && frm.doc.linkedin === 0){ frappe.throw(__("Select atleast one Social Media from Share on.")) } - if(frm.doc.scheduled_time) { + if (frm.doc.scheduled_time) { let scheduled_time = new Date(frm.doc.scheduled_time); let date_time = new Date(); - if(scheduled_time.getTime() < date_time.getTime()){ + if (scheduled_time.getTime() < date_time.getTime()){ frappe.throw(__("Invalid Scheduled Time")); } } - if(frm.doc.text?.length > 280){ + if (frm.doc.text?.length > 280){ frappe.throw(__("Length Must be less than 280.")) } }, refresh: function(frm){ - if(frm.doc.docstatus === 1){ - if(frm.doc.post_status != "Posted"){ + if (frm.doc.docstatus === 1){ + if (frm.doc.post_status != "Posted"){ add_post_btn(frm); } - else if(frm.doc.post_status == "Posted"){ + else if (frm.doc.post_status == "Posted"){ frm.set_df_property('sheduled_time', 'read_only', 1); } - let html = '
'; - if(frm.doc.twitter){ + + let html=''; + if (frm.doc.twitter){ let color = frm.doc.twitter_post_id ? "green" : "red"; let status = frm.doc.twitter_post_id ? "Posted" : "Not Posted"; - html += '
' + - ' ' + - '
' ; + html += `
+ +
` ; } - if(frm.doc.linkedin){ + if (frm.doc.linkedin){ let color = frm.doc.linkedin_post_id ? "green" : "red"; let status = frm.doc.linkedin_post_id ? "Posted" : "Not Posted"; - html += '
' + - ' ' + - '
' ; + html += `
+ +
` ; } - html += '
'; + html = `
${html}
`; frm.dashboard.set_headline_alert(html); } } diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.py b/erpnext/crm/doctype/social_media_post/social_media_post.py index 2cca60892b..ed1b583944 100644 --- a/erpnext/crm/doctype/social_media_post/social_media_post.py +++ b/erpnext/crm/doctype/social_media_post/social_media_post.py @@ -33,7 +33,7 @@ class SocialMediaPost(Document): self.db_set("linkedin_post_id", linkedin_post.headers['X-RestLi-Id'].split(":")[-1]) self.db_set("post_status", "Posted") - except Exception as e: + except: self.db_set("post_status", "Error") title = _("Error while POSTING {0}").format(self.name) traceback = frappe.get_traceback() diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.js b/erpnext/crm/doctype/twitter_settings/twitter_settings.js index be5ae9d710..eae25206b3 100644 --- a/erpnext/crm/doctype/twitter_settings/twitter_settings.js +++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.js @@ -3,7 +3,7 @@ frappe.ui.form.on('Twitter Settings', { onload: function(frm){ - if(frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret){ + if (frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret){ frappe.confirm( __('Session not valid, Do you want to login?'), function(){ @@ -12,31 +12,30 @@ frappe.ui.form.on('Twitter Settings', { function(){ window.close(); } - ) + ); } }, refresh: function(frm){ - if(frm.doc.session_status=="Active"){ - frm.dashboard.set_headline_alert( - '
' + - '
' + - ' ' + - '
' + - '
' - ); + let msg,color; + if (frm.doc.session_status == "Active"){ + msg = __("Session Active"); + color = 'green'; } - else if(frm.doc.session_status=="Expired"){ - frm.dashboard.set_headline_alert( - '
' + - '
' + - ' ' + - '
' + - '
' - ); + else { + msg = __("Session Not Active. Save doc to login."); + color = 'red'; } + + frm.dashboard.set_headline_alert( + `
+
+ +
+
` + ); }, login: function(frm){ - if(frm.doc.consumer_key && frm.doc.consumer_secret){ + if (frm.doc.consumer_key && frm.doc.consumer_secret){ frappe.call({ doc: frm.doc, method: "get_authorize_url", diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.py b/erpnext/crm/doctype/twitter_settings/twitter_settings.py index f069a600dd..64f53b5eb0 100644 --- a/erpnext/crm/doctype/twitter_settings/twitter_settings.py +++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.py @@ -14,6 +14,7 @@ class TwitterSettings(Document): def get_authorize_url(self): callback_url = "{0}/?cmd=erpnext.crm.doctype.twitter_settings.twitter_settings.callback".format(frappe.utils.get_url()) auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret"), callback_url) + try: redirect_url = auth.get_authorization_url() return redirect_url @@ -28,16 +29,21 @@ class TwitterSettings(Document): 'oauth_token' : oauth_token, 'oauth_token_secret' : oauth_verifier } + try: auth.get_access_token(oauth_verifier) - self.db_set("oauth_token", auth.access_token) - self.db_set("oauth_secret", auth.access_token_secret) api = self.get_api() user = api.me() - self.db_set("account_name", user._json["screen_name"]) profile_pic = (user._json["profile_image_url"]).replace("_normal","") - self.db_set("profile_pic", profile_pic) - self.db_set("session_status", "Active") + + frappe.db.set_value(self.doctype, self.name, { + "oauth_token" : auth.access_token, + "oauth_secret" : auth.access_token_secret, + "account_name" : user._json["screen_name"], + "profile_pic" : profile_pic, + "session_status" : "Active" + }) + frappe.local.response["type"] = "redirect" frappe.local.response["location"] = get_url_to_form("Twitter Settings","Twitter Settings") except TweepError as e: @@ -64,6 +70,7 @@ class TwitterSettings(Document): media = get_file_path(media) api = self.get_api() media = api.media_upload(media) + return media.media_id def send_tweet(self, text, media_id=None): @@ -73,6 +80,7 @@ class TwitterSettings(Document): response = api.update_status(status = text, media_ids = [media_id]) else: response = api.update_status(status = text) + return response except TweepError as e: @@ -87,4 +95,4 @@ class TwitterSettings(Document): def callback(oauth_token, oauth_verifier): twitter_settings = frappe.get_single("Twitter Settings") twitter_settings.get_access_token(oauth_token,oauth_verifier) - frappe.db.commit() + frappe.db.commit() From 524eb6ce9b411f4a8c64bdef1626cf298fc2c5f5 Mon Sep 17 00:00:00 2001 From: Anupam K Date: Sat, 25 Apr 2020 00:35:26 +0530 Subject: [PATCH 5/5] freeze screen while posting and login --- erpnext/crm/doctype/linkedin_settings/linkedin_settings.js | 1 + erpnext/crm/doctype/social_media_post/social_media_post.js | 2 ++ erpnext/crm/doctype/twitter_settings/twitter_settings.js | 1 + 3 files changed, 4 insertions(+) diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js index b05b6021af..50b98e9ce1 100644 --- a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js +++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js @@ -55,6 +55,7 @@ frappe.ui.form.on('LinkedIn Settings', { }, login: function(frm){ if (frm.doc.consumer_key && frm.doc.consumer_secret){ + frappe.dom.freeze(); frappe.call({ doc: frm.doc, method: "get_authorization_url", diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.js b/erpnext/crm/doctype/social_media_post/social_media_post.js index c2a17d8bec..3a14f2d2e9 100644 --- a/erpnext/crm/doctype/social_media_post/social_media_post.js +++ b/erpnext/crm/doctype/social_media_post/social_media_post.js @@ -51,6 +51,7 @@ var add_post_btn = function(frm){ }); } var post = function(frm){ + frappe.dom.freeze(); frappe.call({ method: "erpnext.crm.doctype.social_media_post.social_media_post.publish", args: { @@ -59,6 +60,7 @@ var post = function(frm){ }, callback: function(r) { frm.reload_doc(); + frappe.dom.unfreeze(); } }) diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.js b/erpnext/crm/doctype/twitter_settings/twitter_settings.js index eae25206b3..8f9c419062 100644 --- a/erpnext/crm/doctype/twitter_settings/twitter_settings.js +++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.js @@ -36,6 +36,7 @@ frappe.ui.form.on('Twitter Settings', { }, login: function(frm){ if (frm.doc.consumer_key && frm.doc.consumer_secret){ + frappe.dom.freeze(); frappe.call({ doc: frm.doc, method: "get_authorize_url",