Permalink
Browse files

Repo migration

  • Loading branch information...
ductionist committed Apr 5, 2017
1 parent a99aa8e commit 4b67129e36eaf56d75d77621bb921b7fdd84c3d9
Showing with 1,285 additions and 0 deletions.
  1. +56 −0 .gitignore
  2. +1 −0 Procfile
  3. +51 −0 README.md
  4. +22 −0 manage.py
  5. 0 ombudsman/__init__.py
  6. +8 −0 ombudsman/admin.py
  7. +24 −0 ombudsman/debug.py
  8. +98 −0 ombudsman/decorators.py
  9. +32 −0 ombudsman/forms.py
  10. +38 −0 ombudsman/helpers.py
  11. +27 −0 ombudsman/migrations/0001_initial.py
  12. +30 −0 ombudsman/migrations/0002_auto_20170329_2342.py
  13. +20 −0 ombudsman/migrations/0003_auto_20170401_0229.py
  14. 0 ombudsman/migrations/__init__.py
  15. +14 −0 ombudsman/models.py
  16. +169 −0 ombudsman/settings.py
  17. +51 −0 ombudsman/test.py
  18. +37 −0 ombudsman/urls.py
  19. +171 −0 ombudsman/views.py
  20. +16 −0 ombudsman/wsgi.py
  21. +11 −0 requirements.txt
  22. +103 −0 static/css/typenugget-bright.css
  23. +116 −0 static/css/typenugget.css
  24. BIN static/favicon/android-icon-144x144.png
  25. BIN static/favicon/android-icon-192x192.png
  26. BIN static/favicon/android-icon-36x36.png
  27. BIN static/favicon/android-icon-48x48.png
  28. BIN static/favicon/android-icon-72x72.png
  29. BIN static/favicon/android-icon-96x96.png
  30. BIN static/favicon/apple-icon-114x114.png
  31. BIN static/favicon/apple-icon-120x120.png
  32. BIN static/favicon/apple-icon-144x144.png
  33. BIN static/favicon/apple-icon-152x152.png
  34. BIN static/favicon/apple-icon-180x180.png
  35. BIN static/favicon/apple-icon-57x57.png
  36. BIN static/favicon/apple-icon-60x60.png
  37. BIN static/favicon/apple-icon-72x72.png
  38. BIN static/favicon/apple-icon-76x76.png
  39. BIN static/favicon/apple-icon-precomposed.png
  40. BIN static/favicon/apple-icon.png
  41. +2 −0 static/favicon/browserconfig.xml
  42. BIN static/favicon/favicon-16x16.png
  43. BIN static/favicon/favicon-32x32.png
  44. BIN static/favicon/favicon-96x96.png
  45. BIN static/favicon/favicon.ico
  46. +41 −0 static/favicon/manifest.json
  47. BIN static/favicon/ms-icon-144x144.png
  48. BIN static/favicon/ms-icon-150x150.png
  49. BIN static/favicon/ms-icon-310x310.png
  50. BIN static/favicon/ms-icon-70x70.png
  51. BIN static/img/integrations@2x.png
  52. +147 −0 templates/home.html
@@ -0,0 +1,56 @@
.DS_Store

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]

# C extensions
*.so

# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg

# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.cache
nosetests.xml
coverage.xml

# Translations
*.mo
*.pot

# Django stuff:
*.log

# Sphinx documentation
docs/_build/

# PyBuilder
target/
@@ -0,0 +1 @@
web: gunicorn ombudsman.wsgi --log-file -
@@ -0,0 +1,51 @@
# [Ombudsman](https://ombudsman.user.camp) by [User Camp](https://user.camp)

Ombudsman is a Zapier integration that sends triggers whenever new data is available in your Windows Dev Center.

## Usage

