-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcontactcard.py
280 lines (223 loc) · 8.17 KB
/
contactcard.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
# -*- coding: utf-8 -*-
"""One-file Flask app that displays Salesforce Contact sObject records.
Additionally, the records may be edited and have the record changes
automatically synced back to Salesforce. This app requires a PostGreSQL
database that is connected to Salesforce via Heroku Connect.
"""
from base64 import b64encode
from functools import wraps
from os import environ, urandom
from re import search
from typing import Callable, Optional, Union
from flask import flash, Flask, redirect, render_template, request, url_for
from flask_sqlalchemy import SQLAlchemy
from flask_wtf import FlaskForm
from sqlalchemy.sql import text
from wtforms import StringField
from wtforms.validators import DataRequired, Email
class Config:
"""This class stores configuration variables for the Flask application.
Attributes
----------
SECRET_KEY : str
Secret used to generate security-based data, such as CSRF tokens
(default: random 64-character string)
SQLALCHEMY_DATABASE_URI : str
URI of the database this app uses. This variable is provided by the
Heroku PostGreSQL add-on.
SQLALCHEMY_TRACK_MODIFICATIONS : bool
This variable is used by SQLAlchemy. SQLAlchemy recommends setting it
to False if it is not explicitly needed, as the feature has a side
effect of slowing down transactions.
HEROKU_CONNECT_INITED : bool
Whether or not Heroku Connect has been initialized. The app will update
this value as necessary.
"""
SECRET_KEY: str = environ.get(
'SECRET_KEY',
b64encode(urandom(48)).decode('utf-8')
)
SQLALCHEMY_DATABASE_URI: str = environ['DATABASE_URL']
SQLALCHEMY_TRACK_MODIFICATIONS: bool = False
HEROKU_CONNECT_INITED = False
# Flask application, configured
app = Flask(__name__)
app.config.from_object(Config())
# Database, configured
db = SQLAlchemy()
db.init_app(app)
class ContactModel(db.Model): # type: ignore
"""Model used to interface with Heroku Connect Contact table. Column types
are left blank so as to blindly assume the characteristics placed on them
by the Heroku Connect mapping.
Attributes
----------
__tablename__ : str
Table name, as set by Heroku Connect
__table_args__ : dict
Table arguments. The only argument the app must pass is the schema
namespace, which directs SQLAlchemy to the Heroku Connect schema when
connecting the model to the database table.
id : int
PostGreSQL record id
sfid : str
Salesforce 18-character record ID
firstname : str
First name
lastname : str
Last name
title : str, optional
Company title
email : str
Valid email address (enforced by app and Salesforce)
phone : str
Valid phone number (enforced by Salesforce)
"""
__tablename__ = 'contact'
__table_args__ = {'schema': 'salesforce'}
id = db.Column(primary_key=True)
sfid = db.Column()
firstname = db.Column()
lastname = db.Column()
title = db.Column()
email = db.Column()
phone = db.Column()
class ContactForm(FlaskForm):
"""Form used to render and edit contact information.
Attributes
----------
firstname : :obj:`StringField`
First-name form-field
lastname : :obj:`StringField`
Last-name form-field
title : :obj:`StringField`, optional
Company-title form-field
email : :obj:`StringField`
Email form-field
phone : :obj:`StringField`
Phone form-field
"""
firstname = StringField(
label="First name",
validators=[DataRequired()]
)
lastname = StringField(
label="Last name",
validators=[DataRequired()]
)
title = StringField(label="Title")
email = StringField(
label="Email Address",
validators=[DataRequired(), Email()]
)
phone = StringField(
label="Phone Number",
validators=[DataRequired()]
)
def salesforce_connection_exists() -> bool:
"""Tests whether Heroku Connect has been configured.
"""
if not app.config['HEROKU_CONNECT_INITED']:
with db.engine.connect() as conn:
# Search for the 'salesforce' schema
statement = text(
"SELECT COUNT(*) "
"FROM information_schema.schemata "
"WHERE schema_name = 'salesforce';"
)
# Unpack the proxy and extract single result (count)
results_proxy = conn.execute(statement)
results = [result for result in results_proxy]
result = results.pop()
schema_exists = bool(result['count'])
if schema_exists:
app.config['HEROKU_CONNECT_INITED'] = True
return app.config['HEROKU_CONNECT_INITED']
def heroku_connect_required(func: Callable) -> Callable:
"""View decorator that checks whether Heroku Connect has been configured
and redirects the user to the welcome page if the connection has not been
set up.
"""
@wraps(func)
def decorated_view(*args, **kwargs) -> Callable:
"""Returns original view method if Heroku Connect is setup.
If not set up, redirects to the welcome page.
"""
if salesforce_connection_exists():
return func(*args, **kwargs)
else:
return redirect(url_for('welcome'))
return decorated_view
@app.before_request
def force_https() -> redirect:
"""Redirects all HTTP requests to HTTPS."""
if request.url.startswith('http://'):
url = request.url.replace('http://', 'https://', 1)
return redirect(url, code=301)
@app.route('/welcome')
def welcome() -> Union[redirect, render_template]:
"""Renders the welcome page to the app.
Only available if Heroku Connect has not yet been set up.
"""
if salesforce_connection_exists():
return redirect(url_for('index'))
app_name_match = search(
'^https?://([\w-]*).herokuapp.com.*',
request.url_root
)
app_name = app_name_match.group(1)
heroku_resource_url = (
f"https://dashboard.heroku.com/apps/{app_name}/resources"
)
return render_template(
'welcome.html',
heroku_resource_url=heroku_resource_url
)
@app.route('/')
@heroku_connect_required
def index() -> render_template:
"""Renders page for URL root (index)."""
# Get all Contact sObject records and pass to the renderer
contacts = ContactModel.query.all()
return render_template('index.html', contacts=contacts)
@app.route('/contact/<string:sfid>', methods=['GET', 'POST'])
@heroku_connect_required
def contact(sfid: str) -> Union[redirect, render_template]:
"""Renders or edits a contact based on the Salesforce record.
sfid : str
Salesforce 18-character record ID
"""
# Get Contact sObject record with matching Salesforce ID
contact: Optional[ContactModel] = ContactModel.query.filter_by(
sfid=sfid
).first()
if contact is None:
flash(
'No contact with matching Salesforce ID exists.',
category='danger'
)
# Since the record does not exist, redirect to the index page.
return redirect(url_for('index'))
form = ContactForm()
# First, check if this is a submission (POST).
if form.is_submitted():
# Next, validate form data.
if form.validate():
# Populate changes onto Contact sObject record and commit the
# changes to the PostGreSQL database. Heroku Connect will sync
# those changes back to Salesforce in the background.
form.populate_obj(contact)
db.session.add(contact)
db.session.commit()
flash('Contact successfully updated.', category='success')
else:
# Form validation failed; flash error messages
for errors in form.errors.values():
for error in errors:
flash(error, category='danger')
else:
# Else, not a submission (GET); Populate the form with the contents
# of the Contact sObject record.
form.process(formdata=None, obj=contact)
# Render the contact page.
return render_template('contact.html', sfid=sfid, form=form)