Browse Source

Linkedin, Twitter Integration

develop
Anupam K 5 years ago
parent
commit
d683dc250d
  1. 0
      erpnext/crm/doctype/linkedin_settings/__init__.py
  2. 66
      erpnext/crm/doctype/linkedin_settings/linkedin_settings.js
  3. 111
      erpnext/crm/doctype/linkedin_settings/linkedin_settings.json
  4. 157
      erpnext/crm/doctype/linkedin_settings/linkedin_settings.py
  5. 10
      erpnext/crm/doctype/linkedin_settings/test_linkedin_settings.py
  6. 0
      erpnext/crm/doctype/social_media_post/__init__.py
  7. 54
      erpnext/crm/doctype/social_media_post/social_media_post.js
  8. 157
      erpnext/crm/doctype/social_media_post/social_media_post.json
  9. 49
      erpnext/crm/doctype/social_media_post/social_media_post.py
  10. 10
      erpnext/crm/doctype/social_media_post/social_media_post_list.js
  11. 10
      erpnext/crm/doctype/social_media_post/test_social_media_post.py
  12. 0
      erpnext/crm/doctype/twitter_settings/__init__.py
  13. 10
      erpnext/crm/doctype/twitter_settings/test_twitter_settings.py
  14. 52
      erpnext/crm/doctype/twitter_settings/twitter_settings.js
  15. 101
      erpnext/crm/doctype/twitter_settings/twitter_settings.json
  16. 90
      erpnext/crm/doctype/twitter_settings/twitter_settings.py
  17. 3
      erpnext/hooks.py
  18. 6
      erpnext/selling/doctype/campaign/campaign_dashboard.py
  19. 1
      requirements.txt

0
erpnext/crm/doctype/linkedin_settings/__init__.py

66
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(
'<div class="row">' +
'<div class="col-xs-12">' +
'<span class="indicator whitespace-nowrap red'+ '' +'"><span class="hidden-xs">Session Not Active. Save doc to login.</span></span> ' +
'</div>' +
'</div>'
);
}
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(
'<div class="row">' +
'<div class="col-xs-12">' +
'<span class="indicator whitespace-nowrap '+ color +'"><span class="hidden-xs">' + msg + ' </span></span> ' +
'</div>' +
'</div>'
);
}
},
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");
}
});

111
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
}

157
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")

10
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

0
erpnext/crm/doctype/social_media_post/__init__.py

54
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 = '<div class="row">';
if(frm.doc.twitter){
let color = frm.doc.twitter_post_id ? "green" : "red";
let status = frm.doc.twitter_post_id ? "Posted" : "Not Posted";
html += '<div class="col-xs-6">' +
'<span class="indicator whitespace-nowrap '+ color +'"><span class="hidden-xs">Twitter : '+ status +'</span></span> ' +
'</div>' ;
}
if(frm.doc.linkedin){
let color = frm.doc.linkedin_post_id ? "green" : "red";
let status = frm.doc.linkedin_post_id ? "Posted" : "Not Posted";
html += '<div class="col-xs-6">' +
'<span class="indicator whitespace-nowrap '+ color +'"><span class="hidden-xs">LinkedIn : '+ status +'</span></span> ' +
'</div>' ;
}
html += '</div>';
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();
}
})
}

157
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
}

49
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()

10
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]];
}
}

10
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

0
erpnext/crm/doctype/twitter_settings/__init__.py

10
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

52
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(
'<div class="row">' +
'<div class="col-xs-12">' +
'<span class="indicator whitespace-nowrap green'+ '' +'"><span class="hidden-xs">Session Active</span></span> ' +
'</div>' +
'</div>'
);
}
else if(frm.doc.session_status=="Expired"){
frm.dashboard.set_headline_alert(
'<div class="row">' +
'<div class="col-xs-12">' +
'<span class="indicator whitespace-nowrap red'+ '' +'"><span class="hidden-xs">Session Not Active. Save doc to login.</span></span> ' +
'</div>' +
'</div>'
);
}
},
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");
}
});

101
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
}

90
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()

3
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',

6
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']
}
],
]
}

1
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
Loading…
Cancel
Save