├── .gitignore ├── compareorgs ├── __init__.py ├── management │ ├── __init__.py │ ├── commands │ │ ├── __init__.py │ │ └── delete_jobs.py │ ├── __init__.pyc │ └── __pycache__ │ │ └── __init__.cpython-311.pyc ├── migrations │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-311.pyc │ │ └── 0001_initial.cpython-311.pyc │ └── 0001_initial.py ├── tests.py ├── __pycache__ │ ├── admin.cpython-311.pyc │ ├── forms.cpython-311.pyc │ ├── models.cpython-311.pyc │ ├── tasks.cpython-311.pyc │ ├── utils.cpython-311.pyc │ ├── views.cpython-311.pyc │ └── __init__.cpython-311.pyc ├── utils.py ├── forms.py ├── admin.py ├── models.py ├── views.py └── tasks.py ├── runtime.txt ├── sforgcompare ├── __init__.py ├── __pycache__ │ ├── urls.cpython-311.pyc │ ├── wsgi.cpython-311.pyc │ ├── __init__.cpython-311.pyc │ └── settings.cpython-311.pyc ├── asgi.py ├── wsgi.py ├── urls.py └── settings.py ├── db.sqlite3 ├── static ├── images │ ├── icon.png │ ├── logo.png │ ├── favicon.ico │ ├── i-icon.png │ ├── loading.gif │ ├── oh-dear.png │ ├── logo-small.png │ └── tquila-logo@1x.png ├── bootstrap │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.ttf │ │ └── glyphicons-halflings-regular.woff │ └── css │ │ ├── bootstrap-theme.min.css │ │ ├── bootstrap-theme.css.map │ │ └── bootstrap-theme.css ├── jquery-syntaxhighlighter │ ├── prettify │ │ ├── prettify.min.css │ │ └── prettify.min.js │ ├── styles │ │ ├── style.min.css │ │ └── theme-balupton.min.css │ └── jquery.syntaxhighlighter.min.js ├── js │ ├── main.js │ ├── compare-offline.js │ ├── jquery-migrate-1.1.0.min.js │ └── compare.js └── css │ └── styles.css ├── Procfile ├── templates ├── 500.html ├── logout.html ├── 404.html ├── loading.html ├── oauth_response.html ├── compare_results.html ├── base.html ├── compare_results_offline.html └── index.html ├── README.md ├── manage.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | .pyc -------------------------------------------------------------------------------- /compareorgs/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.11.3 -------------------------------------------------------------------------------- /sforgcompare/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /compareorgs/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /compareorgs/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /compareorgs/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benedwards44/sforgcompare/HEAD/db.sqlite3 -------------------------------------------------------------------------------- /compareorgs/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /static/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benedwards44/sforgcompare/HEAD/static/images/icon.png -------------------------------------------------------------------------------- /static/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benedwards44/sforgcompare/HEAD/static/images/logo.png -------------------------------------------------------------------------------- /static/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benedwards44/sforgcompare/HEAD/static/images/favicon.ico -------------------------------------------------------------------------------- /static/images/i-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benedwards44/sforgcompare/HEAD/static/images/i-icon.png -------------------------------------------------------------------------------- /static/images/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benedwards44/sforgcompare/HEAD/static/images/loading.gif -------------------------------------------------------------------------------- /static/images/oh-dear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benedwards44/sforgcompare/HEAD/static/images/oh-dear.png -------------------------------------------------------------------------------- /static/images/logo-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benedwards44/sforgcompare/HEAD/static/images/logo-small.png -------------------------------------------------------------------------------- /static/images/tquila-logo@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benedwards44/sforgcompare/HEAD/static/images/tquila-logo@1x.png -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn sforgcompare.wsgi --workers $WEB_CONCURRENCY 2 | worker: celery -A compareorgs.tasks worker -B --loglevel=info -------------------------------------------------------------------------------- /compareorgs/management/__init__.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benedwards44/sforgcompare/HEAD/compareorgs/management/__init__.pyc -------------------------------------------------------------------------------- /compareorgs/__pycache__/admin.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benedwards44/sforgcompare/HEAD/compareorgs/__pycache__/admin.cpython-311.pyc -------------------------------------------------------------------------------- /compareorgs/__pycache__/forms.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benedwards44/sforgcompare/HEAD/compareorgs/__pycache__/forms.cpython-311.pyc -------------------------------------------------------------------------------- /compareorgs/__pycache__/models.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benedwards44/sforgcompare/HEAD/compareorgs/__pycache__/models.cpython-311.pyc -------------------------------------------------------------------------------- /compareorgs/__pycache__/tasks.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benedwards44/sforgcompare/HEAD/compareorgs/__pycache__/tasks.cpython-311.pyc -------------------------------------------------------------------------------- /compareorgs/__pycache__/utils.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benedwards44/sforgcompare/HEAD/compareorgs/__pycache__/utils.cpython-311.pyc -------------------------------------------------------------------------------- /compareorgs/__pycache__/views.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benedwards44/sforgcompare/HEAD/compareorgs/__pycache__/views.cpython-311.pyc -------------------------------------------------------------------------------- /sforgcompare/__pycache__/urls.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benedwards44/sforgcompare/HEAD/sforgcompare/__pycache__/urls.cpython-311.pyc -------------------------------------------------------------------------------- /sforgcompare/__pycache__/wsgi.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benedwards44/sforgcompare/HEAD/sforgcompare/__pycache__/wsgi.cpython-311.pyc -------------------------------------------------------------------------------- /compareorgs/__pycache__/__init__.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benedwards44/sforgcompare/HEAD/compareorgs/__pycache__/__init__.cpython-311.pyc -------------------------------------------------------------------------------- /sforgcompare/__pycache__/__init__.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benedwards44/sforgcompare/HEAD/sforgcompare/__pycache__/__init__.cpython-311.pyc -------------------------------------------------------------------------------- /sforgcompare/__pycache__/settings.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benedwards44/sforgcompare/HEAD/sforgcompare/__pycache__/settings.cpython-311.pyc -------------------------------------------------------------------------------- /static/bootstrap/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benedwards44/sforgcompare/HEAD/static/bootstrap/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /static/bootstrap/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benedwards44/sforgcompare/HEAD/static/bootstrap/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /static/bootstrap/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benedwards44/sforgcompare/HEAD/static/bootstrap/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /compareorgs/management/__pycache__/__init__.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benedwards44/sforgcompare/HEAD/compareorgs/management/__pycache__/__init__.cpython-311.pyc -------------------------------------------------------------------------------- /compareorgs/migrations/__pycache__/__init__.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benedwards44/sforgcompare/HEAD/compareorgs/migrations/__pycache__/__init__.cpython-311.pyc -------------------------------------------------------------------------------- /compareorgs/migrations/__pycache__/0001_initial.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benedwards44/sforgcompare/HEAD/compareorgs/migrations/__pycache__/0001_initial.cpython-311.pyc -------------------------------------------------------------------------------- /compareorgs/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utility class for various methods 3 | """ 4 | 5 | def chunks(l, n): 6 | """ 7 | Split a list into specified chunks 8 | """ 9 | n = max(1, n) 10 | return [l[i:i + n] for i in range(0, len(l), n)] -------------------------------------------------------------------------------- /compareorgs/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | class JobForm(forms.Form): 4 | org_one = forms.IntegerField(required=False) 5 | org_two = forms.IntegerField(required=False) 6 | api_choice = forms.CharField() 7 | email_choice = forms.CharField() 8 | email = forms.CharField() 9 | contextual_diff = forms.BooleanField(required=False, initial=True) -------------------------------------------------------------------------------- /templates/500.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% load static %} 4 | 5 | {% block content %} 6 | 7 |
8 | 9 |

500 - Website Error

10 | 11 | Doh 12 | 13 | 16 | 17 |
18 | 19 | {% endblock %} -------------------------------------------------------------------------------- /sforgcompare/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for sforgcompare project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.1/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'sforgcompare.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /sforgcompare/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for sforgcompare project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'sforgcompare.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /compareorgs/management/commands/delete_jobs.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import NoArgsCommand, CommandError, BaseCommand 2 | from compareorgs.models import Job 3 | import datetime 4 | 5 | class Command(NoArgsCommand): 6 | 7 | def handle_noargs(self, **options): 8 | 9 | one_day_ago = datetime.datetime.now() - datetime.timedelta(hours=24) 10 | jobs = Job.objects.filter(created_date__lt = one_day_ago) 11 | jobs.delete() 12 | 13 | 14 | -------------------------------------------------------------------------------- /templates/logout.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 | 5 |

Salesforce Org Compare

6 |

7 | Successfully logged out of Salesforce and Package Builder 8 |

9 | 10 | 11 | 12 | 13 | 18 | 19 | {% endblock %} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sforgcompare 2 | Django + Heroku application which compares metadata between two Salesforce environments and presents the differences 3 | 4 | ## Addons 5 | 6 | 1. Heroku Postgres 7 | 2. Heroku Redis 8 | 3. Heroku Scheduler (for clearing database) 9 | 10 | ## Config Variables 11 | 12 | 1. DEFAULT_FROM_EMAIL (eg. ben@edwards.nz) 13 | 2. EMAIL_HOST (eg. smtp.gmail.com) 14 | 3. EMAIL_HOST_PASSWORD 15 | 4. EMAIL_HOST_USER (eg. ben@edwards.nz) 16 | 5. EMAIL_PORT (eg. 587) 17 | 6. SALESFORCE_API_VERSION (eg. 39) 18 | 7. SALESFORCE_CONSUMER_KEY (from Salesforce Connected App) 19 | 8. SALESFORCE_CONSUMER_SECRET (from Salesforce Connected App) 20 | -------------------------------------------------------------------------------- /static/jquery-syntaxhighlighter/prettify/prettify.min.css: -------------------------------------------------------------------------------- 1 | .str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun{color:#660}.pln{color:#000}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec{color:#606}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee}@media print{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun{color:#440}.pln{color:#000}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}} -------------------------------------------------------------------------------- /templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% load static %} 4 | 5 | {% block content %} 6 | 7 |
8 | 9 |

404 - Page Not Found

10 | 11 | Doh 12 | 13 | 18 | 19 |
20 | 21 | {% endblock %} -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'sforgcompare.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | amqp==5.1.1 2 | asgiref==3.6.0 3 | async-timeout==4.0.2 4 | billiard==3.6.4.0 5 | boto==2.49.0 6 | boto3==1.26.118 7 | botocore==1.29.118 8 | celery==5.2.7 9 | certifi==2022.12.7 10 | charset-normalizer==3.1.0 11 | click==8.1.3 12 | click-didyoumean==0.3.0 13 | click-plugins==1.1.1 14 | click-repl==0.2.0 15 | dj-database-url==1.3.0 16 | Django==4.2 17 | django-storages==1.13.2 18 | gunicorn==20.1.0 19 | idna==3.4 20 | jmespath==1.0.1 21 | kombu==5.2.4 22 | prompt-toolkit==3.0.38 23 | psycopg2==2.9.6 24 | python-dateutil==2.8.2 25 | pytz==2023.3 26 | redis==4.5.4 27 | requests==2.28.2 28 | s3transfer==0.6.0 29 | six==1.16.0 30 | sqlparse==0.4.4 31 | suds==1.1.2 32 | typing_extensions==4.5.0 33 | urllib3==1.26.15 34 | vine==5.0.0 35 | wcwidth==0.2.6 36 | whitenoise==6.4.0 37 | -------------------------------------------------------------------------------- /sforgcompare/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from django.contrib import admin 3 | from compareorgs import views 4 | 5 | urlpatterns = [ 6 | path('', views.index, name='index'), 7 | path('admin/', admin.site.urls), 8 | path('oauth_response/', views.oauth_response, name='oauth_response'), 9 | path('job_status//', views.job_status), 10 | path('compare_orgs//', views.compare_orgs), 11 | path('compare_result//', views.compare_results), 12 | path('compare_result//build_file/', views.build_file), 13 | path('re-run-job//', views.rerunjob), 14 | path('check_file_status//', views.check_file_status), 15 | path('get_metadata//', views.get_metadata), 16 | path('get_diffhtml//', views.get_diffhtml), 17 | ] 18 | -------------------------------------------------------------------------------- /static/jquery-syntaxhighlighter/styles/style.min.css: -------------------------------------------------------------------------------- 1 | .prettyprint ol,.prettyprint ul{list-style:none}.prettyprint ol.linenums{list-style:decimal outside;padding-left:47px;color:#afafaf;font-size:12px}.prettyprint ol.linenums li{padding-left:15px;border-left:3px #6ce26c solid}.prettyprint li>*{font-size:14px}.prettyprint{border:5px solid #DDD;padding:7px 0 5px;font-size:14px;white-space:pre;overflow:auto;max-height:500px;width:100%;display:block}.prettyprint ol,.prettyprint ul{padding:5px 0}.prettyprint li.L0,.prettyprint li.L1,.prettyprint li.L2,.prettyprint li.L3,.prettyprint li.L4,.prettyprint li.L5,.prettyprint li.L6,.prettyprint li.L7,.prettyprint li.L8,.prettyprint li.L9{list-style:inherit;background:0}.prettyprint.alternate .prettyprint.alternate li.L1,.prettyprint.alternate li.L3,.prettyprint.alternate li.L5,.prettyprint.alternate li.L7,.prettyprint.alternate li.L9{background:#f5f5f5} -------------------------------------------------------------------------------- /static/jquery-syntaxhighlighter/styles/theme-balupton.min.css: -------------------------------------------------------------------------------- 1 | .prettyprint.theme-balupton{font-family:Consolas,'Bitstream Vera Sans Mono','Courier New',Courier,monospace;font-size:14px;font-style:normal;font-weight:normal;line-height:15px}.prettyprint.theme-balupton .com{color:#008200}.prettyprint.theme-balupton .lit{color:#066}.prettyprint.theme-balupton.lang-html .lit{color:#066}.prettyprint.theme-balupton.lang-html .kwd{color:#066;font-weight:bold}.prettyprint.theme-balupton.lang-html .atv+.pln,.prettyprint.theme-balupton.lang-html .pun+.pln{color:blue}.prettyprint.theme-balupton .atv,.prettyprint.theme-balupton .str{color:blue}.prettyprint.theme-balupton .atn{color:gray}.prettyprint.theme-balupton .pln{color:black}.prettyprint.theme-balupton .pun{color:#666}.prettyprint.theme-balupton .typ{color:#606}.prettyprint.theme-balupton .tag,.prettyprint.theme-balupton .kwd{color:#069;font-weight:bold} -------------------------------------------------------------------------------- /compareorgs/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from compareorgs.models import Job, Org, Component, ComponentType, ComponentListUnique, OfflineFileJob 3 | 4 | class OrgInline(admin.TabularInline): 5 | fields = ['org_number','org_name', 'username', 'instance_url', 'access_token', 'status', 'error', 'error_stacktrace'] 6 | ordering = ['org_number'] 7 | model = Org 8 | extra = 0 9 | 10 | class ComponentInline(admin.TabularInline): 11 | fields = ['name', 'content'] 12 | ordering = ['name'] 13 | model = Component 14 | extra = 0 15 | 16 | class ComponentListUniqueInline(admin.TabularInline): 17 | fields = ['order','row_html','diff_html'] 18 | ordering = ['order'] 19 | model = ComponentListUnique 20 | extra = 0 21 | 22 | class OfflineFileJobInline(admin.TabularInline): 23 | fields = ['status', 'error'] 24 | model = OfflineFileJob 25 | extra = 0 26 | 27 | class ComponentTypeAdmin(admin.ModelAdmin): 28 | list_display = ['org_name','name'] 29 | ordering = ['org', 'name'] 30 | inlines = [ComponentInline] 31 | 32 | class JobAdmin(admin.ModelAdmin): 33 | list_display = ('created_date','finished_date','email','status','error') 34 | ordering = ['-created_date'] 35 | inlines = [OrgInline, OfflineFileJobInline] 36 | 37 | 38 | class OrgAdmin(admin.ModelAdmin): 39 | list_display = ('job','org_name','username','status') 40 | ordering = ['job'] 41 | 42 | 43 | admin.site.register(Job, JobAdmin) 44 | admin.site.register(Org, OrgAdmin) 45 | admin.site.register(ComponentType, ComponentTypeAdmin) -------------------------------------------------------------------------------- /static/js/main.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function () 2 | { 3 | var urlPrefix = 'https://'; 4 | var urlSuffix = '.salesforce.com/services/oauth2/authorize?response_type=code&client_id=' + $('#client_id').val() + '&redirect_uri=' + $('#redirect_uri').val() + '&scope=api&state='; 5 | 6 | $('#orgOneButton').click(function() 7 | { 8 | var loginUrl = urlPrefix; 9 | if ( $('#org_one_env').val() == 'Production' ) 10 | { 11 | loginUrl += 'login' 12 | } 13 | else 14 | { 15 | loginUrl += 'test' 16 | } 17 | loginUrl += urlSuffix + $('#org_one_env').val() + 'org1'; 18 | popupwindow(loginUrl); 19 | }); 20 | 21 | $('#orgTwoButton').click(function() 22 | { 23 | var loginUrl = urlPrefix; 24 | if ( $('#org_two_env').val() == 'Production' ) 25 | { 26 | loginUrl += 'login' 27 | } 28 | else 29 | { 30 | loginUrl += 'test' 31 | } 32 | loginUrl += urlSuffix + $('#org_two_env').val() + 'org2'; 33 | popupwindow(loginUrl); 34 | }); 35 | 36 | $('#id_email_choice').change(function() 37 | { 38 | if ( $(this).val() == 'yes' ) 39 | { 40 | $('#id_email').show(); 41 | } 42 | else 43 | { 44 | $('#id_email').hide(); 45 | } 46 | }); 47 | 48 | $('[data-toggle="popover"]').popover(); 49 | 50 | }); 51 | 52 | function popupwindow(url) 53 | { 54 | var w = 350; 55 | var h = 485; 56 | var left = (screen.width/2)-(w/2); 57 | var top = (screen.height/2)-(h/2); 58 | return window.open(url, 'SalesforceLogin', 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no, width='+w+', height='+h+', top='+top+', left='+left); 59 | } 60 | 61 | function updateOrgDetails(org, username, org_name, org_id, email) 62 | { 63 | if (org == 'org1') 64 | { 65 | $('#orgOneLogin').hide(); 66 | $('#orgOneDisplay').show(); 67 | $('#orgOneUsername').text(username); 68 | $('#orgOneOrg').text(org_name); 69 | $('#id_org_one').val(org_id); 70 | } 71 | else 72 | { 73 | $('#orgTwoLogin').hide(); 74 | $('#orgTwoDisplay').show(); 75 | $('#orgTwoUsername').text(username); 76 | $('#orgTwoOrg').text(org_name); 77 | $('#id_org_two').val(org_id); 78 | } 79 | 80 | $('#id_email').val(email); 81 | 82 | // Both elements are visible, show the GO button 83 | if ( $('#orgOneDisplay').is(":visible") && $('#orgTwoDisplay').is(":visible") ) 84 | { 85 | $('#compareOrgs').show(); 86 | } 87 | } -------------------------------------------------------------------------------- /templates/loading.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% load static %} 4 | 5 | {% block content %} 6 | 7 |

Salesforce Org Compare

8 |

9 | This tool uses the Salesforce Tooling or Metadata API (user selection) to compare metadata between two Orgs. This is useful when planning deployments or building deployment packages, as well as seeing what configuration exists in Production to Sandbox or between Sandbox environments. 10 |

11 |

12 | None of your organisation information or data is captured or kept from running this tool. 13 |

14 | 15 | 20 | 21 |
22 | 23 |

24 | 25 |
26 | Loading 27 |
28 | 29 |
30 |

31 | Querying all metadata and comparing... 32 |

33 |

34 | This can take some time, depending on size of Orgs and API selection. 35 |

36 |
37 | 38 |
39 | 40 | 47 | 48 | 78 | 79 | {% endblock %} -------------------------------------------------------------------------------- /templates/oauth_response.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | Salesforce.com Org Compare Tool 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | {% if error %} 26 | 27 |

Error

28 | 29 | 35 | 36 | 37 | 38 | {% else %} 39 | 40 |

Success

41 | 42 | 45 | 46 | 47 | 48 | 57 | 58 | {% endif %} 59 | 60 | 61 | 62 | 63 | 64 |
65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /compareorgs/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | class Job(models.Model): 4 | random_id = models.CharField(db_index=True,max_length=255, blank=True) 5 | created_date = models.DateTimeField(null=True,blank=True) 6 | finished_date = models.DateTimeField(null=True,blank=True) 7 | password = models.CharField(max_length=255, blank=True) 8 | api_choice = models.CharField(max_length=255,null=True,blank=True) 9 | email_result = models.BooleanField() 10 | email = models.CharField(max_length=255,blank=True) 11 | contextual_diff = models.BooleanField(default=False) 12 | status = models.CharField(max_length=255, blank=True) 13 | error = models.TextField(blank=True) 14 | error_stacktrace = models.TextField(blank=True) 15 | zip_file = models.FileField(upload_to='compare_results/', blank=True, null=True) 16 | zip_file_error = models.TextField(blank=True, null=True) 17 | 18 | def sorted_orgs(self): 19 | return self.org_set.order_by('org_number') 20 | 21 | def sorted_component_list(self): 22 | return self.componentlistunique_set.order_by('order') 23 | 24 | class Org(models.Model): 25 | job = models.ForeignKey(Job, blank=True, null=True, on_delete=models.deletion.CASCADE) 26 | org_number = models.PositiveSmallIntegerField() 27 | access_token = models.CharField(max_length=255) 28 | instance_url = models.CharField(max_length=255) 29 | org_id = models.CharField(max_length=255) 30 | org_name = models.CharField(max_length=255, blank=True) 31 | username = models.CharField(max_length=255, blank=True) 32 | status = models.CharField(max_length=255, blank=True) 33 | error = models.TextField(blank=True) 34 | error_stacktrace = models.TextField(blank=True) 35 | 36 | def sorted_component_types(self): 37 | return self.componenttype_set.order_by('name') 38 | 39 | class ComponentType(models.Model): 40 | org = models.ForeignKey(Org, on_delete=models.deletion.CASCADE) 41 | name = models.CharField(max_length=255) 42 | 43 | def __str__(self): 44 | return '%s' % (self.name) 45 | 46 | def sorted_components(self): 47 | return self.component_set.order_by('name') 48 | 49 | def org_name(self): 50 | return self.org.org_name 51 | 52 | class Component(models.Model): 53 | component_type = models.ForeignKey(ComponentType, on_delete=models.deletion.CASCADE) 54 | name = models.CharField(max_length=255) 55 | content = models.TextField(blank=True, null=True) 56 | 57 | def __str__(self): 58 | return '%s' % (self.name) 59 | 60 | class ComponentListUnique(models.Model): 61 | job = models.ForeignKey(Job, on_delete=models.deletion.CASCADE) 62 | diff_html = models.TextField(blank=True, null=True) 63 | row_html = models.TextField(blank=True, null=True) 64 | order = models.PositiveSmallIntegerField() 65 | 66 | 67 | class OfflineFileJob(models.Model): 68 | job = models.ForeignKey(Job, on_delete=models.deletion.CASCADE) 69 | status = models.CharField(max_length=255) 70 | error = models.TextField(blank=True, null=True) 71 | error_stacktrace = models.TextField(blank=True) 72 | 73 | -------------------------------------------------------------------------------- /static/js/compare-offline.js: -------------------------------------------------------------------------------- 1 | $.SyntaxHighlighter.init(); 2 | 3 | $(document).ready(function () 4 | { 5 | $('tr.component').hide(); 6 | $('tr.success').hide(); 7 | 8 | // Toggle file show and hide 9 | $('tr.type td').click(function() 10 | { 11 | var componentType = $(this).parent().attr('class').split('_')[1]; 12 | $('.component_' + componentType).toggle(); 13 | 14 | if ( $('#display_option').val() == 'diff') 15 | { 16 | $('tr.success').hide(); 17 | } 18 | 19 | }); 20 | 21 | // Open code view modal 22 | $('tr.component td').click(function() 23 | { 24 | // Set loading gif while metadata loads 25 | $('#codeModalBody').html('Loading'); 26 | 27 | // Component type of the cell clicked 28 | var componentType = $(this).parent().attr('class').split('_')[1].trim(); 29 | var componentName = $(this).text().trim(); 30 | 31 | // Set label of the modal 32 | $('#codeModalLabel').text(componentType + '/' + componentName); 33 | 34 | // If diff file - query for diff HTML that Python generated 35 | if ( $(this).hasClass('diff') ) 36 | { 37 | var metadata = component_data['diff-' + $(this).attr('id')]; 38 | 39 | $('#codeModalBody').html(metadata); 40 | 41 | // Remove nowrap attribute. This is handled better with CSS. 42 | $('#codeModalBody td[nowrap="nowrap"]').removeAttr('nowrap'); 43 | } 44 | // Otherwise obtain metadata for display 45 | else 46 | { 47 | 48 | console.log(componentName); 49 | 50 | var metadata = component_data['component-' + $(this).attr('id')]; 51 | 52 | if (componentType == 'ApexClass' || componentType == 'ApexTrigger' || componentType == 'classes' || componentType == 'triggers') 53 | { 54 | if (componentName.indexOf('meta.xml') != -1) { 55 | 56 | metadata = metadata.replace(//g,'>') 58 | .replace(/\n/g, '
'); 59 | } 60 | } 61 | else 62 | { 63 | metadata = metadata.replace(//g,'>') 65 | .replace(/\n/g, '
'); 66 | } 67 | 68 | var $content = $('
' + metadata + '
'); 69 | $content.syntaxHighlight(); 70 | $('#codeModalBody').html($content); 71 | $.SyntaxHighlighter.init(); 72 | } 73 | 74 | // Load modal 75 | $('#viewCodeModal').modal(); 76 | 77 | }); 78 | 79 | // Change display options 80 | $('#display_option').change(function() 81 | { 82 | $('tr.component').hide(); 83 | $('tr.type').show(); 84 | 85 | if ( $(this).val() == 'diff') 86 | { 87 | checkAnyChildVisible(); 88 | } 89 | else 90 | { 91 | $('#no_differences_message').hide(); 92 | } 93 | 94 | }); 95 | 96 | $('.loading-display').hide(); 97 | $('#compare_results').show(); 98 | checkAnyChildVisible(); 99 | 100 | }); 101 | 102 | // Check if the parent component type (eg ApexClass), has any children. If not, ApexClass won't display at all 103 | function checkAnyChildVisible() 104 | { 105 | // Loop through type rows 106 | $.each($('tr.type'), function() 107 | { 108 | var childVisible = false; 109 | 110 | // Loop through component rows 111 | $.each($('tr[class*="component_' + $(this).attr('class').split('_')[1] + '"]'), function() 112 | { 113 | // It a row is visible, this is enough to know to show the parent 114 | if ( !$(this).hasClass('success') ) 115 | { 116 | childVisible = true; 117 | return; 118 | } 119 | }); 120 | 121 | // If no children are visible, hide the parent 122 | if (!childVisible) 123 | { 124 | $(this).hide(); 125 | } 126 | 127 | }); 128 | 129 | var rowVisible = false; 130 | 131 | // Check that anything at all is visible 132 | $.each($('tr.type'), function() 133 | { 134 | if ($(this).is(':visible')) 135 | { 136 | rowVisible = true; 137 | return; 138 | } 139 | }); 140 | 141 | // If no rows are visible, display message 142 | if (!rowVisible) 143 | { 144 | $('#no_differences_message').show(); 145 | } 146 | 147 | } -------------------------------------------------------------------------------- /sforgcompare/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | import dj_database_url 3 | from pathlib import Path 4 | 5 | BASE_DIR = Path(__file__).resolve().parent.parent 6 | 7 | IS_HEROKU = "DYNO" in os.environ 8 | 9 | # SECURITY WARNING: keep the secret key used in production secret! 10 | SECRET_KEY = os.environ.get('SECRET_KEY', 'LOCAL') 11 | 12 | # SECURITY WARNING: don't run with debug turned on in production! 13 | DEBUG = os.environ.get('DEBUG') == '1' 14 | TEMPLATE_DEBUG = DEBUG 15 | THUMBNAIL_DEBUG = DEBUG 16 | 17 | ADMINS = ( 18 | ('Ben Edwards', 'ben@edwards.nz'), 19 | ) 20 | 21 | if IS_HEROKU: 22 | SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') 23 | SECURE_SSL_REDIRECT = True 24 | 25 | ALLOWED_HOSTS = ['*'] 26 | 27 | # Application definition 28 | 29 | INSTALLED_APPS = ( 30 | 'django.contrib.admin', 31 | 'django.contrib.auth', 32 | 'django.contrib.contenttypes', 33 | 'django.contrib.sessions', 34 | 'django.contrib.messages', 35 | 'django.contrib.staticfiles', 36 | 'compareorgs', 37 | 'storages', 38 | ) 39 | 40 | MIDDLEWARE = [ 41 | 'django.middleware.security.SecurityMiddleware', 42 | 'whitenoise.middleware.WhiteNoiseMiddleware', 43 | 'django.contrib.sessions.middleware.SessionMiddleware', 44 | 'django.middleware.common.CommonMiddleware', 45 | 'django.middleware.csrf.CsrfViewMiddleware', 46 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 47 | 'django.contrib.messages.middleware.MessageMiddleware', 48 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 49 | ] 50 | 51 | SECURE_CROSS_ORIGIN_OPENER_POLICY = None 52 | 53 | ROOT_URLCONF = 'sforgcompare.urls' 54 | 55 | TEMPLATES = [ 56 | { 57 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 58 | 'DIRS': [ 59 | BASE_DIR / 'templates' 60 | ], 61 | 'APP_DIRS': True, 62 | 'OPTIONS': { 63 | 'context_processors': [ 64 | 'django.template.context_processors.debug', 65 | 'django.template.context_processors.request', 66 | 'django.contrib.auth.context_processors.auth', 67 | 'django.contrib.messages.context_processors.messages', 68 | ], 69 | }, 70 | }, 71 | ] 72 | 73 | WSGI_APPLICATION = 'sforgcompare.wsgi.application' 74 | 75 | MAX_CONN_AGE = 600 76 | 77 | DATABASES = { 78 | 'default': { 79 | 'ENGINE': 'django.db.backends.sqlite3', 80 | 'NAME': BASE_DIR / 'db.sqlite3', 81 | } 82 | } 83 | 84 | if "DATABASE_URL" in os.environ: 85 | # Configure Django for DATABASE_URL environment variable. 86 | DATABASES["default"] = dj_database_url.config( 87 | conn_max_age=MAX_CONN_AGE, ssl_require=True) 88 | 89 | # Celery settings 90 | BROKER_POOL_LIMIT = 1 91 | 92 | # Internationalization 93 | # https://docs.djangoproject.com/en/1.6/topics/i18n/ 94 | 95 | LANGUAGE_CODE = 'en-gb' 96 | TIME_ZONE = 'UTC' 97 | USE_I18N = True 98 | USE_L10N = True 99 | USE_TZ = True 100 | 101 | 102 | STATIC_ROOT = BASE_DIR / "staticfiles" 103 | STATICFILES_DIRS = [ 104 | BASE_DIR / "static", 105 | ] 106 | STATIC_URL = 'static/' 107 | 108 | # SALESFORCE KEYS 109 | SALESFORCE_CONSUMER_KEY = os.environ.get('SALESFORCE_CONSUMER_KEY') 110 | SALESFORCE_CONSUMER_SECRET = os.environ.get('SALESFORCE_CONSUMER_SECRET') 111 | SALESFORCE_REDIRECT_URI = 'https://sforgcompare.herokuapp.com/oauth_response' 112 | SALESFORCE_API_VERSION = int(os.environ.get('SALESFORCE_API_VERSION', '55')) 113 | 114 | # AWS Settings 115 | AWS_ACCESS_KEY_ID = os.environ.get('AWS_ACCESS_KEY_ID') 116 | AWS_SECRET_ACCESS_KEY = os.environ.get('AWS_SECRET_ACCESS_KEY') 117 | AWS_STORAGE_BUCKET_NAME = os.environ.get('AWS_STORAGE_BUCKET_NAME') 118 | 119 | STORAGES = { 120 | "default": { 121 | "BACKEND": "storages.backends.s3boto3.S3Boto3Storage", 122 | }, 123 | "staticfiles": { 124 | "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", 125 | }, 126 | } 127 | 128 | # Mail Settings 129 | DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL') 130 | EMAIL_HOST = os.environ.get('EMAIL_HOST') 131 | EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER') 132 | EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD') 133 | EMAIL_PORT = os.environ.get('EMAIL_PORT') 134 | EMAIL_USE_TLS = True 135 | 136 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' -------------------------------------------------------------------------------- /templates/compare_results.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% load static %} 4 | 5 | {% block scripts %} 6 | 7 | 8 | 9 | {% endblock %} 10 | 11 | {% block content %} 12 | 13 |
14 | 15 |
16 | Loading 17 |
18 | 19 |
20 |

21 | Building results 22 |

23 |

24 | Shouldn't be long... 25 |

26 |
27 | 28 |
29 | 30 |
31 | 32 |
33 |

34 | Your Org Compare results are presented below. Files are highlighted based on matches or differences. You can click on components to view the file and see highlighted differences within files. 35 |

36 |
37 | 38 |
39 | 40 |    41 | 42 |
43 | 44 |
45 | 46 |
47 | 48 |
49 | 53 |
54 | 55 |
56 |
57 | 60 | 63 | 66 |
67 |
68 | 69 |
70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | {% autoescape off %} 82 | {{ html_rows }} 83 | {% endautoescape %} 84 | 85 | 86 |

{{ org_left_username }}

{{ org_right_username }}

87 | 88 | 93 | 94 |
95 | 96 | 119 | 120 | 129 | 130 | {% endblock %} -------------------------------------------------------------------------------- /compareorgs/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2 on 2023-04-24 08:23 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Job', 17 | fields=[ 18 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('random_id', models.CharField(blank=True, db_index=True, max_length=255)), 20 | ('created_date', models.DateTimeField(blank=True, null=True)), 21 | ('finished_date', models.DateTimeField(blank=True, null=True)), 22 | ('password', models.CharField(blank=True, max_length=255)), 23 | ('api_choice', models.CharField(blank=True, max_length=255, null=True)), 24 | ('email_result', models.BooleanField()), 25 | ('email', models.CharField(blank=True, max_length=255)), 26 | ('contextual_diff', models.BooleanField(default=False)), 27 | ('status', models.CharField(blank=True, max_length=255)), 28 | ('error', models.TextField(blank=True)), 29 | ('error_stacktrace', models.TextField(blank=True)), 30 | ('zip_file', models.FileField(blank=True, null=True, upload_to='compare_results/')), 31 | ('zip_file_error', models.TextField(blank=True, null=True)), 32 | ], 33 | ), 34 | migrations.CreateModel( 35 | name='Org', 36 | fields=[ 37 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 38 | ('org_number', models.PositiveSmallIntegerField()), 39 | ('access_token', models.CharField(max_length=255)), 40 | ('instance_url', models.CharField(max_length=255)), 41 | ('org_id', models.CharField(max_length=255)), 42 | ('org_name', models.CharField(blank=True, max_length=255)), 43 | ('username', models.CharField(blank=True, max_length=255)), 44 | ('status', models.CharField(blank=True, max_length=255)), 45 | ('error', models.TextField(blank=True)), 46 | ('error_stacktrace', models.TextField(blank=True)), 47 | ('job', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='compareorgs.job')), 48 | ], 49 | ), 50 | migrations.CreateModel( 51 | name='OfflineFileJob', 52 | fields=[ 53 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 54 | ('status', models.CharField(max_length=255)), 55 | ('error', models.TextField(blank=True, null=True)), 56 | ('error_stacktrace', models.TextField(blank=True)), 57 | ('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='compareorgs.job')), 58 | ], 59 | ), 60 | migrations.CreateModel( 61 | name='ComponentType', 62 | fields=[ 63 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 64 | ('name', models.CharField(max_length=255)), 65 | ('org', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='compareorgs.org')), 66 | ], 67 | ), 68 | migrations.CreateModel( 69 | name='ComponentListUnique', 70 | fields=[ 71 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 72 | ('diff_html', models.TextField(blank=True, null=True)), 73 | ('row_html', models.TextField(blank=True, null=True)), 74 | ('order', models.PositiveSmallIntegerField()), 75 | ('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='compareorgs.job')), 76 | ], 77 | ), 78 | migrations.CreateModel( 79 | name='Component', 80 | fields=[ 81 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 82 | ('name', models.CharField(max_length=255)), 83 | ('content', models.TextField(blank=True, null=True)), 84 | ('component_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='compareorgs.componenttype')), 85 | ], 86 | ), 87 | ] 88 | -------------------------------------------------------------------------------- /static/css/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "HelveticaNeueW02-45Ligh", "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Nimbus Sans L", sans-serif; 3 | color: #999a9b; 4 | } 5 | input[type="text"], input[type="password"] { 6 | width: 250px; 7 | padding: 5px; 8 | color: #63666a; 9 | font-size: 0.9em; 10 | padding-left: 0.4em; 11 | max-width: 19em; 12 | } 13 | input[type="submit"], button { 14 | cursor: pointer; 15 | -webkit-appearance: none!important; 16 | -moz-appearance: none!important; 17 | appearance: none!important; 18 | border: none; 19 | font-family: "HelveticaNeueW02-45Ligh", "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Nimbus Sans L", sans-serif; 20 | text-transform: uppercase; 21 | font-size: 0.9em; 22 | line-height: 1.8; 23 | color: white; 24 | text-decoration: none; 25 | background-color: rgba(255, 102, 0, 0.9); 26 | transition: background-color 0.3s; 27 | -moz-transition: background-color 0.3s; 28 | -webkit-transition: background-color 0.3s; 29 | -o-transition: background-color 0.3s; 30 | padding: 0.3em 1em; 31 | } 32 | h1 { 33 | font-family: "HelveticaNeueW02-Thin", "HelveticaNeueW02-45Ligh", "HelveticaNeue-Light", "Helvetica Neue Light", "HelveticaNeueW02-45Ligh", "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Nimbus Sans L", sans-serif; 34 | font-size: 2em; 35 | line-height: 1.1em; 36 | color: #ff6600; 37 | } 38 | h2 { 39 | font-family: "HelveticaNeueW02-Thin", "HelveticaNeueW02-45Ligh", "HelveticaNeue-Light", "Helvetica Neue Light", "HelveticaNeueW02-45Ligh", "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Nimbus Sans L", sans-serif; 40 | font-size: 1.5em; 41 | line-height: 1.1em; 42 | color: #ff6600; 43 | } 44 | h3, h4 { 45 | color: #ff6600; 46 | } 47 | #mti_wfs_colophon { 48 | display: none !important; 49 | } 50 | .grey { 51 | font-size: 20px; 52 | color: #999a9b !important; 53 | } 54 | 55 | .loading { 56 | width: 500px; 57 | height: 300px; 58 | padding: 20px 0 0 40px; 59 | } 60 | 61 | .logo { 62 | float: left; 63 | } 64 | 65 | .logo img { 66 | margin-top:10px;margin-left:10px; 67 | } 68 | 69 | .contact { 70 | float:right; 71 | margin: 10px 10px 0 0; 72 | } 73 | 74 | .contact a { 75 | color: #999a9b; 76 | text-decoration: none; 77 | } 78 | 79 | .contact a:hover { 80 | color: #ff6600; 81 | } 82 | 83 | .clear { 84 | clear:both; 85 | width: 100%; 86 | height:20px; 87 | } 88 | 89 | .object_link { 90 | color: #999a9b; 91 | text-decoration:none; 92 | font-size:0.8em; 93 | } 94 | 95 | a.object_link:hover { 96 | color: #ff6600; 97 | } 98 | 99 | .error { 100 | color: #900; 101 | } 102 | 103 | #compareOrgs { 104 | display:none; 105 | width:500px; 106 | text-align:center; 107 | margin-top:30px; 108 | padding-left: 200px; 109 | } 110 | 111 | #id_api_choice, #id_email_choice { 112 | width: 180px; 113 | float:left; 114 | } 115 | 116 | #id_contextual_diff { 117 | float: left; 118 | width: 30px; 119 | } 120 | 121 | #id_email { 122 | width: 230px; 123 | } 124 | 125 | #id_contextual_diff_label { 126 | float: left; 127 | margin-top: 10px; 128 | } 129 | 130 | .info_icon { 131 | float: left; 132 | margin-left: 10px; 133 | margin-top: 5px; 134 | cursor:pointer; 135 | } 136 | 137 | #submitCompare { 138 | width: 150px; 139 | clear:left; 140 | margin-top: 10px; 141 | float:left; 142 | } 143 | 144 | .back-to-top { 145 | position: fixed; 146 | bottom: 2em; 147 | right: 0px; 148 | text-decoration: none; 149 | color: #000000; 150 | background-color: rgba(235, 235, 235, 0.80); 151 | font-size: 12px; 152 | padding: 1em; 153 | display: none; 154 | } 155 | 156 | .back-to-top:hover { 157 | background-color: rgba(135, 135, 135, 0.50); 158 | color: #ff6600; 159 | text-decoration:none; 160 | } 161 | 162 | .left-half { 163 | float:left; 164 | width:45%; 165 | } 166 | 167 | .right-half { 168 | margin-left: 10%x; 169 | width:45%; 170 | float: left; 171 | } 172 | 173 | .popover { 174 | width:500px !important; 175 | } 176 | 177 | .loading-display { 178 | width: 100%; 179 | height: 600px; 180 | padding: 50px auto; 181 | } 182 | 183 | #compare_results { 184 | display:none; 185 | } 186 | 187 | #compare_results_table tr.type td { 188 | font-weight: bold; 189 | cursor: pointer; 190 | width: 50%; 191 | } 192 | 193 | #compare_results_table tr.component td { 194 | padding-left: 50px; 195 | cursor: pointer; 196 | width: 50%; 197 | } 198 | 199 | #viewCodeModal .modal-dialog { 200 | width: 97%; 201 | } 202 | 203 | .small_dialog { 204 | width: 50% !important; 205 | } 206 | 207 | table.diff { 208 | width: 90%; 209 | } 210 | 211 | table.diff_header td.diff_header { 212 | padding-left:5px; 213 | } 214 | 215 | /* top scrollbar */ 216 | #top_scrollbar, #codeModalBody { 217 | width: 99%; 218 | overflow-x: scroll; 219 | overflow-y: hidden; 220 | } 221 | 222 | #top_scrollbar, #scrollbar { 223 | height: 20px; 224 | } 225 | 226 | #display_option { 227 | float:left; 228 | width: 300px; 229 | } 230 | 231 | #legend { 232 | float:right; 233 | width: 500px; 234 | } 235 | 236 | #legend .legend { 237 | width: 20%; 238 | margin-left:5px; 239 | float: right; 240 | text-align: center; 241 | height: 32px; 242 | padding: 5px; 243 | margin-bottom: 0; 244 | } 245 | 246 | #no_differences_message { 247 | display: none; 248 | } 249 | 250 | /* Python diff css */ 251 | table.diff {font-family:Consolas,'Bitstream Vera Sans Mono','Courier New',Courier,monospace; border:medium; table-layout: fixed; width: 100%} 252 | .diff_header {background-color:#e0e0e0} 253 | table.diff td {width:47%;word-wrap: break-word;padding:0;margin:0;line-height:1;} 254 | table.diff td.diff_header {text-align:right;width:40px} 255 | .diff_next {display:none;} 256 | .diff_add {background-color:#dff0d8} 257 | .diff_chg {background-color:#fcf8e3} 258 | .diff_sub {background-color:#f2dede} 259 | -------------------------------------------------------------------------------- /static/jquery-syntaxhighlighter/jquery.syntaxhighlighter.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License {@link http://creativecommons.org/licenses/MIT/} 3 | MIT License {@link http://creativecommons.org/licenses/MIT/} 4 | MIT License {@link http://creativecommons.org/licenses/MIT/} 5 | MIT License {@link http://creativecommons.org/licenses/MIT/} 6 | MIT License {@link http://creativecommons.org/licenses/MIT/} 7 | MIT License {@link http://creativecommons.org/licenses/MIT/} 8 | */ 9 | "undefined"===typeof window.console&&(window.console={}); 10 | "undefined"===typeof window.console.emulated&&("function"===typeof window.console.log?window.console.hasLog=!0:("undefined"===typeof window.console.log&&(window.console.log=function(){}),window.console.hasLog=!1),"function"===typeof window.console.debug?window.console.hasDebug=!0:("undefined"===typeof window.console.debug&&(window.console.debug=!window.console.hasLog?function(){}:function(){for(var a=["console.debug:"],b=0;b :first-child, li:last-child > :first-child").each(function(){var b=a(this),d=/^([\r\n\s\t]|\ )*$/.test(b.html()),c=b.parent(),c=b.siblings();if(d&&(c.length===0||c.length===1&&c.filter(":last").is("br"))){c=b.parent();b=c.val();c.next().val(b);c.remove()}});d.stripInitialWhitespace&&e.find("li:first-child > :first-child").each(function(){var b=a(this),c=(b.html().match(/^(([\r\n\s\t]|\ )+)/)|| 22 | [])[1]||"";c.length&&b.parent().siblings().children(":first-child").add(b).each(function(){var b=a(this),d=b.html(),d=d.replace(RegExp("^"+c,"gm"),"");b.html(d)})});d.wrapLines?e.css({"overflow-x":"hidden","overflow-y":"hidden","white-space":"pre-wrap","max-height":"none"}):e.css({"overflow-x":"auto","overflow-y":"auto","white-space":"normal","max-height":"500px"});return this}d.debug&&window.console.debug("SyntaxHighlighter.highlight: Chosen SyntaxHighlighter is not yet defined. Waiting 1200 ms then trying again."); 23 | setTimeout(function(){c.highlight.apply(c,[b])},1200)}}})(jQuery); -------------------------------------------------------------------------------- /static/js/jquery-migrate-1.1.0.min.js: -------------------------------------------------------------------------------- 1 | /*! jQuery Migrate v1.1.0 | (c) 2005, 2013 jQuery Foundation, Inc. and other contributors | jquery.org/license */ 2 | jQuery.migrateMute===void 0&&(jQuery.migrateMute=!0),function(e,t,n){"use strict";function r(n){o[n]||(o[n]=!0,e.migrateWarnings.push(n),t.console&&console.warn&&!e.migrateMute&&(console.warn("JQMIGRATE: "+n),e.migrateTrace&&console.trace&&console.trace()))}function a(t,a,o,i){if(Object.defineProperty)try{return Object.defineProperty(t,a,{configurable:!0,enumerable:!0,get:function(){return r(i),o},set:function(e){r(i),o=e}}),n}catch(s){}e._definePropertyBroken=!0,t[a]=o}var o={};e.migrateWarnings=[],!e.migrateMute&&t.console&&console.log&&console.log("JQMIGRATE: Logging is active"),e.migrateTrace===n&&(e.migrateTrace=!0),e.migrateReset=function(){o={},e.migrateWarnings.length=0},"BackCompat"===document.compatMode&&r("jQuery is not compatible with Quirks Mode");var i={},s=e.attr,u=e.attrHooks.value&&e.attrHooks.value.get||function(){return null},c=e.attrHooks.value&&e.attrHooks.value.set||function(){return n},l=/^(?:input|button)$/i,d=/^[238]$/,p=/^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i,f=/^(?:checked|selected)$/i;a(e,"attrFn",i,"jQuery.attrFn is deprecated"),e.attr=function(t,a,o,i){var u=a.toLowerCase(),c=t&&t.nodeType;return i&&4>s.length&&(r("jQuery.fn.attr( props, pass ) is deprecated"),t&&!d.test(c)&&e.isFunction(e.fn[a]))?e(t)[a](o):("type"===a&&o!==n&&l.test(t.nodeName)&&t.parentNode&&r("Can't change the 'type' of an input or button in IE 6/7/8"),!e.attrHooks[u]&&p.test(u)&&(e.attrHooks[u]={get:function(t,r){var a,o=e.prop(t,r);return o===!0||"boolean"!=typeof o&&(a=t.getAttributeNode(r))&&a.nodeValue!==!1?r.toLowerCase():n},set:function(t,n,r){var a;return n===!1?e.removeAttr(t,r):(a=e.propFix[r]||r,a in t&&(t[a]=!0),t.setAttribute(r,r.toLowerCase())),r}},f.test(u)&&r("jQuery.fn.attr('"+u+"') may use property instead of attribute")),s.call(e,t,a,o))},e.attrHooks.value={get:function(e,t){var n=(e.nodeName||"").toLowerCase();return"button"===n?u.apply(this,arguments):("input"!==n&&"option"!==n&&r("jQuery.fn.attr('value') no longer gets properties"),t in e?e.value:null)},set:function(e,t){var a=(e.nodeName||"").toLowerCase();return"button"===a?c.apply(this,arguments):("input"!==a&&"option"!==a&&r("jQuery.fn.attr('value', val) no longer sets properties"),e.value=t,n)}};var g,h,v=e.fn.init,m=e.parseJSON,y=/^(?:[^<]*(<[\w\W]+>)[^>]*|#([\w\-]*))$/;e.fn.init=function(t,n,a){var o;return t&&"string"==typeof t&&!e.isPlainObject(n)&&(o=y.exec(t))&&o[1]&&("<"!==t.charAt(0)&&r("$(html) HTML strings must start with '<' character"),n&&n.context&&(n=n.context),e.parseHTML)?v.call(this,e.parseHTML(e.trim(t),n,!0),n,a):v.apply(this,arguments)},e.fn.init.prototype=e.fn,e.parseJSON=function(e){return e||null===e?m.apply(this,arguments):(r("jQuery.parseJSON requires a valid JSON string"),null)},e.uaMatch=function(e){e=e.toLowerCase();var t=/(chrome)[ \/]([\w.]+)/.exec(e)||/(webkit)[ \/]([\w.]+)/.exec(e)||/(opera)(?:.*version|)[ \/]([\w.]+)/.exec(e)||/(msie) ([\w.]+)/.exec(e)||0>e.indexOf("compatible")&&/(mozilla)(?:.*? rv:([\w.]+)|)/.exec(e)||[];return{browser:t[1]||"",version:t[2]||"0"}},g=e.uaMatch(navigator.userAgent),h={},g.browser&&(h[g.browser]=!0,h.version=g.version),h.chrome?h.webkit=!0:h.webkit&&(h.safari=!0),e.browser=h,a(e,"browser",h,"jQuery.browser is deprecated"),e.sub=function(){function t(e,n){return new t.fn.init(e,n)}e.extend(!0,t,this),t.superclass=this,t.fn=t.prototype=this(),t.fn.constructor=t,t.sub=this.sub,t.fn.init=function(r,a){return a&&a instanceof e&&!(a instanceof t)&&(a=t(a)),e.fn.init.call(this,r,a,n)},t.fn.init.prototype=t.fn;var n=t(document);return r("jQuery.sub() is deprecated"),t};var b=e.fn.data;e.fn.data=function(t){var a,o,i=this[0];return!i||"events"!==t||1!==arguments.length||(a=e.data(i,t),o=e._data(i,t),a!==n&&a!==o||o===n)?b.apply(this,arguments):(r("Use of jQuery.fn.data('events') is deprecated"),o)};var j=/\/(java|ecma)script/i,w=e.fn.andSelf||e.fn.addBack;e.fn.andSelf=function(){return r("jQuery.fn.andSelf() replaced by jQuery.fn.addBack()"),w.apply(this,arguments)},e.clean||(e.clean=function(t,a,o,i){a=a||document,a=!a.nodeType&&a[0]||a,a=a.ownerDocument||a,r("jQuery.clean() is deprecated");var s,u,c,l,d=[];if(e.merge(d,e.buildFragment(t,a).childNodes),o)for(c=function(e){return!e.type||j.test(e.type)?i?i.push(e.parentNode?e.parentNode.removeChild(e):e):o.appendChild(e):n},s=0;null!=(u=d[s]);s++)e.nodeName(u,"script")&&c(u)||(o.appendChild(u),u.getElementsByTagName!==n&&(l=e.grep(e.merge([],u.getElementsByTagName("script")),c),d.splice.apply(d,[s+1,0].concat(l)),s+=l.length));return d});var Q=e.event.add,x=e.event.remove,k=e.event.trigger,N=e.fn.toggle,C=e.fn.live,T=e.fn.die,M="ajaxStart|ajaxStop|ajaxSend|ajaxComplete|ajaxError|ajaxSuccess",S=RegExp("\\b(?:"+M+")\\b"),H=/(?:^|\s)hover(\.\S+|)\b/,A=function(t){return"string"!=typeof t||e.event.special.hover?t:(H.test(t)&&r("'hover' pseudo-event is deprecated, use 'mouseenter mouseleave'"),t&&t.replace(H,"mouseenter$1 mouseleave$1"))};e.event.props&&"attrChange"!==e.event.props[0]&&e.event.props.unshift("attrChange","attrName","relatedNode","srcElement"),e.event.dispatch&&a(e.event,"handle",e.event.dispatch,"jQuery.event.handle is undocumented and deprecated"),e.event.add=function(e,t,n,a,o){e!==document&&S.test(t)&&r("AJAX events should be attached to document: "+t),Q.call(this,e,A(t||""),n,a,o)},e.event.remove=function(e,t,n,r,a){x.call(this,e,A(t)||"",n,r,a)},e.fn.error=function(){var e=Array.prototype.slice.call(arguments,0);return r("jQuery.fn.error() is deprecated"),e.splice(0,0,"error"),arguments.length?this.bind.apply(this,e):(this.triggerHandler.apply(this,e),this)},e.fn.toggle=function(t,n){if(!e.isFunction(t)||!e.isFunction(n))return N.apply(this,arguments);r("jQuery.fn.toggle(handler, handler...) is deprecated");var a=arguments,o=t.guid||e.guid++,i=0,s=function(n){var r=(e._data(this,"lastToggle"+t.guid)||0)%i;return e._data(this,"lastToggle"+t.guid,r+1),n.preventDefault(),a[r].apply(this,arguments)||!1};for(s.guid=o;a.length>i;)a[i++].guid=o;return this.click(s)},e.fn.live=function(t,n,a){return r("jQuery.fn.live() is deprecated"),C?C.apply(this,arguments):(e(this.context).on(t,this.selector,n,a),this)},e.fn.die=function(t,n){return r("jQuery.fn.die() is deprecated"),T?T.apply(this,arguments):(e(this.context).off(t,this.selector||"**",n),this)},e.event.trigger=function(e,t,n,a){return!n&!S.test(e)&&r("Global events are undocumented and deprecated"),k.call(this,e,t,n||document,a)},e.each(M.split("|"),function(t,n){e.event.special[n]={setup:function(){var t=this;return t!==document&&(e.event.add(document,n+"."+e.guid,function(){e.event.trigger(n,null,t,!0)}),e._data(this,n,e.guid++)),!1},teardown:function(){return this!==document&&e.event.remove(document,n+"."+e._data(this,n)),!1}}})}(jQuery,window); 3 | //@ sourceMappingURL=dist/jquery-migrate.min.map -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | Salesforce.com Org Compare Tool 6 | 7 | 8 | 9 | 10 | 11 | {% block css %}{% endblock %} 12 | 13 | 14 | 17 | 18 | 19 | 20 | 21 | {% block scripts %}{% endblock %} 22 | 32 | 33 | 34 | 35 | 36 | {% if request.GET.noheader != '1' %} 37 | 38 | 39 | 93 | 94 | 124 | 125 |
126 | 127 | {% endif %} 128 | 129 |
130 | {% block content %}{% endblock %} 131 |
132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /static/js/compare.js: -------------------------------------------------------------------------------- 1 | $.SyntaxHighlighter.init(); 2 | 3 | $(document).ready(function () 4 | { 5 | $('tr.component').hide(); 6 | $('tr.success').hide(); 7 | 8 | // Toggle file show and hide 9 | $('tr.type td').click(function() 10 | { 11 | var componentType = $(this).parent().attr('class').split('_')[1]; 12 | $('.component_' + componentType).toggle(); 13 | 14 | if ( $('#display_option').val() == 'diff') 15 | { 16 | $('tr.success').hide(); 17 | } 18 | 19 | }); 20 | 21 | // Open code view modal 22 | $('tr.component td').click(function() 23 | { 24 | // Set loading gif while metadata loads 25 | $('#codeModalBody').html('Loading'); 26 | 27 | // Component type of the cell clicked 28 | var componentType = $(this).parent().attr('class').split('_')[1].trim(); 29 | var componentName = $(this).text().trim(); 30 | 31 | // Set label of the modal 32 | $('#codeModalLabel').text(componentType + '/' + componentName); 33 | 34 | // If diff file - query for diff HTML that Python generated 35 | if ( $(this).hasClass('diff') ) 36 | { 37 | $.ajax( 38 | { 39 | url: '/get_diffhtml/' + $(this).attr('id'), 40 | type: 'get', 41 | success: function(resp) 42 | { 43 | $('#codeModalBody').html(resp); 44 | // Remove nowrap attribute. This is handled better with CSS. 45 | $('#codeModalBody td[nowrap="nowrap"]').removeAttr('nowrap'); 46 | }, 47 | failure: function(resp) 48 | { 49 | $('#codeModalBody').html('