Go to [https://ombudsman.user.camp](https://ombudsman.user.camp) to generate a User Camp API key.

Your Windows Developer account needs to be associated with an Azure AD that you are an admin of. You need to create an Azure
AD Application.

You need to join the [private Ombudsman Zapier application](https://zapier.com/developer/invite/61854/389dc5ae8662f71159d766f347d8ad2c/).

Full setup docs available at https://ombudsman.user.camp .

## Deploying it yourself

Ombudsman is built with Django and is meant to be deployed on Heroku.

To deploy Ombudsman yourself, make sure you have the following enviornment variables set:

- `OMBUDSMAN_DJANGO_SECRET_KEY`
- `OMBUDSMAN_ROLLBAR_ACCESS_TOKEN`, a token from [Rollbar](https://rollbar.com), used for error reporting
- `OMBUDSMAN_SENDGRID_USERNAME` from [Sendgrid](http://sendgrid.com/), used for sending out API keys
- `OMBUDSMAN_SENDGRID_PASSWORD`
- `OMBUDSMAN_RECAPTCHA_PUBLIC_KEY` from [reCAPTCHA](https://www.google.com/recaptcha/intro/invisible.html), for preventing abuse of the API signup form
- `OMBUDSMAN_RECAPTCHA_PRIVATE_KEY`

## License

(MIT)

Copyright (c) 2017 User Camp

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,22 @@
#!/usr/bin/env python
import os
import sys

if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ombudsman.settings")
try:
from django.core.management import execute_from_command_line
except ImportError:
# The above import may fail for some other reason. Ensure that the
# issue is really that Django is missing to avoid masking other
# exceptions on Python 2.
try:
import django
except ImportError:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
)
raise
execute_from_command_line(sys.argv)
No changes.
@@ -0,0 +1,8 @@
from django.contrib import admin
from ombudsman.models import *

class ApiKeyGrantAdmin(admin.ModelAdmin):
list_display = ('name', 'email', 'created', 'updated')
date_hierarchy = 'created'

admin.site.register(ApiKeyGrant, ApiKeyGrantAdmin)
@@ -0,0 +1,24 @@
from ombudsman.settings import *

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

SECURE_SSL_REDIRECT = False
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

ALLOWED_HOSTS.append("localhost")

DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'ombudsman',
'USER': 'ben',
'PASSWORD': 'password',
'HOST': 'localhost',
'PORT': '',
}
}

INSTALLED_APPS.append('debug_toolbar')
MIDDLEWARE.append('debug_toolbar.middleware.DebugToolbarMiddleware')
MIDDLEWARE.remove('rollbar.contrib.django.middleware.RollbarNotifierMiddleware')
@@ -0,0 +1,98 @@
from django.core.exceptions import ObjectDoesNotExist
from django.http import HttpResponseForbidden, HttpResponseBadRequest, HttpResponseNotAllowed

from datetime import datetime, date, timedelta
import requests, json

from ombudsman.models import ApiKeyGrant
from ombudsman.settings import AZURE_AD_ENDPOINT

def verify_user_camp_authorization(view_func):
'''
Checks that the `uc_api_key` query string parameter is present, and that its value is a valid
User Camp API key.
Also checks that the request method is GET.
'''
def decorator(request, *args, **kwargs):
if request.method != "GET":
return HttpResponseNotAllowed()

try:
api_key = request.GET['uc_api_key']
except KeyError:
return HttpResponseForbidden("403 Forbidden uc_api_key was missing")

try:
grant = ApiKeyGrant.objects.get(api_key=api_key)
except (ObjectDoesNotExist, ValueError):
return HttpResponseForbidden("403 Forbidden. The API key provided is invalid.")

return view_func(request, *args, **kwargs)
return decorator


def prepare_microsoft_authorization_header(view_func):
'''
Views with this decorator need to accept `microsoft_authorization_header`, a dictionary that contains the
access token necessary to query the Microsoft Store Analytics API.
'''
def decorator(request, *args, **kwargs):
# Make sure the query string contains the four required Azure AD parameters
azure_ad = {}
try:
azure_ad['tenant_id'] = request.GET['azure_ad_tenant_id']
azure_ad['client_id'] = request.GET['azure_ad_client_id']
azure_ad['application_key'] = request.GET['azure_ad_application_key']
azure_ad['app_id_uri'] = request.GET['azure_ad_app_id_uri']

except KeyError:
return HttpResponseBadRequest("400 Bad Request. Make sure all Azure AD parameters are present.")

auth_data = {
'resource': 'https://manage.devcenter.microsoft.com',
'response_type': 'client_credentials',
'client_id': azure_ad['client_id'],
'client_secret': azure_ad['application_key'],
'grant_type': 'client_credentials',
'app_id_uri': azure_ad['app_id_uri'],
}

auth = requests.post(AZURE_AD_ENDPOINT.format(azure_ad['tenant_id']), data=auth_data)
auth_response = json.loads(auth.text)

token = auth_response['access_token']
microsoft_authorization_header = {"Authorization": "Bearer {}".format(token)}

return view_func(request, microsoft_authorization_header, *args, **kwargs)
return decorator

def prepare_store_api_aggregate_parameters(view_func):
'''
Prepares a common set of parameters for all Store API requests
'''
def decorator(request, *args, **kwargs):
# Since this data is often quite delayed, use a bigger time delta.
today = date.today()
start_date = today - timedelta(days=6)

store_api_aggregate_parameters = {
"applicationId": kwargs['app_store_id'],
"orderby": "date desc",
"startDate": str(start_date)
}

# Optional filter. If the request has ?filter=all, do not add the filter key to the eventual request
# to the API.
result_filter = request.GET.get('filter')
if result_filter and result_filter != "none":
store_api_aggregate_parameters['filter'] = result_filter

# Optional groupby. Defaults to `date` for aggregate requests.
groupby = request.GET.get('groupby')
if groupby and groupby != "none":
store_api_aggregate_parameters['groupby'] = groupby

return view_func(request, store_api_aggregate_parameters, *args, **kwargs)
return decorator

@@ -0,0 +1,32 @@
from django.forms import ModelForm

from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Fieldset, ButtonHolder, Submit, Field
from crispy_forms.bootstrap import StrictButton

from captcha.fields import ReCaptchaField

from ombudsman.models import ApiKeyGrant

class ApiKeyGrantForm(ModelForm):
captcha = ReCaptchaField(attrs={
'theme' : 'clean',
})
def __init__(self, *args, **kwargs):
super(ApiKeyGrantForm, self).__init__(*args, **kwargs)
self.fields['captcha'].label = False
self.helper = FormHelper()
self.helper.layout = Layout(
Field('name'),
Field('email'),
Field('app_url'),
Field('captcha')
)
self.helper.add_input(Submit('submit', 'Submit'))
self.helper.form_tag = True
self.helper.form_method = 'post'
self.helper.form_action = ''

class Meta:
model = ApiKeyGrant
fields = ['name', 'email', 'app_url']
@@ -0,0 +1,38 @@
import json, requests

from ombudsman.settings import ROOT_API_ENDPOINT

def get_all_analytics_pages(endpoint, params, headers):
'''
Processes the initial endpoint request, and any @nextLinks it returns.
Returns one big list of all data points (the ['Value'] key in each response dictionary).
'''
pages = []
print "Getting data for {}".format(endpoint)
response = requests.get(endpoint, params=params, headers=headers)
page = json.loads(response.text)

pages.append(page)

next_link = page.get('@nextLink')

while next_link:
print "...processing @nextLink..."
next_link = ROOT_API_ENDPOINT + next_link
next_link = next_link.replace(" ", "+")

# We don't use `params` in this request, because the @nextLink contains them
response = requests.get(next_link, headers=headers)
page = json.loads(response.text)
pages.append(page)
next_link = page.get('@nextLink')

results = []
for page in pages:
try:
results += page['Value']
except KeyError:
continue

return results
@@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.6 on 2017-03-29 23:08
from __future__ import unicode_literals

from django.db import migrations, models
import uuid


class Migration(migrations.Migration):

initial = True

dependencies = [
]

operations = [
migrations.CreateModel(
name='ApiKeyGrant',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('email', models.EmailField(max_length=254)),
('app_link', models.URLField()),
('api_key', models.UUIDField(default=uuid.uuid4)),
],
),
]
@@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.6 on 2017-03-29 23:42
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('ombudsman', '0001_initial'),
]

operations = [
migrations.RenameField(
model_name='apikeygrant',
old_name='app_link',
new_name='app_url',
),
migrations.AddField(
model_name='apikeygrant',
name='created',
field=models.DateField(auto_now_add=True, null=True),
),
migrations.AddField(
model_name='apikeygrant',
name='updated',
field=models.DateField(auto_now=True, null=True),
),
]
Oops, something went wrong.

0 comments on commit 4b67129

Please sign in to comment.