From 9647882f448a871686729a2af78d82c90a6b8fbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CE=9Blisue?= Date: Sun, 17 Aug 2014 07:17:04 +0900 Subject: [PATCH] Working... --- .gitignore | 4 +- requirements.txt | 2 +- setup.py | 2 + src/kawaz/apps/events/admin.py | 2 +- src/kawaz/apps/events/google_calendar.py | 61 ++++++++ .../management/commands/login_to_google.py | 48 ------ src/kawaz/apps/events/models.py | 22 +-- src/kawaz/apps/events/tests/test_utils.py | 72 --------- src/kawaz/apps/events/utils/gcal.py | 147 ------------------ src/kawaz/core/google/__init__.py | 4 + src/kawaz/core/google/calendar/__init__.py | 5 + src/kawaz/core/google/calendar/backend.py | 114 ++++++++++++++ src/kawaz/core/google/calendar/client.py | 121 ++++++++++++++ src/kawaz/core/google/calendar/conf.py | 25 +++ .../google/calendar}/management/__init__.py | 0 .../calendar}/management/commands/__init__.py | 0 .../commands/login_to_google_calendar_api.py | 36 +++++ src/kawaz/core/google/calendar/models.py | 47 ++++++ .../core/google/calendar/requirements.txt | 4 + .../core/google/calendar/tests/__init__.py | 0 .../core/google/calendar/tests/test_client.py | 31 ++++ src/kawaz/core/google/calendar/utils.py | 46 ++++++ src/kawaz/settings.py | 18 ++- util/install_django17c1.sh | 1 + 24 files changed, 517 insertions(+), 295 deletions(-) create mode 100644 src/kawaz/apps/events/google_calendar.py delete mode 100644 src/kawaz/apps/events/management/commands/login_to_google.py delete mode 100644 src/kawaz/apps/events/tests/test_utils.py delete mode 100644 src/kawaz/apps/events/utils/gcal.py create mode 100644 src/kawaz/core/google/__init__.py create mode 100644 src/kawaz/core/google/calendar/__init__.py create mode 100644 src/kawaz/core/google/calendar/backend.py create mode 100644 src/kawaz/core/google/calendar/client.py create mode 100644 src/kawaz/core/google/calendar/conf.py rename src/kawaz/{apps/events => core/google/calendar}/management/__init__.py (100%) rename src/kawaz/{apps/events => core/google/calendar}/management/commands/__init__.py (100%) create mode 100644 src/kawaz/core/google/calendar/management/commands/login_to_google_calendar_api.py create mode 100644 src/kawaz/core/google/calendar/models.py create mode 100644 src/kawaz/core/google/calendar/requirements.txt create mode 100644 src/kawaz/core/google/calendar/tests/__init__.py create mode 100644 src/kawaz/core/google/calendar/tests/test_client.py create mode 100644 src/kawaz/core/google/calendar/utils.py create mode 100755 util/install_django17c1.sh diff --git a/.gitignore b/.gitignore index 51590835..2746f577 100644 --- a/.gitignore +++ b/.gitignore @@ -133,6 +133,6 @@ src/kawaz/statics/vendor/mace.min.js # for configs -src/kawaz/config/client_secret.json -src/kawaz/config/google_token.dat +src/kawaz/config/event/client_secrets.json +src/kawaz/config/event/credentials.dat src/kawaz/local_settings.py diff --git a/requirements.txt b/requirements.txt index 315c58fc..a8229f3f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,4 +12,4 @@ django-bootstrap-form honcho beautifulsoup4 httplib2 -git+https://github.com/enorvelle/GoogleApiPython3x.git@dd92ed3d676581f486521d66c6de88351a0e67fe#egg=google_api_python_client +git+https://github.com/enorvelle/GoogleApiPython3x.git#egg=google_api_python_client diff --git a/setup.py b/setup.py index 2de7886e..3fba8ea6 100644 --- a/setup.py +++ b/setup.py @@ -16,6 +16,8 @@ def read(filename): def readlist(filename): rows = read(filename).split("\n") rows = [x.strip() for x in rows if x.strip()] + rows = [x.split("#egg=")[1] if "#egg=" in x else x + for x in rows] return list(rows) diff --git a/src/kawaz/apps/events/admin.py b/src/kawaz/apps/events/admin.py index 84b4288f..dbed69ba 100644 --- a/src/kawaz/apps/events/admin.py +++ b/src/kawaz/apps/events/admin.py @@ -2,5 +2,5 @@ from .models import Event class EventAdmin(admin.ModelAdmin): - readonly_fields = ('gcal_id',) + pass admin.site.register(Event, EventAdmin) diff --git a/src/kawaz/apps/events/google_calendar.py b/src/kawaz/apps/events/google_calendar.py new file mode 100644 index 00000000..535a9ee3 --- /dev/null +++ b/src/kawaz/apps/events/google_calendar.py @@ -0,0 +1,61 @@ +# coding=utf-8 +""" +Google Calendar 連携用 Backend +`kawaz.core.google.calendar` に依存し settings.GOOGLE_CALENDAR_BACKEND で指定 +されている +""" +__author__ = 'Alisue ' +from django.conf import settings +from django.contrib.sites.models import Site +from kawaz.core.google.calendar.backend import Backend + + +def get_base_url(): + cache_name = '_cached_base_url' + if not hasattr(get_base_url, cache_name): + cs = Site.objects.get(pk=settings.SITE_ID) + setattr(get_base_url, cache_name, 'http://{}'.format(cs)) + return getattr(get_base_url, cache_name) + + +class KawazGoogleCalendarBackend(Backend): + def translate(self, event): + """ + Translate kawaz.apps.events.Event to body parameter of Google Calendar + API. + """ + # translation lambda functions + to_datetime = lambda x: {'datetime': self.__class__.strftime(x)} + to_visibility = lambda x: 'public' if x == 'public' else 'private' + to_source = lambda x: {'url': get_base_url() + x()} + to_attendees = lambda x: [dict(email=a.email, displayName=a.nickname) + for a in x.iterator()] + # translate + translation_table = ( + ('summary', 'title', str), + ('description', 'body', str), + ('location', 'place', str), + ('start', 'period_start', to_datetime), + ('end', 'period_end', to_datetime), + ('visibility', 'pub_state', to_visibility), + ('source', 'get_absolute_url', to_source), + ('attendees', 'attendees', to_attendees), + ) + return {k: fn(getattr(event, a)) for k, a, fn in translation_table} + + def is_valid(self, event, raise_exception=False): + """ + Check if the specified event is valid for translating to body parameter + of Google Calender API + """ + if not event.period_start or not event.period_end: + if raise_exception: + raise AttributeError('`period_start` and `period_end` ' + 'attributes are required to be filled.') + return False + elif event.pub_state == 'draft': + if raise_exception: + raise AttributeError('`pub_state` attribute is required not ' + 'to be "draft".') + return False + return True diff --git a/src/kawaz/apps/events/management/commands/login_to_google.py b/src/kawaz/apps/events/management/commands/login_to_google.py deleted file mode 100644 index 04518b08..00000000 --- a/src/kawaz/apps/events/management/commands/login_to_google.py +++ /dev/null @@ -1,48 +0,0 @@ -# ! -*- coding: utf-8 -*- -# -# created by giginet on 2014/7/30 -# -__author__ = 'giginet' - -from django.core.management.base import BaseCommand, CommandError -from django.conf import settings -from django.core.exceptions import ImproperlyConfigured - -import argparse -import os - -from oauth2client import client -from oauth2client import file -from oauth2client import tools - -class Command(BaseCommand): - """ - oAuth2のアクセストークンを取得します - - Usage: - python manage.py login_to_google - """ - def handle(self, *args, **options): - client_secrets = settings.GOOGLE_CLIENT_SECRET_PATH - storage_path = settings.GOOGLE_CREDENTIALS_PATH - scope = settings.GOOGLE_CLIENT_SCOPES - - if not os.path.exists(client_secrets): - raise ImproperlyConfigured('{] is not exist. Please put this config.'.format(client_secrets)) - parent_parsers = [tools.argparser] - parent_parsers.extend([]) - parser = argparse.ArgumentParser( - description=__doc__, - formatter_class=argparse.RawDescriptionHelpFormatter, - parents=parent_parsers) - flags = parser.parse_args(['--noauth_local_webserver']) - - flow = client.flow_from_clientsecrets(client_secrets, scope=scope) - - storage = file.Storage(storage_path) - credentials = storage.get() - if credentials is None or credentials.invalid: - tools.run_flow(flow, storage, flags) - else: - print("the credential is saved already.") - exit(0) diff --git a/src/kawaz/apps/events/models.py b/src/kawaz/apps/events/models.py index eb76f86b..75041c85 100644 --- a/src/kawaz/apps/events/models.py +++ b/src/kawaz/apps/events/models.py @@ -89,10 +89,10 @@ class Event(models.Model): verbose_name=_("Attendees"), related_name="events_attend", editable=False) - category = models.ForeignKey(Category, verbose_name=_('Category'), null=True, blank=True) + category = models.ForeignKey(Category, verbose_name=_('Category'), + null=True, blank=True) created_at = models.DateTimeField(_("Created at"), auto_now_add=True) updated_at = models.DateTimeField(_("Modified at"), auto_now=True) - gcal_id = models.CharField(_("Calendar ID"), default='', editable=False, max_length=128) objects = EventManager() @@ -211,21 +211,3 @@ def join_organizer(**kwargs): add_permission_logic(Event, EventPermissionLogic()), add_permission_logic(Event, PublishmentPermissionLogic( author_field_name='organizer')), - -from .utils.gcal import GoogleCalendarUpdater -@receiver(post_save, sender=Event) -def update_gcal(sender, instance, created, **kwargs): - """ - イベント作成、更新時にGoogleカレンダーと同期するシグナルレシーバー - """ - updater = GoogleCalendarUpdater() - updater.update_event(instance, created) - - -@receiver(post_delete, sender=Event) -def delete_gcal(sender, instance, **kwargs): - """ - イベント削除時に、Googleカレンダーから削除するシグナルレシーバー - """ - updater = GoogleCalendarUpdater() - updater.delete_event(instance) diff --git a/src/kawaz/apps/events/tests/test_utils.py b/src/kawaz/apps/events/tests/test_utils.py deleted file mode 100644 index 0ae5848f..00000000 --- a/src/kawaz/apps/events/tests/test_utils.py +++ /dev/null @@ -1,72 +0,0 @@ -# ! -*- coding: utf-8 -*- -# -# created by giginet on 2014/7/30 -# -__author__ = 'giginet' - -from django.test import TestCase -from django.test.utils import override_settings -from django.utils import timezone -from kawaz.core.personas.tests.factories import PersonaFactory -from .factories import EventFactory -from ..utils.gcal import GoogleCalendarUpdater - -@override_settings(GOOGLE_CALENDAR_ID='hoge') -class GoogleCalendarUploaderTestCase(TestCase): - def test_body_from_event(self): - """ - Eventから正しい辞書を取り出せる - """ - now = timezone.now() - period_start = now + timezone.timedelta(3) - period_end = now + timezone.timedelta(4) - e = EventFactory(title="geekdrums爆発しろ会", - body="爆発させます", - period_start=period_start, - period_end=period_end, - place='安息の地' - ) - updater = GoogleCalendarUpdater() - body = updater.body_from_event(e) - self.assertEqual(body['summary'], e.title) - self.assertEqual(body['description'], e.body) - self.assertEqual(body['start']['dateTime'], e.period_start.strftime('%Y-%m-%dT%H:%M:%S.000%z')) - self.assertEqual(body['end']['dateTime'], e.period_end.strftime('%Y-%m-%dT%H:%M:%S.000%z')) - self.assertEqual(body['location'], e.place) - self.assertEqual(body['visibility'], 'public') - self.assertEqual(len(body['attendees']), 1) - self.assertEqual(body['attendees'][0]['displayName'], e.organizer.nickname) - self.assertEqual(body['attendees'][0]['email'], e.organizer.email) - self.assertEqual(body['source']['url'], '{}{}'.format('http://example.com', e.get_absolute_url())) - - - def test_body_from_event_private(self): - """ - ProtectedなEventはVisibilityがprivateになる - """ - e = EventFactory(pub_state='protected') - updater = GoogleCalendarUpdater() - body = updater.body_from_event(e) - self.assertEqual(body['visibility'], 'private') - - - def test_body_from_event_attendee(self): - """ - 参加者が増えたら、attendeeの値も更新される - """ - e = EventFactory() - updater = GoogleCalendarUpdater() - user0 = PersonaFactory() - user1 = PersonaFactory() - e.attend(user0) - e.attend(user1) - body = updater.body_from_event(e) - - self.assertEqual(len(body['attendees']), 3) - self.assertEqual(body['attendees'][0]['displayName'], e.organizer.nickname) - self.assertEqual(body['attendees'][0]['email'], e.organizer.email) - self.assertEqual(body['attendees'][1]['displayName'], user0.nickname) - self.assertEqual(body['attendees'][1]['email'], user0.email) - self.assertEqual(body['attendees'][2]['displayName'], user1.nickname) - self.assertEqual(body['attendees'][2]['email'], user1.email) - diff --git a/src/kawaz/apps/events/utils/gcal.py b/src/kawaz/apps/events/utils/gcal.py deleted file mode 100644 index e2e6f105..00000000 --- a/src/kawaz/apps/events/utils/gcal.py +++ /dev/null @@ -1,147 +0,0 @@ -# ! -*- coding: utf-8 -*- -# -# created by giginet on 2014/7/28 -# -__author__ = 'giginet' -from django.conf import settings -from django.contrib.sites.models import Site -import httplib2 - -from apiclient import discovery -from oauth2client import file - -STRFTIME_FORMAT = '%Y-%m-%dT%H:%M:%S.000%z' - -class GoogleCalendarUpdater(object): - """ - EventをGoogleカレンダーと同期するための操作をまとめたクラスです - 主にEvent更新、削除時のSignalとして呼ばれることを想定しています - """ - - def __init__(self): - self.service = None - if not settings.GOOGLE_CALENDAR_ID: - return - try: - self.service = self._login() - except: - pass - - def body_from_event(self, event): - """ - Eventオブジェクトから、Google Calendar API V3に送信するためのJSONを作ります - 詳細は以下のドキュメントを参考にしてください - Ref : https://developers.google.com/google-apps/calendar/v3/reference/events/insert - - param event [Event] Eventインスタンス - return [Dictionary] - """ - if not event.period_start or not event.period_end: - raise Exception("Event instance must be have `period_start` and `period_end`.") - current_site = Site.objects.get(pk=settings.SITE_ID) - base_url = 'http://{}'.format(current_site) - - - body = { - 'summary': event.title, - 'description': event.body, - 'location': event.place, - 'start': { - 'dateTime': event.period_start.strftime(STRFTIME_FORMAT) - }, - 'end': { - 'dateTime': event.period_end.strftime(STRFTIME_FORMAT) - }, - 'visibility': 'public' if event.pub_state == 'public' else 'private', - 'source': { - 'url': base_url + event.get_absolute_url() - }, - 'attendees':[] - } - - for attendee in event.attendees.all(): - user = { - 'email': attendee.email, - 'displayName': attendee.nickname - } - body['attendees'].append(user) - return body - - def _login(self): - """ - oAuth2で取得したACCESS_TOKENを利用してGoogleにログインします - このメソッドを実行する前に、必ず`python manage.py login_to_google`を実行し、 - `GOOGLE_CREDENTIALS_PATH`に認証情報が保存されている必要があります - """ - storage_path = settings.GOOGLE_CREDENTIALS_PATH - - storage = file.Storage(storage_path) - credentials = storage.get() - if credentials is None or credentials.invalid: - raise Exception("You must execute `python manage.py login_to_google` to fetch access_token.") - http = credentials.authorize(http = httplib2.Http()) - - service = discovery.build('calendar', 'v3', http=http) - return service - - def update_event(self, instance, created): - """ - EventをGoogle Calendarと同期します - - param instance [Event] 同期するEventオブジェクト - param created [Boolean] 新規作成かどうか - - もし、Googleカレンダーにイベントが存在していなければ新規作成します - すでに作られていて、編集されていれば更新します。 - すでに作られているが、開催日が未定になったり、下書き状態に切り替わっていたとき、削除します - """ - if not self.service: - return - if not instance.period_start or not instance.period_end or instance.pub_state == 'draft': - # 開始日、終了日が設定されていない、もしくは下書き状態のイベントは無視する - if instance.gcal_id: - # もしカレンダー同期設定済みだったら、削除する - self.delete_event(instance) - instance.gcal_id = '' - instance.save() - return - - kwargs = {} - if instance.number_restriction: - # もし、最大人数が設定されてたら、maxAttendeesを指定する - kwargs['maxAttendees'] - - if created or not instance.gcal_id: - # 新規作成、もしくは同期されていなかったとき - body = self.body_from_event(instance) - try: - created_event = self.service.events().insert(calendarId=settings.GOOGLE_CALENDAR_ID, body=body, **kwargs).execute() - instance.gcal_id = created_event['id'] - instance.save() - except: - pass - else: - # 更新するとき - gcal_id = instance.gcal_id - body = self.body_from_event(instance) - try: - # updateじゃなくてpatchを使うべきらしい - # http://stackoverflow.com/questions/15926676/google-calendar-api-bad-request-400-event-over-developer-console - self.service.events().patch(calendarId=settings.GOOGLE_CALENDAR_ID, eventId=gcal_id, body=body, **kwargs).execute() - except: - pass - - def delete_event(self, instance): - """ - 渡されたEventをGoogle Calendarから削除します - - param instance [Event] 削除するEventオブジェクト - """ - if not self.service: - return - if instance.gcal_id: - return - try: - self.service.events().delete(calendarId=settings.GOOGLE_CALENDAR_ID, eventId=instance.gcal_id).execute() - except: - return diff --git a/src/kawaz/core/google/__init__.py b/src/kawaz/core/google/__init__.py new file mode 100644 index 00000000..6a5b2ff6 --- /dev/null +++ b/src/kawaz/core/google/__init__.py @@ -0,0 +1,4 @@ +# coding=utf-8 +""" +""" +__author__ = 'Alisue ' diff --git a/src/kawaz/core/google/calendar/__init__.py b/src/kawaz/core/google/calendar/__init__.py new file mode 100644 index 00000000..ab642df6 --- /dev/null +++ b/src/kawaz/core/google/calendar/__init__.py @@ -0,0 +1,5 @@ +# coding=utf-8 +""" +Google Calendar Synchronize django app +""" +__author__ = 'Alisue ' diff --git a/src/kawaz/core/google/calendar/backend.py b/src/kawaz/core/google/calendar/backend.py new file mode 100644 index 00000000..564fa7b9 --- /dev/null +++ b/src/kawaz/core/google/calendar/backend.py @@ -0,0 +1,114 @@ +# coding=utf-8 +""" +""" +__author__ = 'Alisue ' +import tolerance +from .conf import settings +from .utils import get_class +from .client import GoogleCalendarClient + + +RFC3339 = '%Y-%m-%dT%H:%M:%S.000%z' + + +def tolerate(fn): + """ + A function decorator to make the function fail silently if + settings.DEBUG is False or fail_silently=True is specified + """ + def switch_function(*args, **kwargs): + fail_silently = kwargs.pop('fail_silently', True) + if settings.DEBUG or not fail_silently: + return False, args, kwargs + return True, args, kwargs + return tolerance.tolerate(switch=switch_function)(fn) + + +class Backend(object): + """ + A manipulation backend between a event like model and a bridge model + """ + @classmethod + def strftime(cls, datetime): + """ + Format datetime object into RFC3339 which Google Calendar API require + """ + return datetime.strftime(RFC3339) + + @classmethod + def get_bridge(cls, event): + """ + Get a bridge instance of the specified event instance + """ + from .models import GoogleCalendarBridge + return GoogleCalendarBridge.objects.get_or_create(event=event)[0] + + def __init__(self): + self.calendar_id = settings.GOOGLE_CALENDAR_ID + self.client = GoogleCalendarClient(self.calendar_id) + + def translate(self, event): + """ + Trasnlate an event like object to a body parameter of Google Calendar + API. + + Subclass must override this method + """ + raise NotImplementedError + + def is_valid(self, event, raise_exception=False): + """ + Check if the specified event like object is valid for translating to + body parameter of Google Calender API + + Subclass should override this method + """ + return True + + @tolerate + def update(self, event, **kwargs): + """ + Update an event on Google Calendar of the specified event instance + """ + bridge = self.__class__.get_bridge(event) + if bridge.gcal_event_id: + # patch or delete event + if self.is_valid(event): + self.client.patch(bridge.gcal_event_id, + self.translate(event), **kwargs) + else: + self.client.delete(bridge.gcal_event_id) + bridge.delete() + elif self.is_valid(event): + gcal_event_id = self.client.insert(self.translate(event), + **kwargs) + if gcal_event_id is not None: + bridge.gcal_event_id = gcal_event_id + bridge.save() + + @tolerate + def delete(self, event): + """ + Delete an event on Google Calendar of the specified event instance + """ + bridge = self.__class__.get_bridge(event) + if bridge.event_id: + self.client.delete(bridge.event_id) + bridge.delete() + + +def get_backend_class(): + """ + Get a backend class + """ + return get_class(settings.GOOGLE_CALENDAR_BACKEND_CLASS) + + +def get_backend(): + """ + Get a backend instance + """ + cache_name = '_cached_backend_instance' + if not hasattr(get_backend, cache_name): + setattr(get_backend, cache_name, get_backend_class()()) + return getattr(get_backend, cache_name) diff --git a/src/kawaz/core/google/calendar/client.py b/src/kawaz/core/google/calendar/client.py new file mode 100644 index 00000000..7d6b8d83 --- /dev/null +++ b/src/kawaz/core/google/calendar/client.py @@ -0,0 +1,121 @@ +# coding=utf-8 +""" +Google Calendar manipulation library (Raw level) +""" +__author__ = 'Alisue ' +import warnings +from argparse import ArgumentParser +import httplib2 + +# require: google_api_python_client +from apiclient import discovery +from oauth2client.client import flow_from_clientsecrets +from oauth2client.file import Storage +from oauth2client import tools + +from .conf import settings + + +class ImproperlyConfiguredWarning(UserWarning): + """ + A warning class which indicate an improper configur + """ + pass + + +def require_enabled(method): + """ + A method decorator which return None if self.enabled is False, otherwise + it execute the decorated method and return the result + """ + def inner(self, *args, **kwargs): + if not self.enabled: + return None + return method(self, *args, **kwargs) + return inner + + +class GoogleCalendarClient(object): + """ + A raw level google calendar API client class + """ + @staticmethod + def console_login(flags=None): + """ + Logged into Google Calendar API with a console and store the obtained + credentials object in a file storage. + """ + SCOPE = 'https://www.googleapis.com/auth/calendar' + CLIENT_SECRETS = settings.GOOGLE_CALENDAR_CLIENT_SECRETS + FILENAME = settings.GOOGLE_CALENDAR_CREDENTIALS + flow = flow_from_clientsecrets(CLIENT_SECRETS, scope=SCOPE) + storage = Storage(FILENAME) + credentials = storage.get() + if credentials is None or credentials.invalid: + flags = {} if flags is None else flags + try: + tools.run_flow(flow, storage, flags) + return True + except Exception: + raise + else: + print(("An access token is already exists in {}. " + "Remove the file if you need to logout manually." + ).format(FILENAME)) + return False + + def __init__(self, calendar_id): + self.calendar_id = calendar_id + # Get credentials + storage = Storage(settings.GOOGLE_CALENDAR_CREDENTIALS) + credentials = storage.get() + # Login Google API with a credentials + if credentials is None or credentials.invalid: + COMMAND_NAME = 'login_to_google_calendar_api' + warnings.warn(('No valid Google API credentials are available. ' + 'python manage.py {} is ' + 'required to be called before enabling Google ' + 'Calendar Sync. ' + 'The Google Calendar Sync feature is disabled now.' + ).format(COMMAND_NAME), + category=ImproperlyConfiguredWarning) + self.enabled = False + else: + http = credentials.authorize(htpp=httplib2.Http()) + self.service = discovery.build('calendar', 'v3', http=http) + self.enabled = True + + @property + def _client(self): + return self.service.events() + + @require_enabled + def insert(self, event, **kwargs): + """ + Insert a google calendar event + """ + created = self._client.insert(calendarId=self.calendar_id, + body=event, + **kwargs).execute() + return created + + @require_enabled + def patch(self, event_id, event, **kwargs): + """ + Patch the google calendar event specified by event_id + """ + patched = self._client.patch(calendarId=self.calendar_id, + eventId=event_id, + body=event, + **kwargs).execute() + return patched + + @require_enabled + def delete(self, event_id, event, **kwargs): + """ + Delete the google calendar event specified by event_id + """ + self._client.delete(calendarId=self.calendar_id, + eventId=event_id, + **kwargs).execute() + return None diff --git a/src/kawaz/core/google/calendar/conf.py b/src/kawaz/core/google/calendar/conf.py new file mode 100644 index 00000000..3544ec5b --- /dev/null +++ b/src/kawaz/core/google/calendar/conf.py @@ -0,0 +1,25 @@ +# coding=utf-8 +""" +""" +__author__ = 'Alisue ' +from django.conf import settings +from appconf import AppConf + + +class GoogleCalendarAppConf(AppConf): + """Google Calendar configures""" + + # A target event model + EVENT_MODEL = None + + # A backend class + BACKEND_CLASS = None + + # Credentials (file) + CREDENTIALS = None + + # CLIENT_SECRETS (file) + CLIENT_SECRETS = None + + class Meta: + prefix = 'google_calendar' diff --git a/src/kawaz/apps/events/management/__init__.py b/src/kawaz/core/google/calendar/management/__init__.py similarity index 100% rename from src/kawaz/apps/events/management/__init__.py rename to src/kawaz/core/google/calendar/management/__init__.py diff --git a/src/kawaz/apps/events/management/commands/__init__.py b/src/kawaz/core/google/calendar/management/commands/__init__.py similarity index 100% rename from src/kawaz/apps/events/management/commands/__init__.py rename to src/kawaz/core/google/calendar/management/commands/__init__.py diff --git a/src/kawaz/core/google/calendar/management/commands/login_to_google_calendar_api.py b/src/kawaz/core/google/calendar/management/commands/login_to_google_calendar_api.py new file mode 100644 index 00000000..6c97deaf --- /dev/null +++ b/src/kawaz/core/google/calendar/management/commands/login_to_google_calendar_api.py @@ -0,0 +1,36 @@ +import os +import argparse +from django.conf import settings +from django.core.management.base import BaseCommand +from django.core.exceptions import ImproperlyConfigured +from oauth2client import tools +from ...client import GoogleCalendarClient + + +class Command(BaseCommand): + """ + Login to Google Calendar API and store an obtained credentials into a file + storage. This command is required to be called for enabling Google Calendar + Sync feature. + + Usage: + python manage.py login_to_google_calendar_api + """ + def handle(self, *args, **options): + client_secrets = settings.GOOGLE_CALENDAR_CLIENT_SECRETS + if not os.path.exists(client_secrets): + raise ImproperlyConfigured(('Google Calendar API client secrets ' + '({}) is not found. ' + 'Please create a correct client ' + 'secrets json file and try again.' + ).format(client_secrets)) + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + parents=[tools.argparser], + ) + import sys + flags = parser.parse_args(['--noauth_local_webserver']) + + if not GoogleCalendarClient.console_login(flags): + exit(1) diff --git a/src/kawaz/core/google/calendar/models.py b/src/kawaz/core/google/calendar/models.py new file mode 100644 index 00000000..df6f0f4f --- /dev/null +++ b/src/kawaz/core/google/calendar/models.py @@ -0,0 +1,47 @@ +# coding=utf-8 +""" +""" +__author__ = 'Alisue ' +from django.db import models +from django.utils.translation import ugettext as _ +from django.db.models.signals import post_save +from django.db.models.signals import post_delete +from django.dispatch import receiver + +from .conf import settings +from .utils import get_model +from .backend import get_backend + + +class GoogleCalendarBridge(models.Model): + """ + A bridge model which enable Google Calendar Sync with Event like model + """ + event = models.OneToOneField(settings.GOOGLE_CALENDAR_EVENT_MODEL, + primary_key=True) + gcal_event_id = models.CharField(_("Google Calendar Event ID"), + default='', editable=False, + blank=True, max_length=128) + + +def get_event_model(): + """ + Get an event model class from settings.GCAL_EVENT_MODEL + """ + return get_model(settings.GOOGLE_CALENDAR_EVENT_MODEL) + + +@receiver(post_save) +def update_google_calendar(sender, instance, created, **kwargs): + model = get_event_model() + if sender == model: + backend = get_backend() + backend.update(instance) + + +@receiver(post_delete) +def delete_google_calendar(sender, instance, **kwargs): + model = get_event_model() + if sender == model: + backend = get_backend() + backend.delete(instance) diff --git a/src/kawaz/core/google/calendar/requirements.txt b/src/kawaz/core/google/calendar/requirements.txt new file mode 100644 index 00000000..63682383 --- /dev/null +++ b/src/kawaz/core/google/calendar/requirements.txt @@ -0,0 +1,4 @@ +httplib2 +git+https://github.com/enorvelle/GoogleApiPython3x.git#egg=google_api_python_client +tolerance +django-appconf diff --git a/src/kawaz/core/google/calendar/tests/__init__.py b/src/kawaz/core/google/calendar/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/kawaz/core/google/calendar/tests/test_client.py b/src/kawaz/core/google/calendar/tests/test_client.py new file mode 100644 index 00000000..c1d01b39 --- /dev/null +++ b/src/kawaz/core/google/calendar/tests/test_client.py @@ -0,0 +1,31 @@ +# coding=utf-8 +""" +""" +__author__ = 'Alisue ' +from django.test import TestCase +from unittest.mock import MagicMock +from ..client import require_enabled + + +class EventsGCalClientRequireEnabledDecoratorTestCase(TestCase): + def test_require_enabled_call_method(self): + """ + decorated method should be called if `enabled` is True + """ + method = MagicMock(return_value=True) + decorated = require_enabled(method) + self = MagicMock() + self.enabled = True + self.assertTrue(decorated(self)) + self.assertTrue(method.called) + + def test_require_enabled_do_not_call_method(self): + """ + decorated method should not be called if `enabled` is False + """ + method = MagicMock(return_value=True) + decorated = require_enabled(method) + self = MagicMock() + self.enabled = False + self.assertIsNone(decorated(self)) + self.assertFalse(method.called) diff --git a/src/kawaz/core/google/calendar/utils.py b/src/kawaz/core/google/calendar/utils.py new file mode 100644 index 00000000..5fa09cee --- /dev/null +++ b/src/kawaz/core/google/calendar/utils.py @@ -0,0 +1,46 @@ +# coding=utf-8 +""" +""" +__author__ = 'Alisue ' +from functools import lru_cache +from importlib import import_module +from django.core.exceptions import ImproperlyConfigured + + +@lru_cache +def get_model(model): + """ + Get a model class from 'app_label.model_name' type of string + """ + if isinstance(model, str): + app_label, model_name = model.rsplit('.', 1) + model = get_model(app_label, model_name) + return model + + +@lru_cache +def get_class(path): + """ + Return a class of a given the dotted Python import path (as a string). + + If the addition cannot be located (e.g., because no such module + exists, or because the module does not contain a class of the + appropriate name), ``django.core.exceptions.ImproperlyConfigured`` + is raised. + """ + if not path: + return None + i = path.rfind('.') + module, attr = path[:i], path[i+1:] + try: + mod = import_module(module) + except ImportError as e: + raise ImproperlyConfigured( + 'Error loading a module %s: "%s"' % (module, e)) + try: + cls = getattr(mod, attr) + except AttributeError: + raise ImproperlyConfigured(( + 'Module "%s" does not define a class named "%s"' + ) % (module, attr)) + return cls diff --git a/src/kawaz/settings.py b/src/kawaz/settings.py index d00eb651..e25dee61 100644 --- a/src/kawaz/settings.py +++ b/src/kawaz/settings.py @@ -49,6 +49,7 @@ 'kawaz.core.publishments', 'kawaz.core.registrations', 'kawaz.core.templatetags', + 'kawaz.core.google.calendar', 'kawaz.apps.announcements', 'kawaz.apps.attachments', 'kawaz.apps.profiles', @@ -179,11 +180,20 @@ SITE_ID = 1 -# Google API -GOOGLE_CLIENT_SECRET_PATH = os.path.join(REPOSITORY_ROOT, 'src', 'kawaz', 'config', 'client_secret.json') -GOOGLE_CREDENTIALS_PATH = os.path.join(REPOSITORY_ROOT, 'src', 'kawaz', 'config', 'google_token.dat') -GOOGLE_CLIENT_SCOPES = ['https://www.googleapis.com/auth/calendar',] +# Google Calendar Synchronize GOOGLE_CALENDAR_ID = '' +GOOGLE_CALENDAR_EVENT_MODEL = 'events.Event' +GOOGLE_CALENDAR_BACKEND_CLASS = ( + 'kawaz.apps.events.google_calendar.GoogleCalendarBackend') +GOOGLE_CALENDAR_CLIENT_SECRETS = os.path.join(REPOSITORY_ROOT, + 'src', 'kawaz', 'config', + 'events', + 'client_secrets.json') +GOOGLE_CALENDAR_CREDENTIALS = os.path.join(REPOSITORY_ROOT, + 'src', + 'kawaz', 'config', + 'events', 'credentials.dat') + if DEBUG: # テスト時のRuntimeWarningをexceptionにしている diff --git a/util/install_django17c1.sh b/util/install_django17c1.sh new file mode 100755 index 00000000..41408238 --- /dev/null +++ b/util/install_django17c1.sh @@ -0,0 +1 @@ +pip install https://www.djangoproject.com/download/1.7c1/tarball/