Skip to content

Commit

Permalink
Merge pull request #375 from DjangoGirls/google-apps-integration
Browse files Browse the repository at this point in the history
Google Apps integration when deploying events
  • Loading branch information
aniav committed Jan 21, 2017
2 parents bd9302d + c0172a6 commit 28294e3
Show file tree
Hide file tree
Showing 9 changed files with 181 additions and 129 deletions.
13 changes: 13 additions & 0 deletions README.md
Expand Up @@ -184,3 +184,16 @@ Key bits of config and secrets are stored in environment variables in two places

* in the WSGI file (linked from the Web Tab)
* in the virtualenv postactivate at ~/.virtualenvs/djangogirls.com/bin/postactivate


### Google Apps API integration

We're using Google Apps Admin SDK for creating email accounts in djangogirls.org domain automatically.

Several things were needed to get this working:

1. Create an app in Developer Console
2. Create a service account to enable 2 legged oauth (https://developers.google.com/identity/protocols/OAuth2ServiceAccount)
3. Enable delegation of domain-wide authority for the service account.
4. Enable Admin SDK for the domain.
5. Give the service account permission to access admin.directory.users service (https://admin.google.com/AdminHome?chromeless=1#OGX:ManageOauthClients).
116 changes: 116 additions & 0 deletions core/gmail_accounts.py
@@ -0,0 +1,116 @@
from httplib2 import Http

from django.conf import settings
from django.utils.crypto import get_random_string
from apiclient.errors import HttpError
from apiclient.discovery import build
from oauth2client.service_account import ServiceAccountCredentials

from core.models import Event


GAPPS_JSON_CREDENTIALS = {
"type": "service_account",
"project_id": "djangogirls-website",
"private_key_id": settings.GAPPS_PRIVATE_KEY_ID,
"private_key": settings.GAPPS_PRIVATE_KEY,
"client_email": "django-girls-website@djangogirls-website.iam.gserviceaccount.com",
"client_id": "114585708723701029855",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://accounts.google.com/o/oauth2/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/django-girls-website%40djangogirls-website.iam.gserviceaccount.com"
}


def get_gapps_client():
credentials = ServiceAccountCredentials.from_json_keyfile_dict(
GAPPS_JSON_CREDENTIALS,
scopes=settings.GAPPS_ADMIN_SDK_SCOPES
)

delegated_credentials = credentials.create_delegated('hello@djangogirls.org')
http_auth = delegated_credentials.authorize(Http())

return build('admin', 'directory_v1', http=http_auth)


def make_email(slug):
"""Get the email address for the given slug"""
return '%s@djangogirls.org' % slug


def create_gmail_account(event):
"""
Create a new account
"""
email = event.email
password = get_random_string(length=10)
get_gapps_client().users().insert(body={
"primaryEmail": email,
"name": {
"fullName": event.name,
"givenName": "Django Girls",
"familyName": event.city,
},
"password": password,
"changePasswordAtNextLogin": True,
}).execute()

return (email, password)


def migrate_gmail_account(slug):
"""
Change the name of an account
"""
old_email = make_email(slug)
old_event = Event.objects.filter(email=old_email).order_by('-id').first()
new_email = make_email(slug+str(old_event.date.month)+str(old_event.date.year))
service = get_gapps_client()

service.users().patch(
userKey=old_email,
body={
"primaryEmail": new_email,
},
).execute()

# The old email address is kept as an alias to the new one, but we don't want this.
service.users().aliases().delete(userKey=new_email, alias=old_email).execute()

old_event.email = new_email
old_event.save()


def get_gmail_account(slug):
"""
Return the details of the given account - just pass in the slug
e.g. get_account('testcity')
"""
service = get_gapps_client()

try:
return service.users().get(userKey=make_email(slug)).execute()
except HttpError:
return None


def get_or_create_gmail(event_application, event):
"""
Function that decides whether Gmail account should be migrated,
or created. Returns a tuple of email address and password.
"""
if get_gmail_account(event_application.website_slug):
# account exists, do we need to migrate?
if event_application.has_past_team_members():
# has old organizers, so no need to do anything
return (make_email(event_application.website_slug), None)
else:
# migrate old email
migrate_gmail_account(event_application.website_slug)
# create new account
return create_gmail_account(event)
else:
# create a new account
return create_gmail_account(event)
5 changes: 5 additions & 0 deletions djangogirls/settings.py
Expand Up @@ -282,3 +282,8 @@
] + MIDDLEWARE

CODEMIRROR_PATH = "vendor/codemirror/"


GAPPS_ADMIN_SDK_SCOPES = 'https://www.googleapis.com/auth/admin.directory.user'
GAPPS_PRIVATE_KEY_ID = os.environ.get('GAPPS_PRIVATE_KEY_ID', '')
GAPPS_PRIVATE_KEY = os.environ.get('GAPPS_PRIVATE_KEY', '')
8 changes: 4 additions & 4 deletions organize/emails.py
Expand Up @@ -13,7 +13,7 @@ def send_application_confirmation(event_application):
'city': event_application.city,
'main_organizer_name': event_application.get_main_organizer_name()
})
send_email(content, subject, event_application.get_all_recipients())
send_email(content, subject, event_application.get_organizers_emails())


