diff --git a/clatoolkit_project/clatoolkit/migrations/0001_initial.py b/clatoolkit_project/clatoolkit/migrations/0001_initial.py index e2aa03a..fde4bb6 100644 --- a/clatoolkit_project/clatoolkit/migrations/0001_initial.py +++ b/clatoolkit_project/clatoolkit/migrations/0001_initial.py @@ -3,6 +3,7 @@ from django.db import models, migrations import django_pgjson.fields +import django.utils.timezone from django.conf import settings @@ -74,12 +75,13 @@ class Migration(migrations.Migration): ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('xapi', django_pgjson.fields.JsonField()), ('platform', models.CharField(max_length=5000)), + ('platform_group_id', models.CharField(max_length=100, blank=True)), ('verb', models.CharField(max_length=5000)), ('platformid', models.CharField(max_length=5000, blank=True)), ('platformparentid', models.CharField(max_length=5000, blank=True)), ('parent_user_external', models.CharField(max_length=5000, null=True, blank=True)), ('message', models.TextField(blank=True)), - ('datetimestamp', models.DateTimeField(auto_now_add=True, null=True)), + ('datetimestamp', models.DateTimeField(default=django.utils.timezone.now)), ('senttolrs', models.CharField(max_length=5000, blank=True)), ('parent_user', models.ForeignKey(related_name='parent_user', to=settings.AUTH_USER_MODEL, null=True)), ], @@ -108,11 +110,13 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('platform', models.CharField(max_length=5000)), - ('verb', models.CharField(max_length=5000)), + ('platform_group_id', models.CharField(max_length=100, blank=True)), + ('type', models.CharField(max_length=100, blank=True)), + ('verb', models.CharField(max_length=5000, blank=True)), ('to_external_user', models.CharField(max_length=5000, null=True, blank=True)), ('platformid', models.CharField(max_length=5000, blank=True)), - ('message', models.TextField()), - ('datetimestamp', models.DateTimeField(blank=True)), + ('message', models.TextField(blank=True)), + ('datetimestamp', models.DateTimeField(null=True)), ('from_user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), ('to_user', models.ForeignKey(related_name='to_user', to=settings.AUTH_USER_MODEL, null=True)), ], @@ -180,6 +184,7 @@ class Migration(migrations.Migration): ('blog_id', models.CharField(max_length=255, blank=True)), ('github_account_name', models.CharField(max_length=255, blank=True)), ('trello_account_name', models.CharField(max_length=255, blank=True)), + ('accounts', django_pgjson.fields.JsonBField(null=True)), ('user', models.OneToOneField(to=settings.AUTH_USER_MODEL)), ], ), diff --git a/clatoolkit_project/clatoolkit/models.py b/clatoolkit_project/clatoolkit/models.py index 65d0c18..5b2f24a 100644 --- a/clatoolkit_project/clatoolkit/models.py +++ b/clatoolkit_project/clatoolkit/models.py @@ -1,9 +1,14 @@ from django.db import models from django.contrib.auth.models import User +from django_pgjson.fields import JsonBField +from django.core.exceptions import ValidationError from django_pgjson.fields import JsonField -from django.core.exceptions import ObjectDoesNotExist +from django.core.exceptions import ObjectDoesNotExist, SuspiciousOperation +from django.http import Http404 +from django.utils import timezone import os + class UserProfile(models.Model): ''' Custom user for data integration, uses Django's User class. @@ -52,6 +57,27 @@ class UserProfile(models.Model): #Trello user ID trello_account_name = models.CharField(max_length=255, blank=True) + # JSON field to store plugin accounts in + accounts = JsonBField(null=True) + + def add_platform_account(self, platform, identifier): + try: + up = self.from_platform_identifier(platform, identifier) + if up != self: + raise ValidationError("This account has already been registered by another user") + + except self.DoesNotExist: + if platform not in self.accounts: + self.accounts[platform] = [] + self.accounts[platform].append(identifier) + + self.save() + + @classmethod + def from_platform_identifier(cls, platform, identifier): + return cls.objects.get(accounts__jcontains={platform: [identifier]}) + + class UserTrelloCourseBoardMap(models.Model): user = models.ForeignKey(User) course_code = models.CharField(max_length=1000, blank=False) @@ -199,6 +225,18 @@ def get_required_platforms(self): return platforms + @classmethod + def from_get(cls, request): + try: + unit_id = request.GET["unit"] + except: + raise SuspiciousOperation("Unit not specified") + try: + unit = cls.objects.get(id=unit_id) + return unit + except cls.DoesNotExist: + raise Http404 + class UnitOfferingMembership(models.Model): user = models.ForeignKey(User) @@ -217,6 +255,7 @@ class LearningRecord(models.Model): xapi = JsonField() unit = models.ForeignKey(UnitOffering) platform = models.CharField(max_length=5000, blank=False) + platform_group_id = models.CharField(max_length=100, blank=True) verb = models.CharField(max_length=5000, blank=False) user = models.ForeignKey(User) platformid = models.CharField(max_length=5000, blank=True) @@ -225,20 +264,30 @@ class LearningRecord(models.Model): parent_user = models.ForeignKey(User, null=True, related_name="parent_user") parent_user_external = models.CharField(max_length=5000, blank=True, null=True) message = models.TextField(blank=True) - datetimestamp = models.DateTimeField(auto_now_add=True, null=True) + datetimestamp = models.DateTimeField(default=timezone.now) senttolrs = models.CharField(max_length=5000, blank=True) class SocialRelationship(models.Model): unit = models.ForeignKey(UnitOffering) platform = models.CharField(max_length=5000, blank=False) - verb = models.CharField(max_length=5000, blank=False) + platform_group_id = models.CharField(max_length=255, blank=True) + type = models.CharField(max_length=100, blank=True) + verb = models.CharField(max_length=5000, blank=True) from_user = models.ForeignKey(User) to_user = models.ForeignKey(User, null=True, related_name="to_user") to_external_user = models.CharField(max_length=5000, blank=True, null=True) platformid = models.CharField(max_length=5000, blank=True) - message = models.TextField(blank=False) - datetimestamp = models.DateTimeField(blank=True) + message = models.TextField(blank=True) + datetimestamp = models.DateTimeField(null=True) + + @classmethod + def relationship_exists(cls, **kwargs): + try: + cls.objects.get(**kwargs) + return True + except cls.DoesNotExist: + return False class CachedContent(models.Model): diff --git a/clatoolkit_project/clatoolkit/views.py b/clatoolkit_project/clatoolkit/views.py index 1658c7d..a0cd001 100644 --- a/clatoolkit_project/clatoolkit/views.py +++ b/clatoolkit_project/clatoolkit/views.py @@ -1,5 +1,6 @@ from django.shortcuts import render, render_to_response from django.shortcuts import redirect +from django.core.urlresolvers import reverse from django.contrib.auth import authenticate, login from django.http import HttpResponseRedirect, HttpResponse, Http404 @@ -405,7 +406,18 @@ def update_offering(request, unit_id): else: form = CreateOfferingForm(instance=unit) - return render(request, "clatoolkit/createoffering.html", {'verb': 'Update', 'form': form}) + all_plugins = settings.DATAINTEGRATION_PLUGINS + excluded = ["Blog", "Diigo", "Forum", "GitHub", "Twitter", "YouTube", "facebook", "trello"] + + plugins = [] + for plugin in all_plugins: + if plugin not in excluded: + plugins.append({ + "a": reverse("{}_config".format(plugin)), + "label": "Configure {}".format(plugin) + }) + + return render(request, "clatoolkit/createoffering.html", {'verb': 'Update', 'form': form, 'unit': unit, "plugins":plugins}) else: raise PermissionDenied() diff --git a/clatoolkit_project/clatoolkit_project/settings.py b/clatoolkit_project/clatoolkit_project/settings.py index 1b52a36..bbb8169 100644 --- a/clatoolkit_project/clatoolkit_project/settings.py +++ b/clatoolkit_project/clatoolkit_project/settings.py @@ -12,6 +12,7 @@ # Build paths inside the project like this: os.path.join(BASE_DIR, ...) import os +import inspect BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -81,22 +82,6 @@ ROOT_URLCONF = 'clatoolkit_project.urls' -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [os.path.join(BASE_DIR, 'templates')], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], - }, - }, -] - WSGI_APPLICATION = 'clatoolkit_project.wsgi.application' @@ -148,31 +133,6 @@ USE_TZ = True - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/1.8/howto/static-files/ - -# web accessible folder -STATIC_ROOT = BASE_DIR #os.path.join(BASE_DIR, 'static') - -# URL prefix for static files. -STATIC_URL = '/static/' - -STATIC_PATH = os.path.join(BASE_DIR,'static') - -# Additional locations of static files -STATICFILES_DIRS = ( - # location of your application, should not be public web accessible - STATIC_PATH, -) - -# List of finder classes that know how to find static files in -# various locations. -STATICFILES_FINDERS = ( - 'django.contrib.staticfiles.finders.FileSystemFinder', - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', -) - AUTH_PROFILE_MODULE = "account.userprofile" GA_TRACKING_ID = '' @@ -200,3 +160,48 @@ DATAINTEGRATION_PLUGINS_INCLUDEDASHBOARD_PLATFORMS = get_includeindashboardwidgets_platforms() DATAINTEGRATION_PLUGINS = get_plugins() DATAINTEGRATION_PLUGINS_INCLUDEAUTHOMATIC = get_includeauthomaticplugins_platforms() + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [os.path.join(BASE_DIR, 'templates'), PLUGIN_PATH], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/1.8/howto/static-files/ + +# web accessible folder +STATIC_ROOT = BASE_DIR #os.path.join(BASE_DIR, 'static') + +# URL prefix for static files. +STATIC_URL = '/static/' + +STATIC_PATH = os.path.join(BASE_DIR,'static') + +# Additional locations of static files +STATICFILES_DIRS = [ + # location of your application, should not be public web accessible + STATIC_PATH, +] + +# Include the static dir for plugins so they can define their own assets +for plugin in pluginModules: + dir = os.path.join(PLUGIN_PATH, plugin, "static") + STATICFILES_DIRS.append((plugin, dir),) + +# List of finder classes that know how to find static files in +# various locations. +STATICFILES_FINDERS = ( + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', +) diff --git a/clatoolkit_project/common/CLRecipe.py b/clatoolkit_project/common/CLRecipe.py index 621cef0..359616b 100644 --- a/clatoolkit_project/common/CLRecipe.py +++ b/clatoolkit_project/common/CLRecipe.py @@ -12,6 +12,7 @@ class CLRecipe(object): PLATFORM_BLOG = 'Blog' PLATFORM_GITHUB = 'GitHub' PLATFORM_TRELLO = 'Trello' + PLATFORM_WORDPRESS = "WordPress" # Verbs VERB_CREATED = 'created' diff --git a/clatoolkit_project/dashboard/views.py b/clatoolkit_project/dashboard/views.py index 27d202f..22dde79 100644 --- a/clatoolkit_project/dashboard/views.py +++ b/clatoolkit_project/dashboard/views.py @@ -1,10 +1,12 @@ from django.shortcuts import render from django.shortcuts import render_to_response +from django.core.urlresolvers import reverse from django.template import RequestContext from django.http import HttpResponse from django.db import connection from utils import * from clatoolkit.models import OfflinePlatformAuthToken, UserProfile, OauthFlowTemp, UnitOffering, UnitOfferingMembership, DashboardReflection, LearningRecord, Classification, UserClassification, GroupMap, UserTrelloCourseBoardMap +from dataintegration.models import PlatformConfig from django.contrib.auth.decorators import login_required from django.contrib.auth import logout from functools import wraps @@ -177,6 +179,21 @@ def myunits(request): # Get a users memberships to unit offerings memberships = UnitOfferingMembership.objects.filter(user=request.user, unit__enabled=True).select_related('unit') + # Get Plugin details + for i in range(0, len(memberships)): + platform_configs = memberships[i].unit.platformconfig_set.all() + if len(platform_configs) > 0: + memberships[i].plugins = [] + for config in platform_configs: + platform = config.platform + groups = settings.DATAINTEGRATION_PLUGINS[platform].get_groups(memberships[i].unit) + memberships[i].plugins.append({ + "label": platform, + "platform": platform, + "refresh": reverse("{}_refresh".format(platform)), + "groups": groups + }) + role = request.user.userprofile.role show_dashboardnav = False diff --git a/clatoolkit_project/dataintegration/core/plugins/base.py b/clatoolkit_project/dataintegration/core/plugins/base.py index ec1957d..7d5dede 100644 --- a/clatoolkit_project/dataintegration/core/plugins/base.py +++ b/clatoolkit_project/dataintegration/core/plugins/base.py @@ -17,7 +17,7 @@ class DIBasePlugin(object): config_json_keys = [] - def perform_import(self, retrieval_param, course_code): + def perform_import(self, retrieval_param, unit): """Called to start the import from api for a plugin. diff --git a/clatoolkit_project/dataintegration/core/socialmediarecipebuilder.py b/clatoolkit_project/dataintegration/core/socialmediarecipebuilder.py index c453270..cfa3e00 100644 --- a/clatoolkit_project/dataintegration/core/socialmediarecipebuilder.py +++ b/clatoolkit_project/dataintegration/core/socialmediarecipebuilder.py @@ -200,18 +200,12 @@ def socialmedia_builder(verb, platform, account_name, account_homepage, object_t return statement -def insert_post(user, post_id, message, created_time, unit, platform, platform_url, tags=()): +def insert_post(user, post_id, message, created_time, unit, platform, platform_url, platform_group_id=None, tags=()): verb = 'created' if check_ifnotinlocallrs(unit, platform, post_id, user, verb): - stm = socialmedia_builder(verb=verb, platform=platform, account_name=get_smid(user, platform), - account_homepage=platform_url, object_type='Note', object_id=post_id, message=message, - timestamp=created_time, account_email=user.email, user_name=user.username, unit=unit, - tags=tags) - jsn = ast.literal_eval(stm.to_json()) - stm_json = pretty_print_json(jsn) - lrs = LearningRecord(xapi=stm_json, unit=unit, verb=verb, platform=platform, user=user, platformid=post_id, - message=message, datetimestamp=created_time) + lrs = LearningRecord(xapi=None, unit=unit, verb=verb, platform=platform, user=user, platformid=post_id, + message=message, datetimestamp=created_time, platform_group_id=platform_group_id) lrs.save() for tag in tags: if tag[0] == "@": @@ -243,19 +237,22 @@ def insert_blogpost(usr_dict, post_id,message,from_name,from_uid, created_time, socialrelationship.save() -def insert_like(user, post_id, message, unit, platform, created_time=None, parent_user=None, parent_user_external=None): +def insert_like(user, post_id, message, unit, platform, platform_group_id=None, created_time=None, parent_user=None, + parent_user_external=None): verb = "liked" if check_ifnotinlocallrs(unit, platform, post_id, user, verb): - lrs = LearningRecord(xapi=None, unit=unit, verb=verb, platform=platform, user=user, platformid=post_id, - message=message, platformparentid=post_id, parent_user=parent_user, - parent_user_external=parent_user_external, datetimestamp=created_time) + lrs = LearningRecord(xapi=None, unit=unit, verb=verb, platform=platform, platform_group_id=platform_group_id, + user=user, platformid=post_id, message=message, platformparentid=post_id, + parent_user=parent_user, parent_user_external=parent_user_external, + datetimestamp=created_time) lrs.save() sr = SocialRelationship(unit=unit, verb=verb, from_user=user, to_user=parent_user, - to_external_user=parent_user_external, platform=platform, message=message, - datetimestamp=created_time, platformid=post_id) + to_external_user=parent_user_external, platform=platform, + platform_group_id=platform_group_id, message=message, datetimestamp=created_time, + platformid=post_id) sr.save() @@ -276,37 +273,35 @@ def insert_blogcomment(usr_dict, post_id, comment_id, comment_message, comment_f def insert_comment(user, post_id, comment_id, comment_message, comment_created_time, unit, platform, platform_url, - parent_user=None, parent_user_external=None): - + platform_group_id=None, parent_user=None, parent_user_external=None): if check_ifnotinlocallrs(unit, platform, comment_id): lrs = LearningRecord(xapi=None, unit=unit, verb='commented', platform=platform, user=user, - platformid=comment_id, platformparentid=post_id, parent_user=parent_user, - parent_user_external=parent_user_external, message=comment_message, - datetimestamp=comment_created_time) + platformid=comment_id, platformparentid=post_id, platform_group_id=platform_group_id, + parent_user=parent_user, parent_user_external=parent_user_external, + message=comment_message, datetimestamp=comment_created_time) lrs.save() sr = SocialRelationship(verb="commented", from_user=user, to_user=parent_user, - to_external_user=parent_user_external, platform=platform, message=comment_message, + to_external_user=parent_user_external, platform=platform, + platform_group_id=platform_group_id, message=comment_message, datetimestamp=comment_created_time, unit=unit, platformid=comment_id) sr.save() -def insert_share(user, post_id, share_id, comment_message, comment_created_time, unit, platform, platform_url, tags=(), - parent_user=None, parent_external_user=None): +def insert_share(user, post_id, share_id, comment_message, comment_created_time, unit, platform, platform_url, + platform_group_id=None, tags=(), parent_user=None, parent_external_user=None): if check_ifnotinlocallrs(unit, platform, share_id): - # TODO - re-enable xAPI - # stm = socialmedia_builder(verb='shared', platform=platform, account_name=comment_from_uid, account_homepage=platform_url, object_type='Note', object_id=share_id, message=comment_message, parent_id=post_id, parent_object_type='Note', timestamp=comment_created_time, account_email=usr_dict['email'], user_name=comment_from_name, course_code=course_code, tags=tags ) - # jsn = ast.literal_eval(stm.to_json()) - # stm_json = pretty_print_json(jsn) - lrs = LearningRecord(xapi="{}", unit=unit, verb='shared', platform=platform, user=user, platformid=share_id, + lrs = LearningRecord(xapi=None, unit=unit, verb='shared', platform=platform, + platform_group_id=platform_group_id, user=user, platformid=share_id, platformparentid=post_id, parent_user=parent_user, parent_external_user=parent_external_user, message=comment_message, datetimestamp=comment_created_time) lrs.save() sr = SocialRelationship(verb="shared", from_user=user, to_user=parent_user, - to_external_user=parent_external_user, platform=platform, message=comment_message, + to_external_user=parent_external_user, platform=platform, + platform_group_id=platform_group_id, message=comment_message, datetimestamp=comment_created_time, course_code=unit.code, platformid=share_id) sr.save() @@ -426,6 +421,35 @@ def insert_closedopen_object(usr_dict, object_id, object_text, obj_updater_uid, lrs.save() +def insert_relationship(from_user, to_user, relationship_type, unit, platform, platform_group_id=None, + directional=True): + search_args = { + "from_user": from_user, + "to_user": to_user, + "type": relationship_type, + "unit": unit, + "platform": platform + } + + if platform_group_id: + search_args["platform_group_id"] = platform_group_id + + # Check if it exists + exists = True if SocialRelationship.relationship_exists(**search_args) else False + + # If the relationship is not direction check if the opposite exists + if not directional: + search_args["from_user"] = to_user + search_args["to_user"] = from_user + + exists = True if SocialRelationship.relationship_exists(**search_args) else False + + # Create the relationship if it doesn't already exist + if not exists: + sr = SocialRelationship(unit=unit, platform=platform, platform_group_id=platform_group_id, + type=relationship_type, from_user=from_user, to_user=to_user) + sr.save() + """def insert_comment(usr_dict, post_id, comment_id, comment_message, comment_from_uid, comment_from_name, comment_created_time, course_code, platform, platform_url, shared_username=None, shared_displayname=None): diff --git a/clatoolkit_project/dataintegration/migrations/0001_initial.py b/clatoolkit_project/dataintegration/migrations/0001_initial.py index 5f89d96..715e892 100644 --- a/clatoolkit_project/dataintegration/migrations/0001_initial.py +++ b/clatoolkit_project/dataintegration/migrations/0001_initial.py @@ -2,11 +2,13 @@ from __future__ import unicode_literals from django.db import models, migrations +import django_pgjson.fields class Migration(migrations.Migration): dependencies = [ + ('clatoolkit', '0001_initial'), ] operations = [ @@ -25,6 +27,15 @@ class Migration(migrations.Migration): ('updatedAt', models.CharField(max_length=30)), ], ), + migrations.CreateModel( + name='PlatformConfig', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('platform', models.CharField(max_length=50)), + ('config', django_pgjson.fields.JsonField()), + ('unit', models.ForeignKey(to='clatoolkit.UnitOffering')), + ], + ), migrations.CreateModel( name='Video', fields=[ diff --git a/clatoolkit_project/dataintegration/models.py b/clatoolkit_project/dataintegration/models.py index 1aae22e..baeeb80 100644 --- a/clatoolkit_project/dataintegration/models.py +++ b/clatoolkit_project/dataintegration/models.py @@ -1,24 +1,34 @@ from django.db import models +from django_pgjson.fields import JsonField +from clatoolkit.models import UnitOffering + # Create your models here. class Comment(models.Model): - commId = models.CharField(max_length=100) - authorDispName = models.CharField(max_length=1000) - text = models.CharField(max_length=10000) - videoId = models.CharField(max_length=20) - videoUrl = models.CharField(max_length=1000) - parentId = models.CharField(max_length=50) - parentUsername = models.CharField(max_length=1000, blank=True) - isReply = models.BooleanField() - updatedAt = models.CharField(max_length=30) + commId = models.CharField(max_length=100) + authorDispName = models.CharField(max_length=1000) + text = models.CharField(max_length=10000) + videoId = models.CharField(max_length=20) + videoUrl = models.CharField(max_length=1000) + parentId = models.CharField(max_length=50) + parentUsername = models.CharField(max_length=1000, blank=True) + isReply = models.BooleanField() + updatedAt = models.CharField(max_length=30) + class Video(models.Model): - videoId = models.CharField(max_length=20) - videoUrl = models.CharField(max_length=1000) - videoTitle = models.CharField(max_length=300) - channelId = models.CharField(max_length=30) - channelUrl = models.CharField(max_length=1000) - #videoList = models.ManyToManyField('self') - #commentList = models.ManyToManyField(Comment) + videoId = models.CharField(max_length=20) + videoUrl = models.CharField(max_length=1000) + videoTitle = models.CharField(max_length=300) + channelId = models.CharField(max_length=30) + channelUrl = models.CharField(max_length=1000) + # videoList = models.ManyToManyField('self') + # commentList = models.ManyToManyField(Comment) + + +class PlatformConfig(models.Model): + unit = models.ForeignKey(UnitOffering) + platform = models.CharField(max_length=50) + config = JsonField() diff --git a/clatoolkit_project/dataintegration/plugins/wordpress/__init__.py b/clatoolkit_project/dataintegration/plugins/wordpress/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/clatoolkit_project/dataintegration/plugins/wordpress/cladi_plugin.py b/clatoolkit_project/dataintegration/plugins/wordpress/cladi_plugin.py new file mode 100644 index 0000000..d5c1444 --- /dev/null +++ b/clatoolkit_project/dataintegration/plugins/wordpress/cladi_plugin.py @@ -0,0 +1,317 @@ +from django.http import HttpResponse, HttpResponseRedirect, Http404, HttpResponseBadRequest +from django.shortcuts import render +from django.conf.urls import url +from django.core.exceptions import PermissionDenied +from django.core.urlresolvers import reverse +from django.contrib.auth.decorators import login_required +from common.CLRecipe import CLRecipe +from dataintegration.core.plugins.base import DIBasePlugin, DIPluginDashboardMixin +from requests_oauthlib import OAuth1Session +from dataintegration.models import PlatformConfig +from dataintegration.core.socialmediarecipebuilder import * +from dataintegration.core.recipepermissions import * +from dataintegration.core.plugins import registry +from forms import ConnectForm + + +class WordPressPlugin(DIBasePlugin, DIPluginDashboardMixin): + platform = CLRecipe.PLATFORM_WORDPRESS + + xapi_verbs = ['created', 'commented'] + xapi_objects = ['Note'] + + user_api_association_name = "WP username" + + @classmethod + def config_view(cls, request): + unit = UnitOffering.from_get(request) + if UnitOfferingMembership.is_admin(request.user, unit): + instances = cls.get_intances(unit) + + return render(request, "wordpress/templates/config.html", {"unit": unit, "instances": instances}) + else: + raise PermissionDenied + + @classmethod + def delete_instance_view(cls, request): + unit = UnitOffering.from_get(request) + if UnitOfferingMembership.is_admin(request.user, unit): + try: + instance = request.GET["instance"] + except: + return HttpResponseBadRequest("Instance not specified") + cls.delete_instance(unit, instance) + return HttpResponseRedirect(reverse("{}_config".format(cls.platform)) + "?unit={}".format(unit.id)) + else: + raise PermissionDenied + + @classmethod + def connect_view(cls, request): + if request.method == "POST": + form = ConnectForm(request.POST) + + if form.is_valid(): + # Get the details the user entered into the form + unit_id = form.cleaned_data["unit_id"] + wp_root = form.cleaned_data["wp_root"] + client_key = form.cleaned_data["client_key"] + client_secret = form.cleaned_data["client_secret"] + + unit = UnitOffering.objects.get(id=unit_id) + + if UnitOfferingMembership.is_admin(request.user, unit): + # Save the WP instance details + cls.add_instance(unit, wp_root, client_key, client_secret) + + # Setup URLs for use in the handshake process + request_token_url = "{}/oauth1/request".format(wp_root) + base_authorization_url = "{}/oauth1/authorize".format(wp_root) + callback_path = reverse("{}_authorize".format(cls.platform)) + callback_uri = "{}://{}{}".format(request.scheme, request.get_host(), callback_path) + + # Start the oauth flow and get temporary credentials + oauth = OAuth1Session(client_key, client_secret, callback_uri) + fetch_response = oauth.fetch_request_token(request_token_url) + temp_token = fetch_response.get('oauth_token') + temp_secret = fetch_response.get('oauth_token_secret') + + # Save credentials to the session as they will need to be reused in the authorization step + request.session["wp_temp_token"] = temp_token + request.session["wp_temp_secret"] = temp_secret + request.session["wp_temp_unit"] = request.GET["unit"] + request.session["wp_temp_root"] = wp_root + + # Redirect the user to authorize the toolkit + authorization_url = oauth.authorization_url(base_authorization_url, oauth_callback=callback_uri) + return HttpResponseRedirect(authorization_url) + + else: + unit = UnitOffering.from_get(request) + form = ConnectForm() + + return render(request, "wordpress/templates/connect.html", {"form": form, "unit": unit}) + + @classmethod + def authorize_view(cls, request): + return cls().authorize(request) + + @classmethod + def refresh_view(cls, request): + unit = UnitOffering.objects.get(id=request.GET["unit"]) + if UnitOfferingMembership.is_admin(request.user, unit): + cls().perform_import(request, unit) + return HttpResponse("Done") + else: + raise PermissionDenied + + @classmethod + def get_url_patterns(cls): + """Returns the URL patterns used by the plugin""" + return [ + url(r'^config/$', login_required(cls.config_view), name="{}_config".format(CLRecipe.PLATFORM_WORDPRESS)), + url(r'^connect/$', login_required(cls.connect_view), name="{}_connect".format(CLRecipe.PLATFORM_WORDPRESS)), + url(r'^delete/$', login_required(cls.delete_instance_view), name="{}_delete_instance".format(CLRecipe.PLATFORM_WORDPRESS)), + url(r'^authorize/$', login_required(cls.authorize_view), name="{}_authorize".format(CLRecipe.PLATFORM_WORDPRESS)), + url(r'^refresh/$', login_required(cls.refresh_view), name="{}_refresh".format(CLRecipe.PLATFORM_WORDPRESS)), + ] + + def __init__(self): + pass + + @classmethod + def authorize(cls, request): + unit_id = request.session["wp_temp_unit"] + wp_root = request.session["wp_temp_root"] + + try: + unit = UnitOffering.objects.get(id=unit_id) + except UnitOffering.DoesNotExist: + raise Http404 + + if UnitOfferingMembership.is_admin(request.user, unit): + access_token_url = "{}/oauth1/access".format(wp_root) + + verifier = request.GET['oauth_verifier'] + + client_key, client_secret = cls.get_client_key(unit, wp_root) + + oauth = OAuth1Session(client_key, client_secret=client_secret, + resource_owner_key=request.session["wp_temp_token"], + resource_owner_secret=request.session["wp_temp_secret"], verifier=verifier) + oauth_tokens = oauth.fetch_access_token(access_token_url) + + cls.save_access_token(unit, wp_root, oauth_tokens.get('oauth_token'), oauth_tokens.get('oauth_token_secret')) + + return HttpResponseRedirect(reverse("{}_config".format(cls.platform)) + "?unit={}".format(unit.id)) + + else: + raise PermissionDenied + + @classmethod + def add_instance(cls, unit, wp_root, access_key, access_secret): + """Add a connection to the platform config for a unit, overwrites existing entries for a non-unique wp_root""" + try: + pc = PlatformConfig.objects.get(unit=unit, platform=cls.platform) + pc.config[wp_root] = { + "client_key": access_key, + "client_secret": access_secret + } + # If no existing config exists + except PlatformConfig.DoesNotExist: + config = { + wp_root: { + "client_key": access_key, + "client_secret": access_secret + } + } + pc = PlatformConfig(unit=unit, platform=cls.platform, config=config) + pc.save() + + @classmethod + def delete_instance(cls, unit, wp_root): + try: + pc = PlatformConfig.objects.get(unit=unit, platform=cls.platform) + pc.config.pop(wp_root) + pc.save() + # If no existing config exists + except PlatformConfig.DoesNotExist: + pass + + @classmethod + def get_platform_config(cls, unit): + return PlatformConfig.objects.get(unit=unit, platform=cls.platform).config + + @classmethod + def get_instance_config(cls, unit, wp_root): + return PlatformConfig.objects.get(unit=unit, platform=cls.platform).config[wp_root] + + @classmethod + def get_intances(cls, unit): + config = cls.get_platform_config(unit) + sites = [s for s in config] + return sites + + @classmethod + def get_groups(cls, unit): + instances = cls.get_intances(unit) + result = [] + for instance in instances: + result.append({ + "label": instance, + "a": instance + }) + return result + + @classmethod + def get_client_key(cls, unit, wp_root): + config = cls.get_instance_config(unit, wp_root) + + return config["client_key"], config["client_secret"] + + @classmethod + def save_access_token(cls, unit, wp_root, access_token_key, access_token_secret): + """Save access token and secret to an existing entry in the PlatformConfig""" + pc = PlatformConfig.objects.get(unit=unit, platform=cls.platform) + + pc.config[wp_root]["access_token_key"] = access_token_key + pc.config[wp_root]["access_token_secret"] = access_token_secret + + pc.save() + + @classmethod + def get_access_token(cls, unit, wp_root): + config = cls.get_instance_config(unit, wp_root) + + return config["access_token_key"], config["access_token_secret"] + + @classmethod + def perform_import(cls, retrieval_param, unit): + config = cls.get_platform_config(unit) + + # For each WordPress instance connected + for instance in config: + + oauth = OAuth1Session(client_key=config[instance]["client_key"], + client_secret=config[instance]["client_secret"], + resource_owner_key=config[instance]["access_token_key"], + resource_owner_secret=config[instance]["access_token_secret"]) + + cls.get_posts(unit, oauth, instance) + cls.get_friendships(unit, oauth, instance) + + @classmethod + def get_posts(cls, unit, oauth, instance): + next_page = "{}/wp-json/clatoolkit-wp/v1/posts".format(instance) + + while next_page: + r = oauth.get(next_page) + result = r.json() + + for blog in result["posts"]: + cls.add_blog_posts(blog, unit, instance) + + if "next_page" in result: + next_page = result["next_page"] + else: + next_page = False + + @classmethod + def add_blog_posts(cls, blog, unit, instance): + + for post in blog["posts"]: + + try: + user = UserProfile.from_platform_identifier(cls.platform, post["author"]["email"]).user + + insert_post(user=user, post_id=post["guid"], message=post["post_content"], + created_time=post["post_date_gmt"], unit=unit, platform=cls.platform, platform_url="", + platform_group_id=instance) + + if "comments" in post: + for comment in post["comments"]: + try: + commenter = UserProfile.from_platform_identifier(cls.platform, + comment["comment_author_email"]).user + + insert_comment(user=commenter, post_id=post["guid"], comment_id=comment["comment_guid"], + comment_message=comment["comment_content"], + comment_created_time=comment["comment_date_gmt"], unit=unit, + platform=cls.platform, platform_group_id=instance, platform_url="", parent_user=user) + + except UserProfile.DoesNotExist: + # Don't store the comment if the author doesn't exist + pass + + except UserProfile.DoesNotExist: + # Do nothing if the post author doesn't exist + pass + + @classmethod + def get_friendships(cls, unit, oauth, instance): + friendship_endpoint = "{}/wp-json/clatoolkit-wp/v1/friendships".format(instance) + relationship_type = "friendship" + + r = oauth.get(friendship_endpoint) + + j = r.json() + + for user_id in j["friendships"]: + user_email = j["entities"][user_id]["email"] + try: + user = UserProfile.from_platform_identifier(platform=cls.platform, identifier=user_email).user + for friend_id in j["friendships"][user_id]: + friend_user_email = j["entities"][friend_id]["email"] + + try: + friend = UserProfile.from_platform_identifier(cls.platform, friend_user_email).user + + insert_relationship(from_user=user, to_user=friend, relationship_type=relationship_type, + unit=unit, platform=cls.platform, platform_group_id=instance, + directional=False) + + except UserProfile.DoesNotExist: + pass + except UserProfile.DoesNotExist: + pass + +registry.register(WordPressPlugin) diff --git a/clatoolkit_project/dataintegration/plugins/wordpress/forms.py b/clatoolkit_project/dataintegration/plugins/wordpress/forms.py new file mode 100644 index 0000000..bf538b4 --- /dev/null +++ b/clatoolkit_project/dataintegration/plugins/wordpress/forms.py @@ -0,0 +1,8 @@ +from django import forms + + +class ConnectForm(forms.Form): + wp_root = forms.URLField(label="WordPress Root URL", widget=forms.TextInput(attrs={'class': 'form-control'})) + client_key = forms.CharField(widget=forms.TextInput(attrs={'class': 'form-control'})) + client_secret = forms.CharField(widget=forms.TextInput(attrs={'class': 'form-control'})) + unit_id = forms.CharField(widget=forms.HiddenInput()) diff --git a/clatoolkit_project/dataintegration/plugins/wordpress/static/css/default.css b/clatoolkit_project/dataintegration/plugins/wordpress/static/css/default.css new file mode 100644 index 0000000..668f378 --- /dev/null +++ b/clatoolkit_project/dataintegration/plugins/wordpress/static/css/default.css @@ -0,0 +1,12 @@ +body { + margin-top: 20px; +} + +h4 { + margin-bottom: 0; +} + +p.btn-line-height { + line-height: 34px; + margin: 0; +} \ No newline at end of file diff --git a/clatoolkit_project/dataintegration/plugins/wordpress/templates/config.html b/clatoolkit_project/dataintegration/plugins/wordpress/templates/config.html new file mode 100644 index 0000000..1e95edc --- /dev/null +++ b/clatoolkit_project/dataintegration/plugins/wordpress/templates/config.html @@ -0,0 +1,43 @@ + + +
+ +{{ instance }}
+{{ group.label }}
+ {% endif %} + {% endfor %} + [ + Activity | + Content Analysis | + Social Network Analysis + ] + Refresh Imported Data + {% endfor %} + {% endif %} {% if unit.enable_coi_classifier %}