mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-06 15:00:27 +00:00
Merge pull request #27114 from frappe-pr-bot/backport/develop/24664
refactor: social media post fixes
This commit is contained in:
@@ -2,8 +2,8 @@
|
|||||||
// For license information, please see license.txt
|
// For license information, please see license.txt
|
||||||
|
|
||||||
frappe.ui.form.on('LinkedIn Settings', {
|
frappe.ui.form.on('LinkedIn Settings', {
|
||||||
onload: function(frm){
|
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(
|
frappe.confirm(
|
||||||
__('Session not valid, Do you want to login?'),
|
__('Session not valid, Do you want to login?'),
|
||||||
function(){
|
function(){
|
||||||
@@ -14,8 +14,9 @@ frappe.ui.form.on('LinkedIn Settings', {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
frm.dashboard.set_headline(__("For more information, {0}.", [`<a target='_blank' href='https://docs.erpnext.com/docs/user/manual/en/CRM/linkedin-settings'>${__('Click here')}</a>`]));
|
||||||
},
|
},
|
||||||
refresh: function(frm){
|
refresh: function(frm) {
|
||||||
if (frm.doc.session_status=="Expired"){
|
if (frm.doc.session_status=="Expired"){
|
||||||
let msg = __("Session Not Active. Save doc to login.");
|
let msg = __("Session Not Active. Save doc to login.");
|
||||||
frm.dashboard.set_headline_alert(
|
frm.dashboard.set_headline_alert(
|
||||||
@@ -53,7 +54,7 @@ frappe.ui.form.on('LinkedIn Settings', {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
login: function(frm){
|
login: function(frm) {
|
||||||
if (frm.doc.consumer_key && frm.doc.consumer_secret){
|
if (frm.doc.consumer_key && frm.doc.consumer_secret){
|
||||||
frappe.dom.freeze();
|
frappe.dom.freeze();
|
||||||
frappe.call({
|
frappe.call({
|
||||||
@@ -67,7 +68,7 @@ frappe.ui.form.on('LinkedIn Settings', {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
after_save: function(frm){
|
after_save: function(frm) {
|
||||||
frm.trigger("login");
|
frm.trigger("login");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"actions": [],
|
"actions": [],
|
||||||
"creation": "2020-01-30 13:36:39.492931",
|
"creation": "2020-01-30 13:36:39.492931",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
|
"documentation": "https://docs.erpnext.com/docs/user/manual/en/CRM/linkedin-settings",
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
@@ -87,7 +88,7 @@
|
|||||||
],
|
],
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-04-16 23:22:51.966397",
|
"modified": "2021-02-18 15:19:21.920725",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "CRM",
|
"module": "CRM",
|
||||||
"name": "LinkedIn Settings",
|
"name": "LinkedIn Settings",
|
||||||
|
|||||||
@@ -3,11 +3,12 @@
|
|||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import frappe, requests, json
|
import frappe
|
||||||
|
import requests
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.utils import get_site_url, get_url_to_form, get_link_to_form
|
from frappe.utils import get_url_to_form
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils.file_manager import get_file, get_file_path
|
from frappe.utils.file_manager import get_file_path
|
||||||
from six.moves.urllib.parse import urlencode
|
from six.moves.urllib.parse import urlencode
|
||||||
|
|
||||||
class LinkedInSettings(Document):
|
class LinkedInSettings(Document):
|
||||||
@@ -42,11 +43,7 @@ class LinkedInSettings(Document):
|
|||||||
self.db_set("access_token", response["access_token"])
|
self.db_set("access_token", response["access_token"])
|
||||||
|
|
||||||
def get_member_profile(self):
|
def get_member_profile(self):
|
||||||
headers = {
|
response = requests.get(url="https://api.linkedin.com/v2/me", headers=self.get_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())
|
response = frappe.parse_json(response.content.decode())
|
||||||
|
|
||||||
frappe.db.set_value(self.doctype, self.name, {
|
frappe.db.set_value(self.doctype, self.name, {
|
||||||
@@ -55,16 +52,16 @@ class LinkedInSettings(Document):
|
|||||||
"session_status": "Active"
|
"session_status": "Active"
|
||||||
})
|
})
|
||||||
frappe.local.response["type"] = "redirect"
|
frappe.local.response["type"] = "redirect"
|
||||||
frappe.local.response["location"] = get_url_to_form("LinkedIn Settings","LinkedIn Settings")
|
frappe.local.response["location"] = get_url_to_form("LinkedIn Settings", "LinkedIn Settings")
|
||||||
|
|
||||||
def post(self, text, media=None):
|
def post(self, text, title, media=None):
|
||||||
if not media:
|
if not media:
|
||||||
return self.post_text(text)
|
return self.post_text(text, title)
|
||||||
else:
|
else:
|
||||||
media_id = self.upload_image(media)
|
media_id = self.upload_image(media)
|
||||||
|
|
||||||
if media_id:
|
if media_id:
|
||||||
return self.post_text(text, media_id=media_id)
|
return self.post_text(text, title, media_id=media_id)
|
||||||
else:
|
else:
|
||||||
frappe.log_error("Failed to upload media.","LinkedIn Upload Error")
|
frappe.log_error("Failed to upload media.","LinkedIn Upload Error")
|
||||||
|
|
||||||
@@ -82,9 +79,7 @@ class LinkedInSettings(Document):
|
|||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
headers = {
|
headers = self.get_headers()
|
||||||
"Authorization": "Bearer {}".format(self.access_token)
|
|
||||||
}
|
|
||||||
response = self.http_post(url=register_url, body=body, headers=headers)
|
response = self.http_post(url=register_url, body=body, headers=headers)
|
||||||
|
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
@@ -100,24 +95,33 @@ class LinkedInSettings(Document):
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def post_text(self, text, media_id=None):
|
def post_text(self, text, title, media_id=None):
|
||||||
url = "https://api.linkedin.com/v2/shares"
|
url = "https://api.linkedin.com/v2/shares"
|
||||||
headers = {
|
headers = self.get_headers()
|
||||||
"X-Restli-Protocol-Version": "2.0.0",
|
headers["X-Restli-Protocol-Version"] = "2.0.0"
|
||||||
"Authorization": "Bearer {}".format(self.access_token),
|
headers["Content-Type"] = "application/json; charset=UTF-8"
|
||||||
"Content-Type": "application/json; charset=UTF-8"
|
|
||||||
}
|
|
||||||
body = {
|
body = {
|
||||||
"distribution": {
|
"distribution": {
|
||||||
"linkedInDistributionTarget": {}
|
"linkedInDistributionTarget": {}
|
||||||
},
|
},
|
||||||
"owner":"urn:li:organization:{0}".format(self.company_id),
|
"owner":"urn:li:organization:{0}".format(self.company_id),
|
||||||
"subject": "Test Share Subject",
|
"subject": title,
|
||||||
"text": {
|
"text": {
|
||||||
"text": text
|
"text": text
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reference_url = self.get_reference_url(text)
|
||||||
|
if reference_url:
|
||||||
|
body["content"] = {
|
||||||
|
"contentEntities": [
|
||||||
|
{
|
||||||
|
"entityLocation": reference_url
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
if media_id:
|
if media_id:
|
||||||
body["content"]= {
|
body["content"]= {
|
||||||
"contentEntities": [{
|
"contentEntities": [{
|
||||||
@@ -141,20 +145,60 @@ class LinkedInSettings(Document):
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
content = json.loads(response.content)
|
self.api_error(response)
|
||||||
|
|
||||||
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
|
return response
|
||||||
|
|
||||||
|
def get_headers(self):
|
||||||
|
return {
|
||||||
|
"Authorization": "Bearer {}".format(self.access_token)
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_reference_url(self, text):
|
||||||
|
import re
|
||||||
|
regex_url = r"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+"
|
||||||
|
urls = re.findall(regex_url, text)
|
||||||
|
if urls:
|
||||||
|
return urls[0]
|
||||||
|
|
||||||
|
def delete_post(self, post_id):
|
||||||
|
try:
|
||||||
|
response = requests.delete(url="https://api.linkedin.com/v2/shares/urn:li:share:{0}".format(post_id), headers=self.get_headers())
|
||||||
|
if response.status_code !=200:
|
||||||
|
raise
|
||||||
|
except Exception:
|
||||||
|
self.api_error(response)
|
||||||
|
|
||||||
|
def get_post(self, post_id):
|
||||||
|
url = "https://api.linkedin.com/v2/organizationalEntityShareStatistics?q=organizationalEntity&organizationalEntity=urn:li:organization:{0}&shares[0]=urn:li:share:{1}".format(self.company_id, post_id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(url=url, headers=self.get_headers())
|
||||||
|
if response.status_code !=200:
|
||||||
|
raise
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
self.api_error(response)
|
||||||
|
|
||||||
|
response = frappe.parse_json(response.content.decode())
|
||||||
|
if len(response.elements):
|
||||||
|
return response.elements[0]
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def api_error(self, response):
|
||||||
|
content = frappe.parse_json(response.content.decode())
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
@frappe.whitelist(allow_guest=True)
|
||||||
def callback(code=None, error=None, error_description=None):
|
def callback(code=None, error=None, error_description=None):
|
||||||
if not error:
|
if not error:
|
||||||
|
|||||||
@@ -1,67 +1,139 @@
|
|||||||
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
// For license information, please see license.txt
|
// For license information, please see license.txt
|
||||||
frappe.ui.form.on('Social Media Post', {
|
frappe.ui.form.on('Social Media Post', {
|
||||||
validate: function(frm){
|
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."))
|
frappe.throw(__("Select atleast one Social Media Platform to Share on."));
|
||||||
}
|
}
|
||||||
if (frm.doc.scheduled_time) {
|
if (frm.doc.scheduled_time) {
|
||||||
let scheduled_time = new Date(frm.doc.scheduled_time);
|
let scheduled_time = new Date(frm.doc.scheduled_time);
|
||||||
let date_time = new Date();
|
let date_time = new Date();
|
||||||
if (scheduled_time.getTime() < date_time.getTime()){
|
if (scheduled_time.getTime() < date_time.getTime()) {
|
||||||
frappe.throw(__("Invalid Scheduled Time"));
|
frappe.throw(__("Scheduled Time must be a future time."));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (frm.doc.text?.length > 280){
|
frm.trigger('validate_tweet_length');
|
||||||
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='';
|
text: function(frm) {
|
||||||
if (frm.doc.twitter){
|
if (frm.doc.text) {
|
||||||
let color = frm.doc.twitter_post_id ? "green" : "red";
|
frm.set_df_property('text', 'description', `${frm.doc.text.length}/280`);
|
||||||
let status = frm.doc.twitter_post_id ? "Posted" : "Not Posted";
|
frm.refresh_field('text');
|
||||||
html += `<div class="col-xs-6">
|
frm.trigger('validate_tweet_length');
|
||||||
<span class="indicator whitespace-nowrap ${color}"><span>Twitter : ${status} </span></span>
|
}
|
||||||
</div>` ;
|
},
|
||||||
}
|
|
||||||
if (frm.doc.linkedin){
|
validate_tweet_length: function(frm) {
|
||||||
let color = frm.doc.linkedin_post_id ? "green" : "red";
|
if (frm.doc.text && frm.doc.text.length > 280) {
|
||||||
let status = frm.doc.linkedin_post_id ? "Posted" : "Not Posted";
|
frappe.throw(__("Tweet length Must be less than 280."));
|
||||||
html += `<div class="col-xs-6">
|
}
|
||||||
<span class="indicator whitespace-nowrap ${color}"><span>LinkedIn : ${status} </span></span>
|
},
|
||||||
</div>` ;
|
|
||||||
}
|
onload: function(frm) {
|
||||||
html = `<div class="row">${html}</div>`;
|
frm.trigger('make_dashboard');
|
||||||
frm.dashboard.set_headline_alert(html);
|
},
|
||||||
}
|
|
||||||
}
|
make_dashboard: function(frm) {
|
||||||
|
if (frm.doc.post_status == "Posted") {
|
||||||
|
frappe.call({
|
||||||
|
doc: frm.doc,
|
||||||
|
method: 'get_post',
|
||||||
|
freeze: true,
|
||||||
|
callback: (r) => {
|
||||||
|
if (!r.message) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let datasets = [], colors = [];
|
||||||
|
if (r.message && r.message.twitter) {
|
||||||
|
colors.push('#1DA1F2');
|
||||||
|
datasets.push({
|
||||||
|
name: 'Twitter',
|
||||||
|
values: [r.message.twitter.favorite_count, r.message.twitter.retweet_count]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (r.message && r.message.linkedin) {
|
||||||
|
colors.push('#0077b5');
|
||||||
|
datasets.push({
|
||||||
|
name: 'LinkedIn',
|
||||||
|
values: [r.message.linkedin.totalShareStatistics.likeCount, r.message.linkedin.totalShareStatistics.shareCount]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (datasets.length) {
|
||||||
|
frm.dashboard.render_graph({
|
||||||
|
data: {
|
||||||
|
labels: ['Likes', 'Retweets/Shares'],
|
||||||
|
datasets: datasets
|
||||||
|
},
|
||||||
|
|
||||||
|
title: __("Post Metrics"),
|
||||||
|
type: 'bar',
|
||||||
|
height: 300,
|
||||||
|
colors: colors
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
refresh: function(frm) {
|
||||||
|
frm.trigger('text');
|
||||||
|
|
||||||
|
if (frm.doc.docstatus === 1) {
|
||||||
|
if (!['Posted', 'Deleted'].includes(frm.doc.post_status)) {
|
||||||
|
frm.trigger('add_post_btn');
|
||||||
|
}
|
||||||
|
if (frm.doc.post_status !='Deleted') {
|
||||||
|
frm.add_custom_button(('Delete Post'), function() {
|
||||||
|
frappe.confirm(__('Are you sure want to delete the Post from Social Media platforms?'),
|
||||||
|
function() {
|
||||||
|
frappe.call({
|
||||||
|
doc: frm.doc,
|
||||||
|
method: 'delete_post',
|
||||||
|
freeze: true,
|
||||||
|
callback: () => {
|
||||||
|
frm.reload_doc();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frm.doc.post_status !='Deleted') {
|
||||||
|
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 += `<div class="col-xs-6">
|
||||||
|
<span class="indicator whitespace-nowrap ${color}"><span>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>LinkedIn : ${status} </span></span>
|
||||||
|
</div>` ;
|
||||||
|
}
|
||||||
|
html = `<div class="row">${html}</div>`;
|
||||||
|
frm.dashboard.set_headline_alert(html);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
add_post_btn: function(frm) {
|
||||||
|
frm.add_custom_button(__('Post Now'), function() {
|
||||||
|
frappe.call({
|
||||||
|
doc: frm.doc,
|
||||||
|
method: 'post',
|
||||||
|
freeze: true,
|
||||||
|
callback: function() {
|
||||||
|
frm.reload_doc();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
var add_post_btn = function(frm){
|
|
||||||
frm.add_custom_button(('Post Now'), function(){
|
|
||||||
post(frm);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
var post = function(frm){
|
|
||||||
frappe.dom.freeze();
|
|
||||||
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();
|
|
||||||
frappe.dom.unfreeze();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,9 +3,11 @@
|
|||||||
"autoname": "format: CRM-SMP-{YYYY}-{MM}-{DD}-{###}",
|
"autoname": "format: CRM-SMP-{YYYY}-{MM}-{DD}-{###}",
|
||||||
"creation": "2020-01-30 11:53:13.872864",
|
"creation": "2020-01-30 11:53:13.872864",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
|
"documentation": "https://docs.erpnext.com/docs/user/manual/en/CRM/social-media-post",
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
|
"title",
|
||||||
"campaign_name",
|
"campaign_name",
|
||||||
"scheduled_time",
|
"scheduled_time",
|
||||||
"post_status",
|
"post_status",
|
||||||
@@ -30,32 +32,24 @@
|
|||||||
"fieldname": "text",
|
"fieldname": "text",
|
||||||
"fieldtype": "Small Text",
|
"fieldtype": "Small Text",
|
||||||
"label": "Tweet",
|
"label": "Tweet",
|
||||||
"mandatory_depends_on": "eval:doc.twitter ==1",
|
"mandatory_depends_on": "eval:doc.twitter ==1"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "image",
|
"fieldname": "image",
|
||||||
"fieldtype": "Attach Image",
|
"fieldtype": "Attach Image",
|
||||||
"label": "Image",
|
"label": "Image"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "1",
|
||||||
"fieldname": "twitter",
|
"fieldname": "twitter",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Twitter",
|
"label": "Twitter"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "1",
|
||||||
"fieldname": "linkedin",
|
"fieldname": "linkedin",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "LinkedIn",
|
"label": "LinkedIn"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "amended_from",
|
"fieldname": "amended_from",
|
||||||
@@ -64,27 +58,22 @@
|
|||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "Social Media Post",
|
"options": "Social Media Post",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1,
|
"read_only": 1
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:doc.twitter ==1",
|
"depends_on": "eval:doc.twitter ==1",
|
||||||
"fieldname": "content",
|
"fieldname": "content",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Twitter",
|
"label": "Twitter"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
"fieldname": "post_status",
|
"fieldname": "post_status",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Post Status",
|
"label": "Post Status",
|
||||||
"options": "\nScheduled\nPosted\nError",
|
"no_copy": 1,
|
||||||
"read_only": 1,
|
"options": "\nScheduled\nPosted\nCancelled\nDeleted\nError",
|
||||||
"show_days": 1,
|
"read_only": 1
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
@@ -92,9 +81,8 @@
|
|||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"label": "Twitter Post Id",
|
"label": "Twitter Post Id",
|
||||||
"read_only": 1,
|
"no_copy": 1,
|
||||||
"show_days": 1,
|
"read_only": 1
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
@@ -102,82 +90,69 @@
|
|||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"label": "LinkedIn Post Id",
|
"label": "LinkedIn Post Id",
|
||||||
"read_only": 1,
|
"no_copy": 1,
|
||||||
"show_days": 1,
|
"read_only": 1
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "campaign_name",
|
"fieldname": "campaign_name",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Campaign",
|
"label": "Campaign",
|
||||||
"options": "Campaign",
|
"options": "Campaign"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_6",
|
"fieldname": "column_break_6",
|
||||||
"fieldtype": "Column Break",
|
"fieldtype": "Column Break",
|
||||||
"label": "Share On",
|
"label": "Share On"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_14",
|
"fieldname": "column_break_14",
|
||||||
"fieldtype": "Column Break",
|
"fieldtype": "Column Break"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "tweet_preview",
|
"fieldname": "tweet_preview",
|
||||||
"fieldtype": "HTML",
|
"fieldtype": "HTML"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"collapsible": 1,
|
"collapsible": 1,
|
||||||
"depends_on": "eval:doc.linkedin==1",
|
"depends_on": "eval:doc.linkedin==1",
|
||||||
"fieldname": "linkedin_section",
|
"fieldname": "linkedin_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "LinkedIn",
|
"label": "LinkedIn"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"collapsible": 1,
|
"collapsible": 1,
|
||||||
"fieldname": "attachments_section",
|
"fieldname": "attachments_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Attachments",
|
"label": "Attachments"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "linkedin_post",
|
"fieldname": "linkedin_post",
|
||||||
"fieldtype": "Text",
|
"fieldtype": "Text",
|
||||||
"label": "Post",
|
"label": "Post",
|
||||||
"mandatory_depends_on": "eval:doc.linkedin ==1",
|
"mandatory_depends_on": "eval:doc.linkedin ==1"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_15",
|
"fieldname": "column_break_15",
|
||||||
"fieldtype": "Column Break",
|
"fieldtype": "Column Break"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
"fieldname": "scheduled_time",
|
"fieldname": "scheduled_time",
|
||||||
"fieldtype": "Datetime",
|
"fieldtype": "Datetime",
|
||||||
"label": "Scheduled Time",
|
"label": "Scheduled Time",
|
||||||
"read_only_depends_on": "eval:doc.post_status == \"Posted\"",
|
"read_only_depends_on": "eval:doc.post_status == \"Posted\""
|
||||||
"show_days": 1,
|
},
|
||||||
"show_seconds": 1
|
{
|
||||||
|
"fieldname": "title",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Title",
|
||||||
|
"reqd": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-06-14 10:31:33.961381",
|
"modified": "2021-04-14 14:24:59.821223",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "CRM",
|
"module": "CRM",
|
||||||
"name": "Social Media Post",
|
"name": "Social Media Post",
|
||||||
@@ -228,5 +203,6 @@
|
|||||||
],
|
],
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
"title_field": "title",
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
@@ -10,17 +10,51 @@ import datetime
|
|||||||
|
|
||||||
class SocialMediaPost(Document):
|
class SocialMediaPost(Document):
|
||||||
def validate(self):
|
def validate(self):
|
||||||
|
if (not self.twitter and not self.linkedin):
|
||||||
|
frappe.throw(_("Select atleast one Social Media Platform to Share on."))
|
||||||
|
|
||||||
if self.scheduled_time:
|
if self.scheduled_time:
|
||||||
current_time = frappe.utils.now_datetime()
|
current_time = frappe.utils.now_datetime()
|
||||||
scheduled_time = frappe.utils.get_datetime(self.scheduled_time)
|
scheduled_time = frappe.utils.get_datetime(self.scheduled_time)
|
||||||
if scheduled_time < current_time:
|
if scheduled_time < current_time:
|
||||||
frappe.throw(_("Invalid Scheduled Time"))
|
frappe.throw(_("Scheduled Time must be a future time."))
|
||||||
|
|
||||||
|
if self.text and len(self.text) > 280:
|
||||||
|
frappe.throw(_("Tweet length must be less than 280."))
|
||||||
|
|
||||||
def submit(self):
|
def submit(self):
|
||||||
if self.scheduled_time:
|
if self.scheduled_time:
|
||||||
self.post_status = "Scheduled"
|
self.post_status = "Scheduled"
|
||||||
super(SocialMediaPost, self).submit()
|
super(SocialMediaPost, self).submit()
|
||||||
|
|
||||||
|
def on_cancel(self):
|
||||||
|
self.db_set('post_status', 'Cancelled')
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def delete_post(self):
|
||||||
|
if self.twitter and self.twitter_post_id:
|
||||||
|
twitter = frappe.get_doc("Twitter Settings")
|
||||||
|
twitter.delete_tweet(self.twitter_post_id)
|
||||||
|
|
||||||
|
if self.linkedin and self.linkedin_post_id:
|
||||||
|
linkedin = frappe.get_doc("LinkedIn Settings")
|
||||||
|
linkedin.delete_post(self.linkedin_post_id)
|
||||||
|
|
||||||
|
self.db_set('post_status', 'Deleted')
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_post(self):
|
||||||
|
response = {}
|
||||||
|
if self.linkedin and self.linkedin_post_id:
|
||||||
|
linkedin = frappe.get_doc("LinkedIn Settings")
|
||||||
|
response['linkedin'] = linkedin.get_post(self.linkedin_post_id)
|
||||||
|
if self.twitter and self.twitter_post_id:
|
||||||
|
twitter = frappe.get_doc("Twitter Settings")
|
||||||
|
response['twitter'] = twitter.get_tweet(self.twitter_post_id)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
def post(self):
|
def post(self):
|
||||||
try:
|
try:
|
||||||
if self.twitter and not self.twitter_post_id:
|
if self.twitter and not self.twitter_post_id:
|
||||||
@@ -29,28 +63,22 @@ class SocialMediaPost(Document):
|
|||||||
self.db_set("twitter_post_id", twitter_post.id)
|
self.db_set("twitter_post_id", twitter_post.id)
|
||||||
if self.linkedin and not self.linkedin_post_id:
|
if self.linkedin and not self.linkedin_post_id:
|
||||||
linkedin = frappe.get_doc("LinkedIn Settings")
|
linkedin = frappe.get_doc("LinkedIn Settings")
|
||||||
linkedin_post = linkedin.post(self.linkedin_post, self.image)
|
linkedin_post = linkedin.post(self.linkedin_post, self.title, self.image)
|
||||||
self.db_set("linkedin_post_id", linkedin_post.headers['X-RestLi-Id'].split(":")[-1])
|
self.db_set("linkedin_post_id", linkedin_post.headers['X-RestLi-Id'])
|
||||||
self.db_set("post_status", "Posted")
|
self.db_set("post_status", "Posted")
|
||||||
|
|
||||||
except:
|
except:
|
||||||
self.db_set("post_status", "Error")
|
self.db_set("post_status", "Error")
|
||||||
title = _("Error while POSTING {0}").format(self.name)
|
title = _("Error while POSTING {0}").format(self.name)
|
||||||
traceback = frappe.get_traceback()
|
frappe.log_error(message=frappe.get_traceback(), title=title)
|
||||||
frappe.log_error(message=traceback , title=title)
|
|
||||||
|
|
||||||
def process_scheduled_social_media_posts():
|
def process_scheduled_social_media_posts():
|
||||||
posts = frappe.get_list("Social Media Post", filters={"post_status": "Scheduled", "docstatus":1}, fields= ["name", "scheduled_time","post_status"])
|
posts = frappe.get_list("Social Media Post", filters={"post_status": "Scheduled", "docstatus":1}, fields= ["name", "scheduled_time"])
|
||||||
start = frappe.utils.now_datetime()
|
start = frappe.utils.now_datetime()
|
||||||
end = start + datetime.timedelta(minutes=10)
|
end = start + datetime.timedelta(minutes=10)
|
||||||
for post in posts:
|
for post in posts:
|
||||||
if post.scheduled_time:
|
if post.scheduled_time:
|
||||||
post_time = frappe.utils.get_datetime(post.scheduled_time)
|
post_time = frappe.utils.get_datetime(post.scheduled_time)
|
||||||
if post_time > start and post_time <= end:
|
if post_time > start and post_time <= end:
|
||||||
publish('Social Media Post', post.name)
|
sm_post = frappe.get_doc('Social Media Post', post.name)
|
||||||
|
sm_post.post()
|
||||||
@frappe.whitelist()
|
|
||||||
def publish(doctype, name):
|
|
||||||
sm_post = frappe.get_doc(doctype, name)
|
|
||||||
sm_post.post()
|
|
||||||
frappe.db.commit()
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
frappe.listview_settings['Social Media Post'] = {
|
frappe.listview_settings['Social Media Post'] = {
|
||||||
add_fields: ["status","post_status"],
|
add_fields: ["status", "post_status"],
|
||||||
get_indicator: function(doc) {
|
get_indicator: function(doc) {
|
||||||
return [__(doc.post_status), {
|
return [__(doc.post_status), {
|
||||||
"Scheduled": "orange",
|
"Scheduled": "orange",
|
||||||
"Posted": "green",
|
"Posted": "green",
|
||||||
"Error": "red"
|
"Error": "red",
|
||||||
}[doc.post_status]];
|
"Deleted": "red"
|
||||||
}
|
}[doc.post_status]];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// For license information, please see license.txt
|
// For license information, please see license.txt
|
||||||
|
|
||||||
frappe.ui.form.on('Twitter Settings', {
|
frappe.ui.form.on('Twitter Settings', {
|
||||||
onload: function(frm){
|
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(
|
frappe.confirm(
|
||||||
__('Session not valid, Do you want to login?'),
|
__('Session not valid, Do you want to login?'),
|
||||||
@@ -14,10 +14,11 @@ frappe.ui.form.on('Twitter Settings', {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
frm.dashboard.set_headline(__("For more information, {0}.", [`<a target='_blank' href='https://docs.erpnext.com/docs/user/manual/en/CRM/twitter-settings'>${__('Click here')}</a>`]));
|
||||||
},
|
},
|
||||||
refresh: function(frm){
|
refresh: function(frm) {
|
||||||
let msg, color, flag=false;
|
let msg, color, flag=false;
|
||||||
if (frm.doc.session_status == "Active"){
|
if (frm.doc.session_status == "Active") {
|
||||||
msg = __("Session Active");
|
msg = __("Session Active");
|
||||||
color = 'green';
|
color = 'green';
|
||||||
flag = true;
|
flag = true;
|
||||||
@@ -28,7 +29,7 @@ frappe.ui.form.on('Twitter Settings', {
|
|||||||
flag = true;
|
flag = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flag){
|
if (flag) {
|
||||||
frm.dashboard.set_headline_alert(
|
frm.dashboard.set_headline_alert(
|
||||||
`<div class="row">
|
`<div class="row">
|
||||||
<div class="col-xs-12">
|
<div class="col-xs-12">
|
||||||
@@ -38,7 +39,7 @@ frappe.ui.form.on('Twitter Settings', {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
login: function(frm){
|
login: function(frm) {
|
||||||
if (frm.doc.consumer_key && frm.doc.consumer_secret){
|
if (frm.doc.consumer_key && frm.doc.consumer_secret){
|
||||||
frappe.dom.freeze();
|
frappe.dom.freeze();
|
||||||
frappe.call({
|
frappe.call({
|
||||||
@@ -52,7 +53,7 @@ frappe.ui.form.on('Twitter Settings', {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
after_save: function(frm){
|
after_save: function(frm) {
|
||||||
frm.trigger("login");
|
frm.trigger("login");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"actions": [],
|
"actions": [],
|
||||||
"creation": "2020-01-30 10:29:08.562108",
|
"creation": "2020-01-30 10:29:08.562108",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
|
"documentation": "https://docs.erpnext.com/docs/user/manual/en/CRM/twitter-settings",
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
@@ -77,7 +78,7 @@
|
|||||||
"image_field": "profile_pic",
|
"image_field": "profile_pic",
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-05-13 17:50:47.934776",
|
"modified": "2021-02-18 15:18:07.900031",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "CRM",
|
"module": "CRM",
|
||||||
"name": "Twitter Settings",
|
"name": "Twitter Settings",
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ class TwitterSettings(Document):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
auth.get_access_token(oauth_verifier)
|
auth.get_access_token(oauth_verifier)
|
||||||
api = self.get_api(auth.access_token, auth.access_token_secret)
|
self.access_token = auth.access_token
|
||||||
|
self.access_token_secret = auth.access_token_secret
|
||||||
|
api = self.get_api()
|
||||||
user = api.me()
|
user = api.me()
|
||||||
profile_pic = (user._json["profile_image_url"]).replace("_normal","")
|
profile_pic = (user._json["profile_image_url"]).replace("_normal","")
|
||||||
|
|
||||||
@@ -50,11 +52,11 @@ class TwitterSettings(Document):
|
|||||||
frappe.msgprint(_("Error! Failed to get access token."))
|
frappe.msgprint(_("Error! Failed to get access token."))
|
||||||
frappe.throw(_('Invalid Consumer Key or Consumer Secret Key'))
|
frappe.throw(_('Invalid Consumer Key or Consumer Secret Key'))
|
||||||
|
|
||||||
def get_api(self, access_token, access_token_secret):
|
def get_api(self):
|
||||||
# authentication of consumer key and secret
|
# authentication of consumer key and secret
|
||||||
auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret"))
|
auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret"))
|
||||||
# authentication of access token and secret
|
# authentication of access token and secret
|
||||||
auth.set_access_token(access_token, access_token_secret)
|
auth.set_access_token(self.access_token, self.access_token_secret)
|
||||||
|
|
||||||
return tweepy.API(auth)
|
return tweepy.API(auth)
|
||||||
|
|
||||||
@@ -68,13 +70,13 @@ class TwitterSettings(Document):
|
|||||||
|
|
||||||
def upload_image(self, media):
|
def upload_image(self, media):
|
||||||
media = get_file_path(media)
|
media = get_file_path(media)
|
||||||
api = self.get_api(self.access_token, self.access_token_secret)
|
api = self.get_api()
|
||||||
media = api.media_upload(media)
|
media = api.media_upload(media)
|
||||||
|
|
||||||
return media.media_id
|
return media.media_id
|
||||||
|
|
||||||
def send_tweet(self, text, media_id=None):
|
def send_tweet(self, text, media_id=None):
|
||||||
api = self.get_api(self.access_token, self.access_token_secret)
|
api = self.get_api()
|
||||||
try:
|
try:
|
||||||
if media_id:
|
if media_id:
|
||||||
response = api.update_status(status = text, media_ids = [media_id])
|
response = api.update_status(status = text, media_ids = [media_id])
|
||||||
@@ -84,12 +86,32 @@ class TwitterSettings(Document):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
except TweepError as e:
|
except TweepError as e:
|
||||||
content = json.loads(e.response.content)
|
self.api_error(e)
|
||||||
content = content["errors"][0]
|
|
||||||
if e.response.status_code == 401:
|
def delete_tweet(self, tweet_id):
|
||||||
self.db_set("session_status", "Expired")
|
api = self.get_api()
|
||||||
frappe.db.commit()
|
try:
|
||||||
frappe.throw(content["message"],title="Twitter Error {0} {1}".format(e.response.status_code, e.response.reason))
|
api.destroy_status(tweet_id)
|
||||||
|
except TweepError as e:
|
||||||
|
self.api_error(e)
|
||||||
|
|
||||||
|
def get_tweet(self, tweet_id):
|
||||||
|
api = self.get_api()
|
||||||
|
try:
|
||||||
|
response = api.get_status(tweet_id, trim_user=True, include_entities=True)
|
||||||
|
except TweepError as e:
|
||||||
|
self.api_error(e)
|
||||||
|
|
||||||
|
return response._json
|
||||||
|
|
||||||
|
def api_error(self, 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(allow_guest=True)
|
@frappe.whitelist(allow_guest=True)
|
||||||
def callback(oauth_token = None, oauth_verifier = None):
|
def callback(oauth_token = None, oauth_verifier = None):
|
||||||
|
|||||||
Reference in New Issue
Block a user