def send_application_notification(event_application):
Expand All @@ -31,7 +31,7 @@ def send_application_notification(event_application):
'application': event_application,
})
send_email(content, subject, ['hello@djangogirls.org'],
reply_to=event_application.get_all_recipients())
reply_to=event_application.get_organizers_emails())


def send_application_deployed_email(event_application, event, email_password):
Expand All @@ -42,7 +42,7 @@ def send_application_deployed_email(event_application, event, email_password):
'event': event,
'password': email_password,
})
recipients = event_application.get_all_recipients()
recipients = event_application.get_organizers_emails()
recipients.append(event.email) # add event's djangogirls.org email
send_email(content, subject, recipients)

Expand All @@ -56,4 +56,4 @@ def send_application_rejection_email(event_application):
content = render_to_string('emails/organize/rejection.html', {
'application': event_application
})
send_email(content, subject, event_application.get_all_recipients())
send_email(content, subject, event_application.get_organizers_emails())
23 changes: 18 additions & 5 deletions organize/models.py
Expand Up @@ -7,6 +7,7 @@
from django.utils import timezone
from django_date_extensions.fields import ApproximateDateField

from core import gmail_accounts
from core.models import Event
from core.validators import validate_approximatedate

Expand Down Expand Up @@ -94,6 +95,16 @@ def create_event(self):

return event

def has_past_team_members(self):
""" For repeated events, check whether there are any common
team members who applied to organize again
"""
previous_event = Event.objects.filter(city=self.city,
country=self.country).order_by('-id').first()
organizers = previous_event.team.all().values_list('email', flat=True)
applicants = self.get_organizers_emails()
return len(set(organizers).intersection(applicants)) > 0

@transaction.atomic
def deploy(self):
""" Deploy Event based on the current EventApplication
Expand All @@ -111,9 +122,11 @@ def deploy(self):
# copy old one or copy old and change organizaers.
event = self.create_event()

# TODO: use method created in separate branch to create gmail acconut
# and get password from it.
password = "FAKE_PASS"
# sort out Gmail accounts
email, email_password = gmail_accounts.get_or_create_gmail(
event_application=self,
event=event
)

# add main organizer of the Event
main_organizer = event.add_organizer(
Expand All @@ -136,7 +149,7 @@ def deploy(self):
send_application_deployed_email(
event_application=self,
event=event,
email_password=password
email_password=email_password
)

def clean(self):
Expand All @@ -145,7 +158,7 @@ def clean(self):
'comment': 'This field is required.'
})

def get_all_recipients(self):
def get_organizers_emails(self):
"""
Returns a list of emails to all organizers in that application
"""
Expand Down
4 changes: 2 additions & 2 deletions organize/tests/test_models.py
Expand Up @@ -23,7 +23,7 @@ def test_comment_required_for_on_hold_application(self):

def test_all_recipients(self):
event_application = EventApplication.objects.get(pk=1)
assert len(event_application.get_all_recipients()) == \
assert len(event_application.get_organizers_emails()) == \
event_application.coorganizers.count() + 1

def test_reject_method(self):
Expand All @@ -33,4 +33,4 @@ def test_reject_method(self):
event_application.status == REJECTED
assert len(mail.outbox) == 1
email = mail.outbox[0]
assert email.to == event_application.get_all_recipients()
assert email.to == event_application.get_organizers_emails()
101 changes: 0 additions & 101 deletions prototype/gmail_accounts.py

This file was deleted.

2 changes: 2 additions & 0 deletions requirements.txt
Expand Up @@ -32,6 +32,8 @@ docopt==0.4.0 # via mandrill
docutils==0.13.1 # via botocore
easy_thumbnails==2.3
freezegun==0.3.8
google-api-python-client==1.6.0
httplib2==0.9.2
icalendar==3.11.2
jmespath==0.9.0 # via boto3, botocore
lxml==3.7.2
Expand Down

0 comments on commit 28294e3

Please sign in to comment.