diff --git a/api/migrations/0001_initial.py b/api/migrations/0001_initial.py deleted file mode 100644 index 561c405..0000000 --- a/api/migrations/0001_initial.py +++ /dev/null @@ -1,106 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.10.5 on 2017-10-02 22:48 -from __future__ import unicode_literals - -from django.conf import settings -import django.contrib.auth.models -import django.contrib.auth.validators -import django.contrib.postgres.fields.jsonb -from django.db import migrations, models -import django.db.models.deletion -import django.utils.timezone - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='User', - fields=[ - ('password', models.CharField(max_length=128, verbose_name='password')), - ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), - ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), - ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), - ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), - ('last_name', models.CharField(blank=True, max_length=30, verbose_name='last name')), - ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), - ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), - ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), - ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), - ('id', models.AutoField(primary_key=True, serialize=False)), - ('gravatar', models.URLField(blank=True)), - ], - options={ - 'verbose_name': 'user', - 'verbose_name_plural': 'users', - 'abstract': False, - }, - managers=[ - ('objects', django.contrib.auth.models.UserManager()), - ], - ), - migrations.CreateModel( - name='Collection', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=200)), - ('description', models.TextField(blank=True, null=True)), - ('tags', models.TextField(blank=True, null=True)), - ('created_by_org', models.CharField(blank=True, max_length=200, null=True)), - ('date_created', models.DateTimeField(auto_now_add=True)), - ('date_updated', models.DateTimeField(auto_now=True)), - ('settings', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default={})), - ('submission_settings', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default={})), - ('collection_type', models.CharField(max_length=50)), - ('location', models.CharField(max_length=200)), - ('address', models.CharField(max_length=200)), - ('start_datetime', models.DateTimeField(blank=True, null=True)), - ('end_datetime', models.DateTimeField(blank=True, null=True)), - ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ], - options={ - 'permissions': (('approve_collection_items', 'Approve collection items'),), - }, - ), - migrations.CreateModel( - name='Group', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=200)), - ('description', models.TextField(blank=True, null=True)), - ('date_created', models.DateTimeField(auto_now_add=True)), - ('date_updated', models.DateTimeField(auto_now=True)), - ('collection', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='groups', to='api.Collection')), - ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ], - ), - migrations.CreateModel( - name='Item', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=200)), - ('description', models.TextField(blank=True, null=True)), - ('type', models.CharField(choices=[('none', 'none'), ('project', 'project'), ('preprint', 'preprint'), ('registration', 'registration'), ('presentation', 'presentation'), ('website', 'website'), ('event', 'event'), ('meeting', 'meeting')], max_length=200)), - ('status', models.CharField(choices=[('none', 'none'), ('approved', 'approved'), ('pending', 'pending'), ('rejected', 'rejected')], max_length=200, null=True)), - ('source_id', models.CharField(blank=True, max_length=200, null=True)), - ('url', models.URLField(blank=True, null=True)), - ('metadata', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True)), - ('date_created', models.DateTimeField(auto_now_add=True)), - ('date_submitted', models.DateTimeField(blank=True, default=None, null=True)), - ('date_accepted', models.DateTimeField(blank=True, default=None, null=True)), - ('location', models.CharField(blank=True, default=None, max_length=200, null=True)), - ('start_time', models.DateTimeField(blank=True, default=None, null=True)), - ('end_time', models.DateTimeField(blank=True, default=None, null=True)), - ('category', models.CharField(blank=True, choices=[('none', 'none'), ('talk', 'talk'), ('poster', 'poster')], max_length=200, null=True)), - ('file_link', models.CharField(blank=True, max_length=1000, null=True)), - ('collection', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='api.Collection')), - ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ('group', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='items', to='api.Group')), - ], - ), - ] diff --git a/api/migrations/0002_auto_20171002_2248.py b/api/migrations/0002_auto_20171002_2248.py deleted file mode 100644 index c9cdd51..0000000 --- a/api/migrations/0002_auto_20171002_2248.py +++ /dev/null @@ -1,35 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.10.5 on 2017-10-02 22:48 -from __future__ import unicode_literals - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('auth', '0008_alter_user_username_max_length'), - ('workflow', '0001_initial'), - ('api', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='collection', - name='workflow', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='collections', to='workflow.Workflow'), - ), - migrations.AddField( - model_name='user', - name='groups', - field=models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups'), - ), - migrations.AddField( - model_name='user', - name='user_permissions', - field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions'), - ), - ] diff --git a/api/migrations/0003_auto_20171003_0010.py b/api/migrations/0003_auto_20171003_0010.py deleted file mode 100644 index 7dcb96f..0000000 --- a/api/migrations/0003_auto_20171003_0010.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.10.5 on 2017-10-03 00:10 -from __future__ import unicode_literals - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0002_auto_20171002_2248'), - ] - - operations = [ - migrations.AlterField( - model_name='collection', - name='workflow', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='collections', to='workflow.Workflow'), - ), - ] diff --git a/api/migrations/0004_auto_20171003_0012.py b/api/migrations/0004_auto_20171003_0012.py deleted file mode 100644 index 51c86e7..0000000 --- a/api/migrations/0004_auto_20171003_0012.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.10.5 on 2017-10-03 00:12 -from __future__ import unicode_literals - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('api', '0003_auto_20171003_0010'), - ] - - operations = [ - migrations.AlterField( - model_name='collection', - name='workflow', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='collections', to='workflow.Workflow'), - ), - ] diff --git a/api/models.py b/api/models.py index 10bb0a0..e69de29 100644 --- a/api/models.py +++ b/api/models.py @@ -1,105 +0,0 @@ -from __future__ import unicode_literals -from django.db import models -from django.contrib.auth.models import AbstractUser -from django.contrib.postgres.fields import JSONField - - -class User(AbstractUser): - id = models.AutoField(primary_key=True) - gravatar = models.URLField(blank=True) - - @property - def full_name(self): - return self.first_name + " " + self.last_name - -class Collection(models.Model): - title = models.CharField(max_length=200) - description = models.TextField(null=True, blank=True) - tags = models.TextField(null=True, blank=True) - created_by = models.ForeignKey(User) - created_by_org = models.CharField(null=True, blank=True, max_length=200) - date_created = models.DateTimeField(auto_now_add=True) - date_updated = models.DateTimeField(auto_now=True) - settings = JSONField(default={}, blank=True) - submission_settings = JSONField(default={}, blank=True) - collection_type = models.CharField(max_length=50) - location = models.CharField(max_length=200) - address = models.CharField(max_length=200) - start_datetime = models.DateTimeField(null=True, blank=True) - end_datetime = models.DateTimeField(null=True, blank=True) - - workflow = models.ForeignKey( - 'workflow.Workflow', - null=True, - blank=True, - related_name="collections", - on_delete=models.SET_NULL - ) - - def save(self, *args, **kwargs): - if not self.pk: # if this is the first save, set default settings based on collection_type - if self.collection_type == 'meeting': - self.settings = resources.meeting_json - elif self.collection_type == 'dataset': - self.settings = resources.dataset_json - super().save(*args, **kwargs) - - def __str__(self): - return self.title - - class Meta: - permissions = ( - ('approve_collection_items', 'Approve collection items'), - ) - - -class Group(models.Model): - title = models.CharField(max_length=200) - description = models.TextField(null=True, blank=True) - collection = models.ForeignKey(to='Collection', related_name='groups') - created_by = models.ForeignKey(User) - date_created = models.DateTimeField(auto_now_add=True) - date_updated = models.DateTimeField(auto_now=True) - - -class Item(models.Model): - TYPES = ( - ('none', 'none'), - ('project', 'project'), - ('preprint', 'preprint'), - ('registration', 'registration'), - ('presentation', 'presentation'), - ('website', 'website'), - ('event', 'event'), - ('meeting', 'meeting') - ) - STATUS = ( - ('none', 'none'), - ('approved', 'approved'), - ('pending', 'pending'), - ('rejected', 'rejected') - ) - CATEGORIES = ( - ('none', 'none'), - ('talk', 'talk'), - ('poster', 'poster') - ) - title = models.CharField(max_length=200) - description = models.TextField(null=True, blank=True) - type = models.CharField(choices=TYPES, max_length=200) - status = models.CharField(choices=STATUS, null=True, max_length=200) - source_id = models.CharField(null=True, blank=True, max_length=200) - url = models.URLField(null=True, blank=True) - collection = models.ForeignKey(to='Collection', related_name='items') - group = models.ForeignKey(to='Group', null=True, blank=True, default=None, related_name='items') - created_by = models.ForeignKey(User) - metadata = JSONField(null=True, blank=True) - date_created = models.DateTimeField(auto_now_add=True) - date_submitted = models.DateTimeField(null=True, blank=True, default=None) - date_accepted = models.DateTimeField(null=True, blank=True, default=None) - file_link = models.TextField(null=True, blank=True) - location = models.CharField(null=True, blank=True, default=None, max_length=200) - start_time = models.DateTimeField(null=True, blank=True, default=None) - end_time = models.DateTimeField(null=True, blank=True, default=None) - category = models.CharField(choices=CATEGORIES, null=True, blank=True, max_length=200) - file_link = models.CharField(null=True, blank=True, max_length=1000) diff --git a/api/serializers.py b/api/serializers.py index 92d19ab..e69de29 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -1,302 +0,0 @@ -from django.utils import timezone -from rest_framework import exceptions -from rest_framework_json_api import serializers -from api.models import Collection, Group, Item, User -from api.base.serializers import RelationshipField -from guardian.shortcuts import assign_perm -from allauth.socialaccount.models import SocialAccount, SocialToken -from django.core.exceptions import ObjectDoesNotExist -from api import search_indexes -from drf_haystack.serializers import HaystackSerializer - -from workflow.models import Workflow - -from rest_framework_json_api.relations import ResourceRelatedField, SerializerMethodResourceRelatedField - -class UserSearchSerializer(HaystackSerializer): - - class Meta: - index_classes = [search_indexes.UserIndex] - fields = [ - 'text', - 'first_name', - 'last_name', - 'email', - 'full_name' - ] - - -class ItemSearchSerializer(HaystackSerializer): - - class Meta: - index_classes = [search_indexes.ItemIndex] - fields = [ - 'text', - 'title', - 'description', - 'created_by', - 'collection' - ] - - -class CollectionSearchSerializer(HaystackSerializer): - - class Meta: - index_classes = [search_indexes.CollectionIndex] - fields = [ - 'text', - 'title', - 'description', - 'created_by' - ] - - -class UserSerializer(serializers.ModelSerializer): - token = serializers.SerializerMethodField() - - class Meta: - model = User - fields = [ - 'id', - 'username', - 'first_name', - 'last_name', - 'email', - 'date_joined', - 'last_login', - 'is_active', - 'gravatar', - 'token' - ] - - class JSONAPIMeta: - resource_name = 'users' - - def get_token(self, obj): - if not obj.id: - return None - try: - account = SocialAccount.objects.get(user=obj) - token = SocialToken.objects.get(account=account).token - except ObjectDoesNotExist: - return None - return token - - -class ItemSerializer(serializers.Serializer): - id = serializers.CharField(read_only=True) - title = serializers.CharField() - type = serializers.ChoiceField( - choices=['none', 'project', 'preprint', 'registration', 'presentation', 'website', 'event', 'meeting']) - description = serializers.CharField(required=False, allow_null=True) - status = serializers.ChoiceField(choices=['none', 'approved', 'pending', 'rejected']) - source_id = serializers.CharField(required=False, allow_null=True) - url = serializers.URLField(required=False, allow_null=True) - created_by = UserSerializer(read_only=True) - metadata = serializers.JSONField(required=False, allow_null=True) - date_created = serializers.DateTimeField(read_only=True) - date_submitted = serializers.DateTimeField(read_only=True, allow_null=True) - date_accepted = serializers.DateTimeField(read_only=True, allow_null=True) - location = serializers.CharField(allow_null=True, required=False) - start_time = serializers.DateTimeField(allow_null=True, required=False) - end_time = serializers.DateTimeField(allow_null=True, required=False) - category = serializers.ChoiceField(choices=['none', 'talk', 'poster'], allow_null=True, required=False) - file_link = serializers.CharField(allow_null=True, allow_blank=True, required=False) - - class Meta: - model = Item - - class JSONAPIMeta: - resource_name = 'items' - - def create(self, validated_data): - user = self.context['request'].user - collection_id = self.context.get('collection_id', None) or self.context['request'].parser_context['kwargs'].get( - 'pk', None) - collection = Collection.objects.get(id=collection_id) - - allow_all = None - if collection.settings: - allow_all = collection.settings.get('allow_all', None) - collection_type = collection.collection_type - if collection_type and validated_data['type'] != collection_type: - raise ValueError('Collection only accepts items of type ' + collection_type) - - status = 'pending' - if user.has_perm('api.approve_collection_items', collection) or allow_all: - status = 'approved' - validated_data['date_accepted'] = timezone.now() - - group_id = self.context.get('group_id', None) or self.context['request'].parser_context['kwargs'].get( - 'group_id', None) - if group_id: - validated_data['group'] = Group.objects.get(id=group_id) - - validated_data['status'] = status - validated_data['date_submitted'] = timezone.now() - item = Item.objects.create( - created_by=user, - collection=collection, - **validated_data - ) - return item - - def update(self, item, validated_data): - user = self.context['request'].user - status = validated_data.get('status', item.status) - collection_id = self.context.get('collection_id', None) or self.context['request'].parser_context['kwargs'].get( - 'pk', None) - if collection_id: - collection = Collection.objects.get(id=collection_id) - else: - collection = item.collection - - if status != item.status and user.has_perm('api.approve_collection_items', collection): - raise exceptions.PermissionDenied(detail='Cannot change submission status.') - elif user.id != item.created_by_id and validated_data.keys() != ['status']: - raise exceptions.PermissionDenied(detail='Cannot update another user\'s submission.') - - group_id = self.context.get('group_id', None) or self.context['request'].parser_context['kwargs'].get( - 'group_id', None) - if group_id: - group = Group.objects.get(id=group_id) - else: - group = None - - item_type = validated_data.get('type', item.type) - if collection.settings: - collection_type = collection.settings.get('type', None) - if collection_type and item_type != collection_type: - raise ValueError('Collection only accepts items of type ' + collection_type) - - item.group = group - item.source_id = validated_data.get('source_id', item.source_id) - item.title = validated_data.get('title', item.title) - item.description = validated_data.get('description', item.description) - item.type = item_type - item.status = status - item.url = validated_data.get('url', item.url) - item.metadata = validated_data.get('metadata', item.metadata) - item.location = validated_data.get('location', item.location) - item.start_time = validated_data.get('start_time', item.start_time) - item.end_time = validated_data.get('end_time', item.end_time) - item.category = validated_data.get('category', item.category) - item.file_link = validated_data.get('file_link', item.file_link) - item.save() - return item - - -class GroupSerializer(serializers.Serializer): - id = serializers.CharField(read_only=True) - title = serializers.CharField(required=True) - description = serializers.CharField(allow_blank=True, required=False) - created_by = UserSerializer(read_only=True) - date_created = serializers.DateTimeField(read_only=True) - date_updated = serializers.DateTimeField(read_only=True) - items = RelationshipField( - related_view='group-item-list', - related_view_kwargs={'pk': '', 'group_id': ''} - ) - items = serializers.HyperlinkedRelatedField( - many=True, - read_only=True, - view_name='track-detail' - ) - - class Meta: - model = Group - - class JSONAPIMeta: - resource_name = 'groups' - - def create(self, validated_data): - user = self.context['request'].user - collection_id = self.context.get('collection_id', None) or self.context['request'].parser_context['kwargs'].get( - 'pk', None) - collection = Collection.objects.get(id=collection_id) - return Group.objects.create( - created_by=user, - collection=collection, - **validated_data - ) - - def update(self, group, validated_data): - group.title = validated_data.get('title', None) - description = validated_data.get('description', None) - if description: - group.description = description - group.save() - return group - - -class CollectionSerializer(serializers.Serializer): - - included_serializers = { - 'workflow': 'workflow.serializers.Workflow' - } - - id = serializers.CharField(read_only=True) - title = serializers.CharField(required=True) - description = serializers.CharField(required=False, allow_blank=True) - tags = serializers.CharField(required=False, allow_blank=True) - address = serializers.CharField(required=False, allow_blank=True) - location = serializers.CharField(required=False, allow_blank=True) - settings = serializers.JSONField(required=False) - submission_settings = serializers.JSONField(required=False) - created_by_org = serializers.CharField(allow_blank=True, required=False) - created_by = RelationshipField( - related_view='user-detail', - related_view_kwargs={'user_id': ''}, - ) - collection_type = serializers.CharField() - date_created = serializers.DateTimeField(read_only=True) - date_updated = serializers.DateTimeField(read_only=True) - groups = RelationshipField( - related_view='collection-group-list', - related_view_kwargs={'pk': ''} - ) - items = RelationshipField( - related_view='collection-item-list', - related_view_kwargs={'pk': ''} - ) - - workflow = ResourceRelatedField( - queryset=Workflow.objects.all(), - many=False, - required=True - ) - - class Meta: - model = Collection - fields = [ - 'id', - 'title', - 'description', - 'tags', - 'created_by', - 'workflow', - 'location', - 'address' - ] - - class JSONAPIMeta: - resource_name = 'collections' - included_resources = [ - 'workflow', - ] - - def create(self, validated_data): - user = self.context['request'].user - collection = Collection.objects.create(created_by=user, **validated_data) - assign_perm('api.approve_collection_items', user, collection) - return collection - - def update(self, collection, validated_data): - collection.title = validated_data.get('title', collection.title) - collection.description = validated_data.get('description', collection.description) - collection.tags = validated_data.get('tags', collection.tags) - collection.settings = validated_data.get('settings', collection.settings) - collection.submission_settings = validated_data.get('submission_settings', collection.submission_settings) - collection.created_by_org = validated_data.get('created_by_org', collection.created_by_org) - collection.save() - return collection \ No newline at end of file diff --git a/api/templates/rest_framework/api.html b/api/templates/rest_framework/api.html new file mode 100644 index 0000000..9d79881 --- /dev/null +++ b/api/templates/rest_framework/api.html @@ -0,0 +1,5 @@ +{% extends "rest_framework/base.html" %} +{% block bootstrap_theme %} + + +{% endblock %} diff --git a/api/urls.py b/api/urls.py index ba76646..0e36b0f 100644 --- a/api/urls.py +++ b/api/urls.py @@ -1,37 +1,27 @@ from django.conf.urls import url, include -import api.views +from collection.views import ( + CollectionSearchView, + ItemSearchView +) from rest_framework.routers import DefaultRouter +from collection.routers import collection_router from workflow.routers import workflow_router, case_router +from user.routers import user_router -router = DefaultRouter() -router.register("collections/search", api.views.CollectionSearchView, base_name="collection-search") -router.register("items/search", api.views.ItemSearchView, base_name="item-search") -router.register("users/search", api.views.UserSearchView, base_name="user-search") +from api.views import api_root + +search_router = DefaultRouter() +search_router.register("collections/search", CollectionSearchView, base_name="collection-search") +search_router.register("items/search", ItemSearchView, base_name="item-search") urlpatterns = [ - url(r'^$', api.views.api_root), - url(r'', include(router.urls)), + url(r'^$', api_root), + url(r'', include(collection_router.urls)), url(r'', include(workflow_router.urls)), url(r'', include(case_router.urls)), - url(r'^collections/$', api.views.CollectionList.as_view(), name='collection-list'), - url(r'^collections/(?P\w+)/$', api.views.CollectionDetail.as_view(), name='collection-detail'), - url(r'^collections/(?P\w+)/groups/$', api.views.CollectionGroupList.as_view(), name='collection-group-list'), - url(r'^collections/(?P\w+)/groups/(?P\w+)/$', api.views.GroupDetail.as_view(), name='collection-group-detail'), - url(r'^collections/(?P\w+)/groups/(?P\w+)/items/$', api.views.GroupItemList.as_view(), name='group-item-list'), - url(r'^collections/(?P\w+)/groups/(?P\w+)/items/(?P\w+)/$', api.views.ItemDetail.as_view(), name='group-item-detail'), - url(r'^collections/(?P\w+)/items/$', api.views.CollectionItemList.as_view(), name='collection-item-list'), - url(r'^collections/(?P\w+)/items/(?P\w+)/$', api.views.ItemDetail.as_view(), name='collection-item-detail'), - - - url(r'^groups/$', api.views.GroupList.as_view(), name='group-list'), - url(r'^groups/(?P\w+)/$', api.views.GroupDetail.as_view(), name='group-detail'), - - url(r'^items/$', api.views.ItemList.as_view(), name='item-list'), - url(r'^items/(?P\w+)/$', api.views.ItemDetail.as_view(), name='item-detail'), + url(r'', include(search_router.urls)), + url(r'', include(user_router.urls)), - url(r'^userinfo/$', api.views.CurrentUser.as_view(), name='current-user'), - url(r'^users/$', api.views.UserList.as_view(), name='user-list'), - url(r'^users/(?P\w+)/$', api.views.UserDetail.as_view(), name='user-detail'), ] diff --git a/api/views.py b/api/views.py index c0ca633..2972dd7 100644 --- a/api/views.py +++ b/api/views.py @@ -1,830 +1,28 @@ -from django.db.models import Q -from django.core.exceptions import ObjectDoesNotExist -from rest_framework import generics -from rest_framework_json_api import pagination -from rest_framework import exceptions as drf_exceptions -from rest_framework import permissions as drf_permissions from rest_framework.decorators import api_view from rest_framework.response import Response from rest_framework.reverse import reverse -from api.serializers import CollectionSerializer, CollectionSearchSerializer, \ - GroupSerializer, ItemSerializer, ItemSearchSerializer, \ - UserSerializer, UserSearchSerializer -from api.models import Collection, Group, Item, User -from api.permissions import CanEditCollection, CanEditItem, CanEditGroup - -from drf_haystack.viewsets import HaystackViewSet - - -class CollectionSearchView(HaystackViewSet): - index_models = [Collection] - serializer_class = CollectionSearchSerializer - - -class ItemSearchView(HaystackViewSet): - index_models = [Item] - serializer_class = ItemSearchSerializer - - -class UserSearchView(HaystackViewSet): - index_models = [User] - serializer_class = UserSearchSerializer - - -class LargeResultsSetPagination(pagination.PageNumberPagination): - page_size = 100 - page_size_query_param = 'page_size' - max_page_size = 1000 @api_view(['GET']) def api_root(request): - return Response({ - 'collections': reverse('collection-list', request=request), - 'items': reverse('item-list', request=request) - }) - - -class CollectionList(generics.ListCreateAPIView): - """ View list of collections and create a new collection. - - ## Collection Attributes - - name type description - ================================================================================================================= - title string collection title - description string collection description - tags string tags describing the collection - settings object general settings for the collection (e.g. collection_type) - submission_settings object settings for the collection's submission form - created_by_org string the organization/institution associated with the collection - date_created iso8601 timestamp date/time when the collection was created - date_updated iso8601 timestamp date/time when the collection was last updated - - ## Actions - - ### Creating New Collections - - Method: POST - URL: /api/collections - Query Params: - Body (JSON): { - "data": { - "type": "collections", # required - "attributes": { - "title": {title}, # required - "description": {description}, # optional - "tags": {tag1, tag2, }, # optional - "created_by_org": {created_by_org} # optional - "settings": {settings} # optional - "submission_settings": {submission_settings} # optional - } - } - } - Success: 201 CREATED + collection representation - - ## Query Params - + `title=`: filters collections by title - - #This Request/Response - - """ - serializer_class = CollectionSerializer - permission_classes = (drf_permissions.IsAuthenticatedOrReadOnly,) - - def get_queryset(self): - queryset = Collection.objects.all().order_by('-date_created') - title = self.request.query_params.get('title', None) - if title is not None: - queryset = queryset.filter(title__icontains=title) - return queryset - - -class CollectionDetail(generics.RetrieveUpdateDestroyAPIView): - """ Details about a given collection. - - ## Collection Attributes - - name type description - ================================================================================================================= - title string collection title - description string collection description - tags string tags describing the collection - settings object general settings for the collection (e.g. collection_type) - submission_settings object settings for the collection's submission form - created_by_org string the organization/institution associated with the collection - date_created iso8601 timestamp date/time when the collection was created - date_updated iso8601 timestamp date/time when the collection was last updated - - ##Relationships - - ### Groups - - List of groups that belong to this collection. - - ### Items - - List of top-level items that belong to this collection. - - ### Created By - - User who created this collection. - - ## Actions - - ###Update - - Method: PUT / PATCH - URL: /api/collections/ - Query Params: - Body (JSON): { - "data": { - "type": "collections", # required - "id": {collection_id}, # required - "attributes": { - "title": {title}, # required - "description": {description}, # optional - "tags": {tag1, tag2, }, # optional - "created_by_org": {created_by_org} # optional - "settings": {settings} # optional - "submission_settings": {submission_settings} # optional - } - } - } - Success: 200 OK + collection representation - - Note: The `title` is required with PUT requests and optional with PATCH requests. - - ###Delete - Method: DELETE - URL: /api/collections/ - Params: - Success: 204 No Content - - #This Request/Response - - """ - queryset = Collection.objects.all() - serializer_class = CollectionSerializer - permission_classes = ( - drf_permissions.IsAuthenticatedOrReadOnly, - CanEditCollection - ) - - - def get_object(self): - try: - collection = Collection.objects.get(id=self.kwargs['pk']) - except ObjectDoesNotExist: - raise drf_exceptions.NotFound - return collection - - -class CollectionGroupList(generics.ListCreateAPIView): - """ View list of groups in a given collection/meeting, or create a new group in a given collection/meeting. - - ## Group Attributes - - name type description - ================================================================================================================= - title string group title - description string group description - date_created iso8601 timestamp date/time when the group was created - date_updated iso8601 timestamp date/time when the group was last updated - - ## Actions - - ### Creating New Groups - - Method: POST - URL: /api/collections//groups OR /api/meetings//groups - Query Params: - Body (JSON): { - "data": { - "type": "groups", # required - "attributes": { - "title": {title}, # required - "description": {description}, # optional - } - } - } - Success: 201 CREATED + group representation - - #This Request/Response - - """ - permission_classes = ( - drf_permissions.IsAuthenticatedOrReadOnly, - CanEditGroup - ) - - def get_serializer_class(self): - return GroupSerializer - - def get_queryset(self): - return Group.objects.filter(collection=self.kwargs['pk']) - - -class GroupList(generics.ListCreateAPIView): - """ View list of all groups, or create a new group in a collection or meeting. - - ## Group Attributes - - name type description - ================================================================================================================= - title string group title - description string group description - date_created iso8601 timestamp date/time when the group was created - date_updated iso8601 timestamp date/time when the group was last updated - - ## Actions - - ### Creating New Groups - - Method: POST - URL: /api/groups - Query Params: - Body (JSON): { - "data": { - "type": "groups", # required - "attributes": { - "title": {title}, # required - "description": {description} # optional - }, - "relationships": { - "collection": { - "data": { - "type": "meetings" | "collections" # required - "id": {collection_id} # required - } - } - } - } - } - Success: 201 CREATED + group representation - - Note: Since the route does not include the collection or meeting id, it must be specified in the payload. - - #This Request/Response - - """ - serializer_class = GroupSerializer - permission_classes = ( - drf_permissions.IsAuthenticatedOrReadOnly, - CanEditGroup - ) - - def get_serializer_context(self): - context = super(GroupList, self).get_serializer_context() - collection = self.request.data.get('collection', None) - if collection: - context.update({'collection_id': collection['id']}) - return context - - def get_queryset(self): - return Group.objects.all() - - -class GroupDetail(generics.RetrieveUpdateDestroyAPIView): - """ Details about a given group. - - ## Group Attributes - - name type description - ================================================================================================================= - title string group title - description string group description - date_created iso8601 timestamp date/time when the group was created - date_updated iso8601 timestamp date/time when the group was last updated - - ##Relationships - - ### Items - - List of items that belong to this group. - - ### Created By - - User who created this group. - - ## Actions - - ###Update - - Method: PUT / PATCH - URL: /api/collections//groups/ OR - /api/meetings//groups/ OR - /api/groups/ - Query Params: - Body (JSON): { - "data": { - "type": "groups", # required - "id": {group_id}, # required - "attributes": { - "title": {title}, # required - "description": {description} # optional - } - } - } - Success: 200 OK + group representation - - Note: The `title` is required with PUT requests and optional with PATCH requests. - - ###Delete - Method: DELETE - URL: /api/collections//groups/ OR - /api/meetings//groups/ OR - /api/groups/ - Params: - Success: 204 No Content - - #This Request/Response - - """ - serializer_class = GroupSerializer - queryset = Group.objects.all() - permission_classes = ( - drf_permissions.IsAuthenticatedOrReadOnly, - CanEditGroup - ) - - def get_object(self): - try: - group = Group.objects.get(id=self.kwargs['group_id']) - except ObjectDoesNotExist: - raise drf_exceptions.NotFound - return group - - -class CollectionItemList(generics.ListCreateAPIView): - """ View list of items in a given collection/meeting, or create a new item in a given collection/meeting. - - ## Item Attributes - - name type description - ================================================================================================================ - title string item title - description string item description - type string type of item (e.g. 'project', 'presentation', etc.) - status string moderation status ('approved', 'pending', 'rejected') - source_id string guid of associated OSF object (e.g. node_id for an OSF project) - url string url of associated OSF object (e.g. project url) - metadata object additional information about the item - date_created iso8601 timestamp date/time when the item was created - date_submitted iso8601 timestamp date/time when the item was submitted - date_accepted iso8601 timestamp date/time when the item was accepted - location string location of the event item - start_time iso8601 timestamp date/time when the event item begins - end_time iso8601 timestamp date/time when the event item ends - category string item category (e.g. 'talk', 'poster') - - ## Actions - - ### Creating New Items - - Method: POST - URL: /api/collections//items OR /api/meetings//items - Query Params: - Body (JSON): { - "data": { - "type": "items", # required - "attributes": { - "title": {title}, # required - "description": {description}, # optional - "type": {type}, # required - "status": {status}, # required - "source_id": {source_id}, # optional - "url": {url}, # optional - "metadata": {metadata}, # optional - "location": {location}, # optional - "start_time": {start_time}, # optional - "end_time": {end_time}, # optional - "category": {category} # required - } - } - } - Success: 201 CREATED + item representation - - Note: Items added by the collection creator will automatically have the status "approved". If "approve_all" is true - in collection.settings, items added by other users will automatically have the status "approved", otherwise - they will be "pending". - - #This Request/Response - """ - serializer_class = ItemSerializer - permission_classes = (drf_permissions.IsAuthenticatedOrReadOnly, ) - pagination_class = LargeResultsSetPagination - - def get_queryset(self): - user = self.request.user - collection_id = self.kwargs['pk'] - collection = Collection.objects.get(id=collection_id) - queryset = Item.objects.filter(collection=collection_id, group=None) - if user.id == collection.created_by_id: - return queryset - return queryset.filter(Q(status='approved') | Q(created_by=user.id)) - - -class GroupItemList(generics.ListCreateAPIView): - """ View list of items or create a new item in a given group. - - ## Item Attributes - - name type description - ================================================================================================================ - title string item title - description string item description - type string type of item (e.g. 'project', 'presentation', etc.) - status string moderation status ('approved', 'pending', 'rejected') - source_id string guid of associated OSF object (e.g. node_id for an OSF project) - url string url of associated OSF object (e.g. project url) - metadata object additional information about the item - date_created iso8601 timestamp date/time when the item was created - date_submitted iso8601 timestamp date/time when the item was submitted - date_accepted iso8601 timestamp date/time when the item was accepted - location string location of the event item - start_time iso8601 timestamp date/time when the event item begins - end_time iso8601 timestamp date/time when the event item ends - category string item category (e.g. 'talk', 'poster') - - ## Actions - - ### Creating New Items - - Method: POST - URL: /api/collections//groups//items OR - /api/meetings//groups//items - Query Params: - Body (JSON): { - "data": { - "type": "items", # required - "attributes": { - "title": {title}, # required - "description": {description}, # optional - "type": {type}, # required - "status": {status}, # required - "source_id": {source_id}, # optional - "url": {url}, # optional - "metadata": {metadata}, # optional - "location": {location}, # optional - "start_time": {start_time}, # optional - "end_time": {end_time}, # optional - "category": {category} # required - } - } - } - Success: 201 CREATED + item representation - - Note: Items added by the collection creator will automatically have the status "approved". If "approve_all" is true - in collection.settings, items added by other users will automatically have the status "approved", otherwise - they will be "pending". - - #This Request/Response """ - - serializer_class = ItemSerializer - permission_classes = (drf_permissions.IsAuthenticatedOrReadOnly,) - - def get_queryset(self): - user = self.request.user - collection_id = self.kwargs['pk'] - collection = Collection.objects.get(id=collection_id) - queryset = Item.objects.filter(group=self.kwargs['group_id']) - if user.id == collection.created_by_id: - return queryset - return queryset.filter(Q(status='approved') | Q(created_by=user.id)) - - -class ItemList(generics.ListCreateAPIView): - """ View list of all items, or create a new item in a collection/meeting or group. - - ## Item Attributes - - name type description - ================================================================================================================ - title string item title - description string item description - type string type of item (e.g. 'project', 'presentation', etc.) - status string moderation status ('approved', 'pending', 'rejected') - source_id string guid of associated OSF object (e.g. node_id for an OSF project) - url string url of associated OSF object (e.g. project url) - metadata object additional information about the item - date_created iso8601 timestamp date/time when the item was created - date_submitted iso8601 timestamp date/time when the item was submitted - date_accepted iso8601 timestamp date/time when the item was accepted - location string location of the event item - start_time iso8601 timestamp date/time when the event item begins - end_time iso8601 timestamp date/time when the event item ends - category string item category (e.g. 'talk', 'poster') - - ## Actions - - ### Creating New Items - - Method: POST - URL: /api/items - Query Params: - Body (JSON): { - "data": { - "type": "items", # required - "attributes": { - "title": {title}, # required - "description": {description}, # optional - "type": {type}, # required - "status": {status}, # required - "source_id": {source_id}, # optional - "url": {url}, # optional - "metadata": {metadata}, # optional - "location": {location}, # optional - "start_time": {start_time}, # optional - "end_time": {end_time}, # optional - "category": {category} # required - }, - "relationships": { - "collection": { - "data": { - "type": "meetings" | "collections" # required - "id": {collection_id} # required - } - }, - "group": { - "data": { - "type": "groups", # optional - "id": {group_id} # optional - } - } - } - } - } - Success: 201 CREATED + item representation - - - ### Notes: - - Since the route does not include the collection/meeting id or a group id, they must be specified in the payload. - - - Items added by the collection creator will automatically have the status "approved". If "approve_all" is true - in collection.settings, items added by other users will automatically have the status "approved", otherwise - they will be "pending". - + # OSF Collections API Root + Welcome to the browsable API for OSF Collections. Learn more about each of + the endponts by clicking on their URL in the response below. """ - serializer_class = ItemSerializer - permission_classes = (drf_permissions.IsAuthenticatedOrReadOnly,) - - def get_serializer_context(self): - context = super(ItemList, self).get_serializer_context() - collection = self.request.data.get('collection', None) - if collection: - context.update({'collection_id': collection['id']}) - group = self.request.data.get('group', None) - if group: - context.update({'group_id': group['id']}) - return context - - def get_queryset(self): - user = self.request.user - return Item.objects.filter(Q(status='approved') | Q(created_by=user.id) | Q(collection__created_by=user.id)) - - -class ItemDetail(generics.RetrieveUpdateDestroyAPIView): - """ Details for a given item. - - ## Item Attributes - - name type description - ================================================================================================================ - title string item title - description string item description - type string type of item (e.g. 'project', 'presentation', etc.) - status string moderation status ('approved', 'pending', 'rejected') - source_id string guid of associated OSF object (e.g. node_id for an OSF project) - url string url of associated OSF object (e.g. project url) - metadata object additional information about the item - date_created iso8601 timestamp date/time when the item was created - date_submitted iso8601 timestamp date/time when the item was submitted - date_accepted iso8601 timestamp date/time when the item was accepted - location string location of the event item - start_time iso8601 timestamp date/time when the event item begins - end_time iso8601 timestamp date/time when the event item ends - category string item category (e.g. 'talk', 'poster') - - ##Relationships - - ### Created By - - User who created this item. - - ## Actions - ###Update - - Method: PUT / PATCH - URL: /api/collections//groups//items/ OR - /api/meetings//groups//items/ OR - /api/items/ - Query Params: - Body (JSON): { - "data": { - "type": "items", # required - "attributes": { - "title": {title}, # required - "description": {description}, # optional - "type": {type}, # required - "status": {status}, # required - "source_id": {source_id}, # optional - "url": {url}, # optional - "metadata": {metadata}, # optional - "location": {location}, # optional - "start_time": {start_time}, # optional - "end_time": {end_time}, # optional - "category": {category} # required - } - } - } - Success: 200 OK + item representation - - Note: The `title`, `type`, `status` and `category` fields are required for PUT and optional for PATCH requests. - - ###Delete - Method: DELETE - URL: /api/collections//groups//items/ OR - /api/meetings//groups//items/ OR - /api/items/ - Params: - Success: 204 No Content - - #This Request/Response - - """ - serializer_class = ItemSerializer - queryset = Item.objects.all() - permission_classes = ( - drf_permissions.IsAuthenticatedOrReadOnly, - CanEditItem - ) - - def get_serializer_context(self): - context = super(ItemDetail, self).get_serializer_context() - collection = self.request.data.get('collection', None) - if collection: - context.update({'collection_id': collection['id']}) - group = self.request.data.get('group', None) - if group: - context.update({'group_id': group['id']}) - else: - context.update({'group_id': None}) - - return context - - def get_object(self): - try: - item = Item.objects.get(id=self.kwargs['item_id']) - except ObjectDoesNotExist: - raise drf_exceptions.NotFound - return item - - -class CurrentUser(generics.RetrieveUpdateDestroyAPIView): - """ Details about the currently logged-in user. - - ## User Attributes - - name type description - ====================================================================================================== - username string username for the user - first_name string first name of the user - last_name string last name of the user - email string email address associated with the user's account - date_joined iso8601 timestamp date/time of account creation - last_login iso8601 timestamp date/time of last login - is_active boolean whether the user account is active - gravatar string link to user gravatar - token string access token for social account (used for OAUTH) - - ## Actions - - ###Update - - Method: PUT / PATCH - URL: /api/users/ - Query Params: - Body (JSON): { - "data": { - "type": "users", # required - "id": {user_id}, # required - "attributes": { - "username": {username}, # required - "first_name": {first_name}, # optional - "last_name": {last_name}, # optional - "email": {email} # optional - "date_joined": {date_joined} # optional - "last_login": {last_login} # optional - "is_active": {is_active} # optional - "gravatar": {gravatar} # optional - } - } - } - Success: 200 OK + user representation - - ###Delete - Method: DELETE - URL: /api/users/ - Params: - Success: 204 No Content - - #This Request/Response - - """ - queryset = User.objects.all() - serializer_class = UserSerializer - - def get_object(self): - return self.request.user - - -class UserList(generics.ListAPIView): - """ View list of users. - - ## User Attributes - - name type description - ====================================================================================================== - username string username for the user - first_name string first name of the user - last_name string last name of the user - email string email address associated with the user's account - date_joined iso8601 timestamp date/time of account creation - last_login iso8601 timestamp date/time of last login - is_active boolean whether the user account is active - gravatar string link to user gravatar - token string access token for social account (used for OAUTH) - - #This Request/Response - - """ - serializer_class = UserSerializer - - # permission_classes = (drf_permissions.IsAuthenticatedOrReadOnly, ) - - def get_queryset(self): - return User.objects.all() - - -class UserDetail(generics.RetrieveUpdateDestroyAPIView): - """ Details about a given user. - - ## User Attributes - - name type description - ====================================================================================================== - username string username for the user - first_name string first name of the user - last_name string last name of the user - email string email address associated with the user's account - date_joined iso8601 timestamp date/time of account creation - last_login iso8601 timestamp date/time of last login - is_active boolean whether the user account is active - gravatar string link to user gravatar - token string access token for social account (used for OAUTH) - - ## Actions - - ###Update - - Method: PUT / PATCH - URL: /api/users/ - Query Params: - Body (JSON): { - "data": { - "type": "users", # required - "id": {user_id}, # required - "attributes": { - "username": {username}, # required - "first_name": {first_name}, # optional - "last_name": {last_name}, # optional - "email": {email} # optional - "date_joined": {date_joined} # optional - "last_login": {last_login} # optional - "is_active": {is_active} # optional - "gravatar": {gravatar} # optional - } - } - } - Success: 200 OK + user representation - - ###Delete - Method: DELETE - URL: /api/users/ - Params: - Success: 204 No Content - - #This Request/Response - """ - serializer_class = UserSerializer + return Response({ - # permission_classes = (drf_permissions.IsAuthenticatedOrReadOnly, ) + 'collections': reverse('collection-list', request=request), + 'items': reverse('item-list', request=request), + 'documents': reverse('document-list', request=request), + 'collection-memberships': reverse('collectionmembership-list', request=request), + 'schemas': reverse('schema-list', request=request), + 'workflows': reverse('workflow-list', request=request), + 'cases': reverse('case-list', request=request), + 'widgets': reverse('widget-list', request=request), + 'parameters': reverse('parameter-list', request=request), + 'parameter-stubs': reverse('parameterstub-list', request=request), + 'parameter-aliases': reverse('parameteralias-list', request=request), + 'sections': reverse('section-list', request=request) + }) - def get_object(self): - try: - user = User.objects.get(id=self.kwargs['user_id']) - except ObjectDoesNotExist: - raise drf_exceptions.NotFound - return user diff --git a/auth/__pycache__/__init__.cpython-36.pyc b/auth/__pycache__/__init__.cpython-36.pyc deleted file mode 100644 index 29d0112..0000000 Binary files a/auth/__pycache__/__init__.cpython-36.pyc and /dev/null differ diff --git a/auth/__pycache__/authentication.cpython-36.pyc b/auth/__pycache__/authentication.cpython-36.pyc deleted file mode 100644 index 3e4961c..0000000 Binary files a/auth/__pycache__/authentication.cpython-36.pyc and /dev/null differ diff --git a/auth/__pycache__/middleware.cpython-36.pyc b/auth/__pycache__/middleware.cpython-36.pyc deleted file mode 100644 index 45fdd01..0000000 Binary files a/auth/__pycache__/middleware.cpython-36.pyc and /dev/null differ diff --git a/auth/__init__.py b/collection/__init__.py similarity index 100% rename from auth/__init__.py rename to collection/__init__.py diff --git a/api/admin.py b/collection/admin.py similarity index 66% rename from api/admin.py rename to collection/admin.py index 9541d69..2ad465b 100644 --- a/api/admin.py +++ b/collection/admin.py @@ -1,12 +1,44 @@ +""" +#Admin Site Views + +- CollectionAdmin +- ItemAdmin +- DocumentAdmin +- CollectionMembershipAdmin +- SchemaAdmin + +""" + + +# Library Imports +# ############################################################################# + + from django import forms from django.contrib import admin from django.contrib.auth import get_user_model from django.contrib.auth.admin import UserAdmin from django.contrib.admin.helpers import ActionForm -from api.models import Collection, Group, Item from guardian.shortcuts import get_objects_for_user, assign_perm +# App Imports +# ############################################################################# + + +from collection.models import ( + Collection, + Item, + Document, + CollectionMembership, + Schema +) + + +# Admin Helpers +# ############################################################################# + + def approve_item(modeladmin, request, queryset): queryset.update(status='approved') @@ -22,12 +54,12 @@ def add_admins(modeladmin, request, queryset): user.save() -class AdminForm(ActionForm): - collection_id = forms.CharField() +# Admin Classes +# ############################################################################# -class GroupAdmin(admin.ModelAdmin): - list_display = ('title', 'collection') +class AdminForm(ActionForm): + collection_id = forms.CharField() class ItemAdmin(admin.ModelAdmin): @@ -42,13 +74,5 @@ def get_search_results(self, request, queryset, search_term): return self.model.objects.filter(collection__in=can_moderate), True -class OSFUserAdmin(UserAdmin): - model = get_user_model() - action_form = AdminForm - actions = [add_admins] - - -admin.site.register(Collection) -admin.site.register(Group, GroupAdmin) -admin.site.register(Item, ItemAdmin) -admin.site.register(get_user_model(), OSFUserAdmin) +# EOF +# ############################################################################# diff --git a/collection/apps.py b/collection/apps.py new file mode 100644 index 0000000..36dedb9 --- /dev/null +++ b/collection/apps.py @@ -0,0 +1,7 @@ +from __future__ import unicode_literals + +from django.apps import AppConfig + + +class CollectionConfig(AppConfig): + name = 'collection' diff --git a/collection/migrations/0001_initial.py b/collection/migrations/0001_initial.py new file mode 100644 index 0000000..07ff8b9 --- /dev/null +++ b/collection/migrations/0001_initial.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-10-15 22:32 +from __future__ import unicode_literals + +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Collection', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=200)), + ('description', models.TextField(blank=True, null=True)), + ('page_settings', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default={})), + ('date_created', models.DateTimeField(auto_now_add=True)), + ('date_updated', models.DateTimeField(auto_now=True)), + ], + options={ + 'permissions': (('approve_collection_items', 'Approve collection items'),), + }, + ), + migrations.CreateModel( + name='CollectionMembership', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('label', models.CharField(max_length=200, null=True)), + ('role', models.CharField(max_length=200, null=True)), + ('date_created', models.DateTimeField(auto_now_add=True)), + ('date_updated', models.DateTimeField(auto_now=True)), + ], + ), + migrations.CreateModel( + name='Document', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('content', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default={}, null=True)), + ('date_created', models.DateTimeField(auto_now_add=True)), + ('date_updated', models.DateTimeField(auto_now=True)), + ], + ), + migrations.CreateModel( + name='Item', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=200)), + ('description', models.TextField(blank=True, null=True)), + ('type', models.CharField(max_length=200)), + ('date_created', models.DateTimeField(auto_now_add=True)), + ('date_updated', models.DateTimeField(auto_now=True)), + ], + options={ + 'permissions': (('approve_collection_items', 'Approve collection items'),), + }, + ), + migrations.CreateModel( + name='Schema', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200, null=True)), + ('definition', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default={}, null=True)), + ('date_created', models.DateTimeField(auto_now_add=True)), + ('date_updated', models.DateTimeField(auto_now=True)), + ], + ), + ] diff --git a/collection/migrations/0002_auto_20171015_2232.py b/collection/migrations/0002_auto_20171015_2232.py new file mode 100644 index 0000000..a8a5d94 --- /dev/null +++ b/collection/migrations/0002_auto_20171015_2232.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-10-15 22:32 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('workflow', '0001_initial'), + ('collection', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='schema', + name='owner', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='item', + name='owner', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='item', + name='schemas', + field=models.ManyToManyField(related_name='items', through='collection.Document', to='collection.Schema'), + ), + migrations.AddField( + model_name='document', + name='collections', + field=models.ManyToManyField(related_name='documents', to='collection.Collection'), + ), + migrations.AddField( + model_name='document', + name='item', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='collection.Item'), + ), + migrations.AddField( + model_name='document', + name='owner', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='document', + name='schema', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='collection.Schema'), + ), + migrations.AddField( + model_name='collectionmembership', + name='collection', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='collection_memberships', to='collection.Collection'), + ), + migrations.AddField( + model_name='collectionmembership', + name='document', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='memberships', to='collection.Document'), + ), + migrations.AddField( + model_name='collectionmembership', + name='item', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='memberships', to='collection.Item'), + ), + migrations.AddField( + model_name='collectionmembership', + name='schema', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='memberships', to='collection.Schema'), + ), + migrations.AddField( + model_name='collection', + name='items', + field=models.ManyToManyField(related_name='collections', through='collection.CollectionMembership', to='collection.Item'), + ), + migrations.AddField( + model_name='collection', + name='owner', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='collection', + name='schemas', + field=models.ManyToManyField(related_name='collections', through='collection.CollectionMembership', to='collection.Schema'), + ), + migrations.AddField( + model_name='collection', + name='workflow', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='collections', to='workflow.Workflow'), + ), + migrations.AlterUniqueTogether( + name='collectionmembership', + unique_together=set([('item', 'collection')]), + ), + ] diff --git a/collection/migrations/__init__.py b/collection/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/collection/mixins.py b/collection/mixins.py new file mode 100644 index 0000000..3d06d9e --- /dev/null +++ b/collection/mixins.py @@ -0,0 +1,45 @@ +""" +Collection Mixins +""" + + +# Library Imports +# ############################################################################# + + +from django.db.models import ForeignKey +from django.contrib.postgres.fields import JSONField + + +# App Imports +# ############################################################################# + + +from collection.models import Collection +from tests import resources + + +# Models +# ############################################################################# + + + +class CollectionUserMixin: + + _collection = ForeignKey( + "Collection", + blank=True, + null=True + ) + + @property + def collection(self, *args, **kwargs): + if getattr(self._collection, "null", None): + self._collection = Collection() + self._collection.title = self.full_name + "'s Collection" + self._collection.description = "My Collection" + self._collection.owner = self + self._collection.save() + return self._collection + + diff --git a/collection/models.py b/collection/models.py new file mode 100644 index 0000000..ee3e51c --- /dev/null +++ b/collection/models.py @@ -0,0 +1,219 @@ +""" +Collection Models +""" + + +# Imports +# ############################################################################# + + +from django.conf import settings +from django.db.models import ( + Model, + CharField, + TextField, + ForeignKey, + DateTimeField, + ManyToManyField, + SET_NULL +) +from django.contrib.auth.models import AbstractUser +from django.contrib.postgres.fields import JSONField + +from tests import resources + + +# Models +# ############################################################################# + + +class Collection(Model): + """ + # `collection.models.Collection` + + The `Collection` model represents a collection in the collections app. + These are not separate models because using the same model allows a + collection to be an item in another collection, leading to collections of + collections. + + Every collection has basic information that should be stored about it, such + as its title and description, and various dates pertaining to creation and + updates. + + Each collection also has items + """ + + title = CharField(max_length=200) + description = TextField(null=True, blank=True) + page_settings = JSONField(default={}, blank=True) + owner = ForeignKey(settings.AUTH_USER_MODEL) + date_created = DateTimeField(auto_now_add=True) + date_updated = DateTimeField(auto_now=True) + + schemas = ManyToManyField( + "Schema", + through="CollectionMembership", + related_name="collections" + ) + + items = ManyToManyField( + "Item", + through="CollectionMembership", + related_name="collections" + ) + + workflow = ForeignKey( + 'workflow.Workflow', + null=True, + blank=True, + related_name="collections", + on_delete=SET_NULL + ) + + def __str__(self): + return self.title + + class Meta: + permissions = ( + ('approve_collection_items', 'Approve collection items'), + ) + + +class Item(Model): + """ + # `collection.model.Item` + + The `Item` model represents an in a collection. Every `Item` has basic + information that should be stored about it, such as its title and + description, and various dates pertaining to creation and updates. + + Each `Item` also has content, this may be the information stored on the node, + and/or other metadata. This content is defined by the related Schema. + """ + + title = CharField(max_length=200) + description = TextField(null=True, blank=True) + type = CharField(max_length=200) + owner = ForeignKey(settings.AUTH_USER_MODEL) + date_created = DateTimeField(auto_now_add=True) + date_updated = DateTimeField(auto_now=True) + + schemas = ManyToManyField( + "Schema", + through="Document", + related_name="items" + ) + + def __str__(self): + return self.title + + class Meta: + permissions = ( + ('approve_collection_items', 'Approve collection items'), + ) + + +class Document(Model): + """ + # `collection.models.Document` + + The `Document` model represents the data of an `Item` for a particular + schema. A given `Item` may have different representations of the data it + contains. The `Document` model is an instance of one of those + representations. + + A `Items`'s document may only be applicable to certain collections, so the + `Document` represents this with relatioships to `Collection` through + `CollectionMembership`. + """ + + content = JSONField(default={}, null=True, blank=True) + owner = ForeignKey(settings.AUTH_USER_MODEL) + date_created = DateTimeField(auto_now_add=True) + date_updated = DateTimeField(auto_now=True) + + schema = ForeignKey( + "Schema", + related_name="documents", + null=False, + blank=False, + ) + + collections = ManyToManyField( + "Collection", + related_name="documents", + ) + + item = ForeignKey( + "Item", + related_name="documents" + ) + + +class CollectionMembership(Model): + """ + # `collection.models.CollectionMembership` + + The `CollectionMembership` model represents a relationship between an item + and a collection it is a member of. + + `CollectionMembership` may be used to constrain content instances to limit + the `Item`'s content if it only applies whe the node is viewed as a member + of a particular `Collection`. + + A label is also available. + """ + + label = CharField(max_length=200, null=True) + role = CharField(max_length=200, null=True) + date_created = DateTimeField(auto_now_add=True) + date_updated = DateTimeField(auto_now=True) + + item = ForeignKey( + "Item", + related_name="memberships", + blank=False, + null=False + ) + + collection = ForeignKey( + "Collection", + blank=False, + null=False, + related_name="collection_memberships" + ) + + document = ForeignKey( + "Document", + blank=False, + null=False, + related_name="memberships" + ) + + schema = ForeignKey( + "Schema", + related_name="memberships", + null=False, + blank=False, + ) + + class Meta: + unique_together = ["item", "collection"] + + +class Schema(Model): + """ + # `Schema` + + The schema defines what valid data for a document'content looks like. + """ + + name = CharField(max_length=200, null=True) + definition = JSONField(default={}, null=True, blank=True) + owner = ForeignKey(settings.AUTH_USER_MODEL) + date_created = DateTimeField(auto_now_add=True) + date_updated = DateTimeField(auto_now=True) + + +# EOF +# ############################################################################# diff --git a/api/permissions.py b/collection/permissions.py similarity index 94% rename from api/permissions.py rename to collection/permissions.py index df51265..1972ddd 100644 --- a/api/permissions.py +++ b/collection/permissions.py @@ -1,5 +1,8 @@ from rest_framework import permissions -from api.models import Group, Item, Collection +from collection.models import ( + Item, + Collection +) class CanEditCollection(permissions.BasePermission): diff --git a/collection/routers.py b/collection/routers.py new file mode 100644 index 0000000..6398704 --- /dev/null +++ b/collection/routers.py @@ -0,0 +1,43 @@ +""" +# Collection Router +""" + + +# Library Imports +# ############################################################################# + + +from django.conf.urls import url, include +from rest_framework.urlpatterns import format_suffix_patterns +from rest_framework_nested import routers +from collections import namedtuple + + +# App Imports +# ############################################################################# + + +from collection.views import ( + CollectionViewSet, + ItemViewSet, + DocumentViewSet, + CollectionMembershipViewSet, + SchemaViewSet +) + + +# Router Setup +# ############################################################################# + + +collection_router = routers.DefaultRouter(trailing_slash=False) + +collection_router.register(r'collections', CollectionViewSet) +collection_router.register(r'items', ItemViewSet) +collection_router.register(r'documents', DocumentViewSet) +collection_router.register(r'collection-memberships', CollectionMembershipViewSet) +collection_router.register(r'schemas', SchemaViewSet) + + +# EOF +# ############################################################################# diff --git a/collection/schemas/json-schema.json b/collection/schemas/json-schema.json new file mode 100644 index 0000000..85eb502 --- /dev/null +++ b/collection/schemas/json-schema.json @@ -0,0 +1,150 @@ +{ + "id": "http://json-schema.org/draft-04/schema#", + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Core schema meta-schema", + "definitions": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#" } + }, + "positiveInteger": { + "type": "integer", + "minimum": 0 + }, + "positiveIntegerDefault0": { + "allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ] + }, + "simpleTypes": { + "enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ] + }, + "stringArray": { + "type": "array", + "items": { "type": "string" }, + "minItems": 1, + "uniqueItems": true + } + }, + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uri" + }, + "$schema": { + "type": "string", + "format": "uri" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "default": {}, + "multipleOf": { + "type": "number", + "minimum": 0, + "exclusiveMinimum": true + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "boolean", + "default": false + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "boolean", + "default": false + }, + "maxLength": { "$ref": "#/definitions/positiveInteger" }, + "minLength": { "$ref": "#/definitions/positiveIntegerDefault0" }, + "pattern": { + "type": "string", + "format": "regex" + }, + "additionalItems": { + "anyOf": [ + { "type": "boolean" }, + { "$ref": "#" } + ], + "default": {} + }, + "items": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/schemaArray" } + ], + "default": {} + }, + "maxItems": { "$ref": "#/definitions/positiveInteger" }, + "minItems": { "$ref": "#/definitions/positiveIntegerDefault0" }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "maxProperties": { "$ref": "#/definitions/positiveInteger" }, + "minProperties": { "$ref": "#/definitions/positiveIntegerDefault0" }, + "required": { "$ref": "#/definitions/stringArray" }, + "additionalProperties": { + "anyOf": [ + { "type": "boolean" }, + { "$ref": "#" } + ], + "default": {} + }, + "definitions": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "properties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "dependencies": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/stringArray" } + ] + } + }, + "enum": { + "type": "array", + "minItems": 1, + "uniqueItems": true + }, + "type": { + "anyOf": [ + { "$ref": "#/definitions/simpleTypes" }, + { + "type": "array", + "items": { "$ref": "#/definitions/simpleTypes" }, + "minItems": 1, + "uniqueItems": true + } + ] + }, + "allOf": { "$ref": "#/definitions/schemaArray" }, + "anyOf": { "$ref": "#/definitions/schemaArray" }, + "oneOf": { "$ref": "#/definitions/schemaArray" }, + "not": { "$ref": "#" } + }, + "dependencies": { + "exclusiveMaximum": [ "maximum" ], + "exclusiveMinimum": [ "minimum" ] + }, + "default": {} +} diff --git a/api/search_indexes.py b/collection/search_indexes.py similarity index 64% rename from api/search_indexes.py rename to collection/search_indexes.py index c1d99af..693416e 100644 --- a/api/search_indexes.py +++ b/collection/search_indexes.py @@ -1,7 +1,8 @@ from haystack import indexes -from api.models import Collection, Item from django.contrib.auth.models import User +from collection.models import Collection, Item + class CollectionIndex(indexes.SearchIndex, indexes.Indexable): text = indexes.CharField(document=True, use_template=True) @@ -13,7 +14,8 @@ def get_model(self): return Collection def index_queryset(self, using=None): - """Used when the entire index for model is updated + """ + Used when the entire index for model is updated """ return self.get_model().objects.all() @@ -29,20 +31,9 @@ def get_model(self): return Item def index_queryset(self, using=None): - """Used when the entire index for model is updated + """ + Used when the entire index for model is updated """ return self.get_model().objects.all() -class UserIndex(indexes.SearchIndex, indexes.Indexable): - text = indexes.CharField(document=True, use_template=True) - first_name = indexes.CharField(model_attr="first_name") - last_name = indexes.CharField(model_attr="last_name") - - def get_model(self): - return User - - def index_queryset(self, using=None): - """Used when the entire index for model is updated - """ - return self.get_model().objects.all() diff --git a/collection/serializers.py b/collection/serializers.py new file mode 100644 index 0000000..fde84a3 --- /dev/null +++ b/collection/serializers.py @@ -0,0 +1,455 @@ +""" +Collection Serializers +""" + + +# Library Imports +# ############################################################################# + + +from django.contrib.auth import get_user_model +from rest_framework.utils import model_meta +from rest_framework.serializers import ( + ModelSerializer, + JSONField, + raise_errors_on_nested_writes +) +from rest_framework_json_api.serializers import ( + Serializer, + CharField, + DateTimeField +) +from rest_framework_json_api.relations import ResourceRelatedField +from drf_haystack.serializers import HaystackSerializer + + +# App Imports +# ############################################################################# + + +from collection.models import ( + Collection, + Item, + Document, + CollectionMembership, + Schema +) + +# TODO Use app import so `Workflow` doesn't need to be imported here. +from workflow.models import Workflow + +from collection import search_indexes + + +# Model Serializers +# ############################################################################# + + +class CollectionModelSerializer(ModelSerializer): + def create(self, validated_data): + """ + We have a bit of extra checking around this in order to provide + descriptive messages when something goes wrong, but this method is + essentially just: + return ExampleModel.objects.create(**validated_data) + If there are many to many fields present on the instance then they + cannot be set until the model is instantiated, in which case the + implementation is like so: + example_relationship = validated_data.pop('example_relationship') + instance = ExampleModel.objects.create(**validated_data) + instance.example_relationship = example_relationship + return instance + The default implementation also does not handle nested relationships. + If you want to support writable nested relationships you'll need + to write an explicit `.create()` method. + """ + raise_errors_on_nested_writes('create', self, validated_data) + + ModelClass = self.Meta.model + + # Remove many-to-many relationships from validated_data. + # They are not valid arguments to the default `.create()` method, + # as they require that the instance has already been saved. + info = model_meta.get_field_info(ModelClass) + many_to_many = {} + for field_name, relation_info in info.relations.items(): + if relation_info.to_many and (field_name in validated_data): + many_to_many[field_name] = validated_data.pop(field_name) + + try: + instance = ModelClass.objects.create(**validated_data) + except TypeError: + tb = traceback.format_exc() + msg = ( + 'Got a `TypeError` when calling `%s.objects.create()`. ' + 'This may be because you have a writable field on the ' + 'serializer class that is not a valid argument to ' + '`%s.objects.create()`. You may need to make the field ' + 'read-only, or override the %s.create() method to handle ' + 'this correctly.\nOriginal exception was:\n %s' % + ( + ModelClass.__name__, + ModelClass.__name__, + self.__class__.__name__, + tb + ) + ) + raise TypeError(msg) + + # Save many-to-many relationships after the instance is created. + if many_to_many: + for field_name, value in many_to_many.items(): + + # If the m2m has a model defined as a through table, then + # relations cannot be added by that relationship's .add() method, + # but should be created using the model's constructor. + # This loop checks to ensure that if the relation does not exist + # it is created. The way this is set up now, it requires the relation + # to be unique. + if field_name in info.relations and info.relations[field_name].has_through_model: + field = info.relations[field_name].model_field + for related_instance in value: + through_class = field.rel.through + through_instance = through_class() + existing_through_instances = through_class.objects\ + .filter(**{field.m2m_field_name()+"_id": instance.id})\ + .filter(**{field.m2m_reverse_field_name()+"_id": related_instance.id}) + if existing_through_instances.exists(): + continue + setattr(through_instance, field.m2m_field_name(), instance) + setattr(through_instance, field.m2m_reverse_field_name(), related_instance) + through_instance.save() + + else: + field = getattr(instance, field_name) + field.set(value) + return instance + + def update(self, instance, validated_data): + raise_errors_on_nested_writes('update', self, validated_data) + info = model_meta.get_field_info(instance) + + # Simply set each attribute on the instance, and then save it. + # Note that unlike `.create()` we don't need to treat many-to-many + # relationships as being a special case. During updates we already + # have an instance pk for the relationships to be associated with. + for attr, value in validated_data.items(): + + # If the m2m has a model defined as a through table, then + # relations cannot be added by that relationship's .add() method, + # but should be created using the model's constructor. + # This loop checks to ensure that if the relation does not exist + # it is created. The way this is set up now, it requires the relation + # to be unique. + if attr in info.relations and info.relations[attr].has_through_model: + field = info.relations[attr].model_field + for related_instance in value: + through_class = field.rel.through + through_instance = through_class() + existing_through_instances = through_class.objects\ + .filter(**{field.m2m_field_name()+"_id": instance.id})\ + .filter(**{field.m2m_reverse_field_name()+"_id": related_instance.id}) + if existing_through_instances.exists(): + continue + setattr(through_instance, field.m2m_field_name(), instance) + setattr(through_instance, field.m2m_reverse_field_name(), related_instance) + through_instance.save() + + elif attr in info.relations and info.relations[attr].to_many: + field = getattr(instance, attr) + field.set(value) + + else: + setattr(instance, attr, value) + + instance.save() + + return instance + + +class CollectionSerializer(CollectionModelSerializer): + """ + # `collection.serializers.CollectionSerializer` + + The `CollectionSerializer` takes incoming payloads and builds `Collection` + objects, and takes `Collection` objects and returns payloads. + """ + + id = CharField(read_only=True) + title = CharField() + description = CharField(required=False, allow_null=True) + owner = ResourceRelatedField( + queryset=get_user_model().objects.all(), + many=False, + required=True + ) + date_created = DateTimeField(read_only=True) + date_updated = DateTimeField(read_only=True) + + schemas = ResourceRelatedField( + queryset=Schema.objects.all(), + many=True, + required=False + ) + + items = ResourceRelatedField( + queryset=Item.objects.all(), + many=True, + required=False + ) + + workflow = ResourceRelatedField( + queryset=Workflow.objects.all(), + many=False, + required=True + ) + + collection_memberships = ResourceRelatedField( + queryset=CollectionMembership.objects.all(), + many=True, + required=False + ) + + documents = ResourceRelatedField( + queryset=Document.objects.all(), + many=True, + required=False + ) + + + class Meta: + model = Collection + fields = [ + "id", + "title", + "description", + "page_settings", + "owner", + "date_created", + "date_updated", + "schemas", + "items", + "workflow", + "collection_memberships", + "documents" + ] + + class JSONAPIMeta: + resource_name = 'collection' + + +class ItemSerializer(CollectionModelSerializer): + """ + # `collection.serializers.ItemSerializer` + + Serializer for the `Item` model + """ + + id = CharField(read_only=True) + title = CharField() + description = CharField(required=False, allow_null=True) + owner = ResourceRelatedField( + queryset=get_user_model().objects.all(), + many=False, + required=True + ) + date_created = DateTimeField(read_only=True) + date_updated = DateTimeField(read_only=True) + + collections = ResourceRelatedField( + queryset=Collection.objects.all(), + many=True, + required=False + ) + + documents = ResourceRelatedField( + queryset=Document.objects.all(), + many=True, + required=False + ) + + memberships = ResourceRelatedField( + queryset=CollectionMembership.objects.all(), + many=True, + required=False + ) + + class Meta: + model = Item + fields = [ + "id", + "title", + "description", + "type", + "owner", + "date_created", + "date_updated", + "collections", + "documents", + "memberships" + ] + + class JSONAPIMeta: + resource_name = 'item' + + +class DocumentSerializer(CollectionModelSerializer): + """ + # `collection.serializers.DocumentSerializer` + + Serializer for the `Document` model + """ + + id = CharField(read_only=True) + content = CharField(required=False, allow_null=True) + owner = ResourceRelatedField( + queryset=get_user_model().objects.all(), + many=False, + required=True + ) + date_created = DateTimeField(read_only=True) + date_updated = DateTimeField(read_only=True) + + memberships = ResourceRelatedField( + queryset=CollectionMembership.objects.all(), + many=True, + required=True + ) + + class Meta: + model = Document + fields = [ + "id", + "content", + "owner", + "date_created", + "date_updated", + "schema", + "collections", + "item", + "memberships" + ] + + class JSONAPIMeta: + resource_name = 'document' + + +class CollectionMembershipSerializer(CollectionModelSerializer): + """ + # `collection.serializers.CollectionMembershipSerializer` + + Serializer for the `CollectionMembership` model + """ + + id = CharField(read_only=True) + label = CharField(required=False, allow_blank=False) + role = CharField(required=False, allow_blank=False) + date_created = DateTimeField(read_only=True) + date_updated = DateTimeField(read_only=True) + + item = ResourceRelatedField( + queryset=Item.objects.all(), + many=False, + required=True + ) + + collection = ResourceRelatedField( + queryset=Collection.objects.all(), + many=False, + required=True + ) + + document = ResourceRelatedField( + queryset=Document.objects.all(), + many=False, + required=True + ) + + class Meta: + model = CollectionMembership + fields = [ + "label", + "role", + "date_created", + "date_updated", + "item", + "collection", + "document" + ] + + class JSONAPIMeta: + resource_name = "collection-membership" + + +class SchemaSerializer(CollectionModelSerializer): + """ + # `collection.serializers.SchemaSerializer` + + Serializer for the `Schema` model + """ + + owner = ResourceRelatedField( + queryset=get_user_model().objects.all(), + many=False, + required=True + ) + + class Meta: + model = Schema + fields = [ + "name", + "definition", + "owner", + "date_created", + "date_updated", + "documents", + "items", + "collections" + ] + + class JSONAPIMeta: + resoure_name = "schema" + + +# Search Serializers for Collections +# ############################################################################# + + +class ItemSearchSerializer(HaystackSerializer): + """ + # `collection.serializers.ItemSearchSerializer` + + The `ItemSearchSerializer` + """ + + class Meta: + index_classes = [search_indexes.ItemIndex] + fields = [ + 'text', + 'title', + 'description', + 'created_by', + 'collection' + ] + + +class CollectionSearchSerializer(HaystackSerializer): + """ + # `collection.serializers.CollectionSearchSerializer` + + The `CollectionSearchSerializer` + """ + + class Meta: + index_classes = [search_indexes.ItemIndex] + fields = [ + 'text', + 'title', + 'description', + 'created_by', + 'collection' + ] + + +# EOF +# ############################################################################# diff --git a/collection/urls.py b/collection/urls.py new file mode 100644 index 0000000..46f1b24 --- /dev/null +++ b/collection/urls.py @@ -0,0 +1,41 @@ +from django.conf.urls import url, include +import api.views +from rest_framework.routers import DefaultRouter + +from collection.routers import collection_router +from workflow.routers import workflow_router, case_router +from auth.routers import auth_router + +search_router = DefaultRouter() +search_router.register("collections/search", api.views.CollectionSearchView, base_name="collection-search") +search_router.register("items/search", api.views.ItemSearchView, base_name="item-search") +search_router.register("users/search", api.views.UserSearchView, base_name="user-search") + +urlpatterns = [ + + url(r'^$', api.views.api_root), + url(r'', include(collection_router.urls)), + url(r'', include(workflow_router.urls)), + url(r'', include(case_router.urls)), + url(r'', include(search_router.urls)), + url(r'', include(auth_router.urls)), + url(r'^collections/$', api.views.CollectionList.as_view(), name='collection-list'), + url(r'^collections/(?P\w+)/$', api.views.CollectionDetail.as_view(), name='collection-detail'), + url(r'^collections/(?P\w+)/groups/$', api.views.CollectionGroupList.as_view(), name='collection-group-list'), + url(r'^collections/(?P\w+)/groups/(?P\w+)/$', api.views.GroupDetail.as_view(), name='collection-group-detail'), + url(r'^collections/(?P\w+)/groups/(?P\w+)/items/$', api.views.GroupItemList.as_view(), name='group-item-list'), + url(r'^collections/(?P\w+)/groups/(?P\w+)/items/(?P\w+)/$', api.views.ItemDetail.as_view(), name='group-item-detail'), + url(r'^collections/(?P\w+)/items/$', api.views.CollectionItemList.as_view(), name='collection-item-list'), + url(r'^collections/(?P\w+)/items/(?P\w+)/$', api.views.ItemDetail.as_view(), name='collection-item-detail'), + + + url(r'^groups/$', api.views.GroupList.as_view(), name='group-list'), + url(r'^groups/(?P\w+)/$', api.views.GroupDetail.as_view(), name='group-detail'), + + url(r'^items/$', api.views.ItemList.as_view(), name='item-list'), + url(r'^items/(?P\w+)/$', api.views.ItemDetail.as_view(), name='item-detail'), + +# url(r'^userinfo/$', api.views.CurrentUser.as_view(), name='current-user'), +# url(r'^users/$', api.views.UserList.as_view(), name='user-list'), +# url(r'^users/(?P\w+)/$', api.views.UserDetail.as_view(), name='user-detail'), +] diff --git a/collection/views.py b/collection/views.py new file mode 100644 index 0000000..1821ad8 --- /dev/null +++ b/collection/views.py @@ -0,0 +1,322 @@ +""" +# Collection Views + +## Defines +- CollectionViewSet +- ItemViewSet +- ColletionMembershipViweSet +- DocumentviewSet +""" + + +# Imports +# ############################################################################# + + +from django.http import HttpResponse +from django.db.models import Q +from django.core.exceptions import ObjectDoesNotExist +from rest_framework import generics +from rest_framework.viewsets import ModelViewSet +from rest_framework_json_api import pagination +from rest_framework import exceptions as drf_exceptions +from rest_framework import permissions as drf_permissions +from rest_framework.decorators import api_view +from rest_framework.response import Response +from rest_framework.reverse import reverse +from drf_haystack.viewsets import HaystackViewSet +from guardian.shortcuts import assign_perm + +from collection.permissions import ( + CanEditCollection, + CanEditItem, + CanEditGroup +) +from collection.models import ( + Collection, + Item, + Document, + CollectionMembership, + Schema +) +from collection.serializers import ( + CollectionSerializer, + ItemSerializer, + DocumentSerializer, + CollectionMembershipSerializer, + SchemaSerializer, + CollectionSearchSerializer, + ItemSearchSerializer, +) + + +# Views +# ############################################################################# + + +class CollectionViewSet(ModelViewSet): + """ + # `CollectionViewSet` + + `ViewSet` for inteacting with Collection models. + + ## `Collection` Attributes + + name type description + ======================================================================= + title string group title + description string group description + date_created iso8601 timestamp date/time when the group was created + date_updated iso8601 timestamp date/time when the group was last + updated + + ## Actions + + ### Creating New Groups + + Method: POST + URL: /api/groups + Query Params: + Body (JSON): { + "data": { + "type": "groups", # required + "attributes": { + "title": {title}, # required + "description": {description} # optional + }, + "relationships": { + "collection": { + "data": { + "type": "meetings" | "collections" # required + "id": {collection_id} # required + } + } + } + } + } + Success: 201 CREATED + group representation + + Note: Since the route does not include the collection or meeting id, it + must be specified in the payload. + + ## This Request/Response + """ + + queryset = Collection.objects.all() + serializer_class = CollectionSerializer + + def get_queryset(self): + + queryset = Collection.objects.all().order_by('-date_created') + + user_id = self.request.query_params.get('user') + username = self.request.query_params.get('username') + org_name = self.request.query_params.get("org") + + if user_id: + queryset = queryset.filter(created_by_id=user_id) + if username: + queryset = queryset.filter(created_by__username=username) + if org_name: + queryset = queryset.filter(created_by_org=org_name) + + return queryset + + def perform_create(self, serializer): + user = self.request.user + collection = serializer.save() + assign_perm('api.approve_collection_items', user, collection) + + def retrieve(self, request, *args, **kwargs): + instance = self.get_object() + if request.user.has_perm("view", instance): + serializer = self.get_serializer(instance) + return Response(serializer.data) + return Response(serializer.data) + + +class ItemViewSet(ModelViewSet): + """ + # `ItemViewSet` + """ + + queryset = Item.objects.all() + serializer_class = ItemSerializer + + def get_queryset(self): + queryset = self.queryset + user = self.request.user + collection = self.request.data.get('collection') + if collection: + queryset = queryset.filter(collection_id=collection) + return queryset.filter(Q(status='approved') | Q(created_by=user.id) | Q(collection__created_by=user.id)) + + def perform_create(self, serializer): + user = self.request.user + collection = Collection.objects.get(self.request.data.get("collection")) + if not user.has_perm('collection.AddItem', collection): + return HttpResponse('Unauthorized', status=401) + serializer.save() + + def retrieve(self, request, *args, **kwargs): + instance = self.get_object() + if request.user.has_perm("view", instance): + serializer = self.get_serializer(instance) + return Response(serializer.data) + return HttpResponse('Not Found', status=404) + + +class DocumentViewSet(ModelViewSet): + """ + # `ItemViewSet` + """ + + queryset = Document.objects.all() + serializer_class = DocumentSerializer + + def get_queryset(self): + + queryset = self.queryset + user = self.request.user + collection_id = self.request.data.get('collection') + item_id = self.request.data.get('item') + type = self.request.data.get('type') + schema = self.request.data.get("schema") + role = self.request.data.get("role") + + if collection_id: + queryset = queryset.filter(collection_id=collection_id) + if item_id: + queryset = queryset.filter(item_id=item_id) + if type: + queryset = queryset.filter(item__type=type) + if schema: + queryset = queryset.filter(schema__item__title=schema) + if role: + queryset - queryset.filter(memberships__role=role) + + return queryset + + def perform_create(self, serializer): + + user = self.request.user + doc = serializer.validated_data + + for collection in doc.collections: + if not user.has_perm('collection.ViewCollection', collection): + return HttpResponse('Collection Not Found', status=404) + if not user.has_perm('collection.AddItem', collection): + return HttpResponse('Unauthorized', status=401) + + serializer.save() + + def retrieve(self, request, *args, **kwargs): + + instance = self.get_object() + + if not request.user.has_perm("view", instance): + return HttpResponse('Document Not Found', status=404) + + serializer = self.get_serializer(instance) + return Response(serializer.data) + + +class CollectionMembershipViewSet(ModelViewSet): + """ + # `ItemViewSet` + """ + + queryset = CollectionMembership.objects.all() + serializer_class = CollectionMembershipSerializer + + def get_queryset(self): + + queryset = self.queryset + user = self.request.user + collection_id = self.request.data.get('collection') + item_id = self.request.data.get('item') + type = self.request.data.get('type') + schema = self.request.data.get("schema") + role = self.request.data.get("role") + + if collection_id: + queryset = queryset.filter(collection_id=collection_id) + if item_id: + queryset = queryset.filter(item_id=item_id) + if type: + queryset = queryset.filter(item__type=type) + if schema: + queryset = queryset.filter(item__schema__item__title=schema) + if role: + queryset - queryset.filter(role=role) + + def perform_create(self, serializer): + + user = self.request.user + _membership = serializer.validated_data + + if not user.has_perm("collection.AddItem", _memberhip.collection): + return HttpResponse('Unauthorized', status=401) + + if not user.has_perm("collection.ViewItem", _membership.item): + return HttpResponse('Item Not Found', status=404) + + if not user.has_perm("collection.ViewDocument", _membership.item): + return HttpResponse('Document Not Found', status=404) + + membership = serializer.save() + + assign_perm('api.EditMembership', user, membership) + + def retrieve(self, request, *args, **kwargs): + + instance = self.get_object() + if request.user.has_perm("view", instance): + serializer = self.get_serializer(instance) + return Response(serializer.data) + return HttpResponse('Not Found', status=404) + + +class SchemaViewSet(ModelViewSet): + """ + # `SchemaViewSet` + """ + + queryset = Schema.objects.all() + serializer_class = SchemaSerializer + + def get_queryset(self): + queryset = self.queryset + return queryset + + def retrieve(self, request, *args, **kwargs): + + instance = self.get_object() + if request.user.has_perm("view", instance): + serializer = self.get_serializer(instance) + return Response(serializer.data) + return HttpResponse('Not Found', status=404) + + +# Search Views +# ############################################################################# + + +class CollectionSearchView(HaystackViewSet): + index_models = [Collection] + serializer_class = CollectionSearchSerializer + + +class ItemSearchView(HaystackViewSet): + index_models = [Item] + serializer_class = ItemSearchSerializer + +# Put in `service.views` +class LargeResultsSetPagination(pagination.PageNumberPagination): + page_size = 100 + page_size_query_param = 'page_size' + max_page_size = 1000 + + +# EOF +# ############################################################################# diff --git a/rmig.sh b/rmig.sh new file mode 100644 index 0000000..0d03401 --- /dev/null +++ b/rmig.sh @@ -0,0 +1,3 @@ +find . -path "*/migrations/*.py" -not -name "__init__.py" -delete +find . -path "*/migrations/*.pyc" -delete +./manage.py makemigrations diff --git a/service/settings/base.py b/service/settings/base.py index 2803b88..b2c5173 100644 --- a/service/settings/base.py +++ b/service/settings/base.py @@ -24,6 +24,8 @@ INSTALLED_APPS = [ 'api.apps.ApiConfig', + 'user.apps.UserConfig', + "collection.apps.CollectionConfig", 'workflow.apps.WorkflowConfig', 'django.contrib.admin', 'django.contrib.auth', @@ -174,7 +176,7 @@ 'dev-labs-2.cos.io' ) -AUTH_USER_MODEL = 'api.User' +AUTH_USER_MODEL = 'user.User' FIXTURE_DIRS = ( '/api/fixtures/', diff --git a/user/__init__.py b/user/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/user/admin.py b/user/admin.py new file mode 100644 index 0000000..e35fbd6 --- /dev/null +++ b/user/admin.py @@ -0,0 +1,27 @@ +from django.conf import settings +from django import forms +from django.contrib import admin +from django.contrib.auth import get_user_model +from django.contrib.auth.admin import UserAdmin +from django.contrib.admin.helpers import ActionForm + +from user.models import User + +from collection.admin import ( + AdminForm, + add_admins +) + + + +class CollectionUserAdmin(UserAdmin): + model = get_user_model() + action_form = AdminForm + actions = [add_admins] + + +admin.site.register(User, CollectionUserAdmin) + + +# EOF +# ############################################################################# diff --git a/user/apps.py b/user/apps.py new file mode 100644 index 0000000..f409b1b --- /dev/null +++ b/user/apps.py @@ -0,0 +1,7 @@ +from __future__ import unicode_literals + +from django.apps import AppConfig + + +class UserConfig(AppConfig): + name = 'user' diff --git a/user/init.py b/user/init.py new file mode 100644 index 0000000..e69de29 diff --git a/user/migrations/0001_initial.py b/user/migrations/0001_initial.py new file mode 100644 index 0000000..9a55b65 --- /dev/null +++ b/user/migrations/0001_initial.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-10-15 22:32 +from __future__ import unicode_literals + +import collection.mixins +import django.contrib.auth.models +import django.contrib.auth.validators +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0008_alter_user_username_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='User', + fields=[ + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=30, verbose_name='last name')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('id', models.AutoField(primary_key=True, serialize=False)), + ('gravatar', models.URLField(blank=True)), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), + ], + options={ + 'verbose_name': 'user', + 'verbose_name_plural': 'users', + 'abstract': False, + }, + bases=(models.Model, collection.mixins.CollectionUserMixin), + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + ] diff --git a/user/migrations/__init__.py b/user/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/user/models.py b/user/models.py new file mode 100644 index 0000000..ca62cb9 --- /dev/null +++ b/user/models.py @@ -0,0 +1,37 @@ +""" +Auth Models +""" + + +# Library Imports +# ############################################################################# + + +from django.db.models import ( + AutoField, + URLField +) +from django.contrib.auth.models import AbstractUser + + +# App Imports +# ############################################################################# + + +from collection.mixins import CollectionUserMixin + + +# Models +# ############################################################################# + + +class User(AbstractUser, CollectionUserMixin): + id = AutoField(primary_key=True) + gravatar = URLField(blank=True) + + @property + def full_name(self): + return self.first_name + " " + self.last_name + +# EOF +# ############################################################################# diff --git a/user/routers.py b/user/routers.py new file mode 100644 index 0000000..546aee3 --- /dev/null +++ b/user/routers.py @@ -0,0 +1,37 @@ +""" +# Collection Router +""" + + +# Library Imports +# ############################################################################# + + +from django.conf.urls import url, include +from rest_framework.urlpatterns import format_suffix_patterns +from rest_framework_nested import routers +from collections import namedtuple + + +# App Imports +# ############################################################################# + + +from user.views import ( + UserViewSet, + UserSearchView, +) + + +# Router Setup +# ############################################################################# + + +user_router = routers.DefaultRouter(trailing_slash=False) + +user_router.register(r'users', UserViewSet) +user_router.register("users/search", UserSearchView, base_name="user-search") + + +# EOF +# ############################################################################# diff --git a/user/search_indexes.py b/user/search_indexes.py new file mode 100644 index 0000000..04c4df1 --- /dev/null +++ b/user/search_indexes.py @@ -0,0 +1,18 @@ +from haystack import indexes +from django.contrib.auth.models import User + + +class UserIndex(indexes.SearchIndex, indexes.Indexable): + text = indexes.CharField(document=True, use_template=True) + first_name = indexes.CharField(model_attr="first_name") + last_name = indexes.CharField(model_attr="last_name") + + def get_model(self): + return User + + def index_queryset(self, using=None): + """ + Used when the entire index for model is updated + """ + return self.get_model().objects.all() + diff --git a/user/serializers.py b/user/serializers.py new file mode 100644 index 0000000..d8e8c49 --- /dev/null +++ b/user/serializers.py @@ -0,0 +1,68 @@ +from django.utils import timezone +from rest_framework import exceptions +from rest_framework_json_api import serializers +from guardian.shortcuts import assign_perm +from allauth.socialaccount.models import SocialAccount, SocialToken +from django.core.exceptions import ObjectDoesNotExist +from drf_haystack.serializers import HaystackSerializer +from rest_framework_json_api.relations import ResourceRelatedField + +from user import search_indexes +from user.models import User +from collection.models import Collection +from workflow.models import Workflow + +class UserSerializer(serializers.ModelSerializer): + token = serializers.SerializerMethodField() + + collection = ResourceRelatedField( + queryset=Collection.objects.all(), + many=False, + required=False + ) + + class Meta: + model = User + fields = [ + 'id', + 'username', + 'first_name', + 'last_name', + 'email', + 'date_joined', + 'last_login', + 'is_active', + 'gravatar', + 'token', + "collection" + ] + + class JSONAPIMeta: + resource_name = 'users' + + def get_token(self, obj): + if not obj.id: + return None + try: + account = SocialAccount.objects.get(user=obj) + token = SocialToken.objects.get(account=account).token + except ObjectDoesNotExist: + return None + return token + + +class UserSearchSerializer(HaystackSerializer): + + class Meta: + index_classes = [search_indexes.UserIndex] + fields = [ + 'text', + 'first_name', + 'last_name', + 'email', + 'full_name' + ] + + +# EOF +# ############################################################################# diff --git a/user/views.py b/user/views.py new file mode 100644 index 0000000..7139f07 --- /dev/null +++ b/user/views.py @@ -0,0 +1,66 @@ +""" +# Service Views + +- CurrentUser +- UserDetail +- UserList +- UserSearchView +""" + + +# Library Imports +# ############################################################################# + + +from rest_framework import generics +from rest_framework_json_api import pagination +from rest_framework import exceptions as drf_exceptions +from rest_framework.viewsets import ModelViewSet +from drf_haystack.viewsets import HaystackViewSet + +from user.models import User +from user.serializers import ( + UserSerializer, + UserSearchSerializer +) + + +# Model Views +# ############################################################################# + + +class UserViewSet(ModelViewSet): + + queryset = User.objects.all() + serializer_class = UserSerializer + + def get_queryset(self): + return self.queryset + + def get_object(self): + pk = self.kwargs.get("pk") + if pk == "me": + return self.request.user + try: + return User.objects.get(id=pk) + except: + raise drf_exceptions.NotFound + + +# Search Views +# ############################################################################# + + +class UserSearchView(HaystackViewSet): + index_models = [User] + serializer_class = UserSearchSerializer + + +class LargeResultsSetPagination(pagination.PageNumberPagination): + page_size = 100 + page_size_query_param = 'page_size' + max_page_size = 1000 + + +# EOF +# ############################################################################# diff --git a/workflow/migrations/0001_initial.py b/workflow/migrations/0001_initial.py index dadf666..f39fb04 100644 --- a/workflow/migrations/0001_initial.py +++ b/workflow/migrations/0001_initial.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.10.5 on 2017-10-02 22:48 +# Generated by Django 1.10.5 on 2017-10-15 22:32 from __future__ import unicode_literals import django.contrib.postgres.fields.jsonb @@ -12,7 +12,7 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('api', '0001_initial'), + ('collection', '0001_initial'), ] operations = [ @@ -20,7 +20,7 @@ class Migration(migrations.Migration): name='Case', fields=[ ('id', models.AutoField(primary_key=True, serialize=False)), - ('collection', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='collection', to='api.Collection')), + ('collection', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='collection', to='collection.Collection')), ], ), migrations.CreateModel( @@ -43,7 +43,7 @@ class Migration(migrations.Migration): ('id', models.AutoField(primary_key=True, serialize=False)), ('name', models.CharField(max_length=64)), ('value', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=None, null=True)), - ('properties', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default={})), + ('properties', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default={}, null=True)), ('cases', models.ManyToManyField(blank=True, related_name='parameters', through='workflow.CaseParameter', to='workflow.Case')), ], ), diff --git a/workflow/migrations/0002_auto_20171003_0047.py b/workflow/migrations/0002_auto_20171003_0047.py deleted file mode 100644 index abf472e..0000000 --- a/workflow/migrations/0002_auto_20171003_0047.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.10.5 on 2017-10-03 00:47 -from __future__ import unicode_literals - -import django.contrib.postgres.fields.jsonb -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('workflow', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='parameter', - name='properties', - field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default={}, null=True), - ), - ] diff --git a/workflow/models.py b/workflow/models.py index 133da0c..9a10e12 100644 --- a/workflow/models.py +++ b/workflow/models.py @@ -5,7 +5,7 @@ from django.db import models from django.contrib.auth.models import AbstractUser from django.contrib.postgres.fields import JSONField - +import base64 class Workflow(models.Model): """Workflow Model""" @@ -165,7 +165,9 @@ class Parameter(models.Model): id = models.AutoField(primary_key=True) name = models.CharField(max_length=64, blank=False) + value = JSONField(null=True, blank=True, default=None) + properties = JSONField(null=True, blank=True, default={}) stub = models.ForeignKey( @@ -255,7 +257,7 @@ class Case(models.Model): ) collection = models.ForeignKey( - 'api.Collection', + 'collection.Collection', related_name='collection', null=True ) diff --git a/workflow/schemas/create-proposal.json b/workflow/schemas/create-proposal.json new file mode 100644 index 0000000..912e573 --- /dev/null +++ b/workflow/schemas/create-proposal.json @@ -0,0 +1,51 @@ +{ + "title": "Make A Proposal", + "description": "Make a proposal for a research project.", + "initialParameters": { + "event-approval-choices": { + "value": [ + { + "label": "Approve", + "parameter": "event-approved" + }, + { + "label": "Deny", + "parameter": "event-denied" + } + ] + } + }, + "sections": [ + { + "label": "Proposal Information", + "Description": "Describe the research idea", + "widgets": [ + { + "label": "Topic", + "description": "What area of research does this proposal apply to?", + "widgetType": "text-field", + "parameters": { + "value": "topic" + } + }, + { + "label": "Premise", + "description": "Describe the basic premise behind the idea.", + "widgetType": "text-area", + "parameters": { + "value": "premise" + } + }, + { + "label": "Submit", + "description": "Submit this proposal", + "widgetType": "proposal-submit", + "parameters": { + "premis": "premise", + "topic": "topic" + } + } + ] + } + ] +} diff --git a/workflow/schemas/meeting-approval.json b/workflow/schemas/meeting-approval.json new file mode 100644 index 0000000..875e001 --- /dev/null +++ b/workflow/schemas/meeting-approval.json @@ -0,0 +1,57 @@ +{ + "title": "Meeting Approval Form", + "description": "The meeting approval form allows admins to approve the submissions that are made to meetings.", + "initialParameters": { + "event-approval-choices": { + "value": [ + { + "label": "Approve", + "parameter": "event-approved" + }, + { + "label": "Deny", + "parameter": "event-denied" + } + ] + } + }, + "sections": [ + { + "Label": "Event Information", + "Description": "Basic information about this meeting event", + "widgets": [ + { + "label": "Event Type", + "description": "Select the type of event that is being submitted. Events may be \"talk\" or \"poster\"", + "widgetType": "item-display", + "parameters": { + "value": "item" + } + } + ] + }, + { + "label": "Resolution", + "description": "Approve or Deny submission of this talk to the meeting", + "widgets": [ + { + "label": "Approve or Deny", + "description": "Select the resolution \"Approve\" or \"Deny\"", + "widgetType": "choice-widget", + "parameters": { + "choices": "event-approval-choices" + } + }, + { + "label": "Submit", + "description": "Submit this talk", + "widgetType": "approval-submit", + "parameters": { + "approve": "event-approved", + "deny": "event-denied" + } + } + ] + } + ] +} diff --git a/workflow/schemas/meeting.json b/workflow/schemas/meeting.json index ae3d7f7..0e1901d 100644 --- a/workflow/schemas/meeting.json +++ b/workflow/schemas/meeting.json @@ -25,7 +25,7 @@ "description": "Select the type of event that is being submitted. Events may be \"talk\" or \"poster\"", "widgetType": "text-field", "parameters": { - "value": "event-type" + "value": "event-category" } }, { @@ -105,7 +105,8 @@ "fileData": "file-data", "fileName": "file-name", "metadata": "metadata", - "node": "node" + "node": "node", + "category": "event-category" } } ] diff --git a/workflow/schemas/preprint-submission.json b/workflow/schemas/preprint-submission.json new file mode 100644 index 0000000..dc8c21f --- /dev/null +++ b/workflow/schemas/preprint-submission.json @@ -0,0 +1,164 @@ +{ + "title": "Preprint Submission Form", + "description": "The meeting submission form allows users to submit information about their talks", + "initialParameters": { + "event-creation-choices": { + "value": [ + { + "label": "Use an existing node", + "parameter": "use-existing-node" + }, + { + "label": "Create a new node", + "parameter": "create-new-node" + } + ] + } + }, + "sections": [ + { + "Label": "Basic Information", + "Description": "Add basic information about this preprint", + "widgets": [ + { + "label": "Preprint Title", + "description": "Ad the title of the preprint", + "widgetType": "text-field", + "parameters": { + "value": "title" + } + }, + { + "label": "Abstract", + "description": "Enter the abstrac of the preprint", + "widgetType": "text-area", + "parameters": { + "value": "abstract" + } + }, + { + "label": "DOI", + "description": "If this preprint has a DOI, enter it.", + "widgetType": "text-field", + "parameters": { + "value": "doi" + } + } + ] + }, + { + "label": "Authors", + "description": "Add the names of the speakers for this talk", + "widgets": [ + { + "label": "Speakers", + "description": "Select the names of the people that will be spealing at the talk or are authors of the poster", + "widgetType": "osf-user-picker", + "parameters": { + "users": "authors" + } + } + ] + }, + { + "label": "Preprint Document", + "description": "Upload the preprint document", + "widgets": [ + { + "label": "Use an existing node", + "description": "Any materials related to this talk are stored in an OSF Project. Files are automatically uploaded to a new project. If a project containing the materials that are related to this talk already exists, that project may be selected instead of creating a new one. Click here to learn more about OSF projects. (This link will open in a new window.)", + "widgetType": "choice-picker", + "parameters": { + "choices": "event-creation-choices" + } + }, + { + "label": "Node Create", + "description": "Creates the node when its parameter 'enable' is set to true", + "widgetType": "node-creator", + "parameters": { + "enable": "create-new-node", + "node": "node" + } + }, + { + "label": "Upload Materials", + "description": "Select the file to upload", + "widgetType": "file-uploader", + "parameters": { + "fileData": "file-data", + "fileName": "file-name" + } + } + ] + }, + { + "label": "Disciplines", + "description": "Select the disciplines that are related to the preprint.", + "widgets": [ + { + "label": "Disciplines", + "description": "Select the relevant disciplines. To remove a discipline, press the 'x' on the discipline plaquard.", + "widgetType": "subject-picker", + "parameters": { + "subjects": "disciplines" + } + } + ] + }, + { + "label": "License", + "description": "Set how the preprint will be licensed", + "widgets": [ + { + "label": "License Type", + "description": "Choose the license this preprint should be available under", + "widgetType": "text-field", + "parameters": { + "value": "license-type" + } + }, + { + "label": "Copyright Year", + "description": "Select the year of copyright", + "widgetType": "text-field", + "parameters": { + "value": "copyright-year" + } + }, + { + "label": "Copyright Holders", + "description": "Enter the copyright holders for the preprint's license", + "widgetType": "text-field", + "parameters": { + "value": "copyright-holders" + } + } + ] + }, + { + "label": "Submit", + "description": "Submit this talk to the meeting", + "widgets": [ + { + "label": "Submit", + "description": "Submit this talk", + "widgetType": "preprint-submit", + "parameters": { + "title": "title-widget", + "abstract": "abstract", + "doi": "doi", + "fileData": "file-data", + "fileName": "file-name", + "disciplines": "disciplines", + "node": "node", + "licenseType": "license-type", + "copyrightYear": "copyright-year", + "copyrightHolders": "copyright-holders", + "authors": "authors" + } + } + ] + } + ] +} diff --git a/workflow/serializers.py b/workflow/serializers.py index cc486bf..a36e284 100644 --- a/workflow/serializers.py +++ b/workflow/serializers.py @@ -8,6 +8,7 @@ from django.contrib.auth.models import User, Group from workflow import models +from collection import models as collection_models class Workflow(ModelSerializer): @@ -269,7 +270,8 @@ def create(self, validated_data): through_instance.save() else: - set_many(instance, field_name, value) + field = getattr(instance, attr) + field.set(value) return instance def update(self, instance, validated_data): @@ -303,8 +305,8 @@ def update(self, instance, validated_data): through_instance.save() elif attr in info.relations and info.relations[attr].to_many: - set_many(instance, attr, value) - + field = getattr(instance, attr) + field.set(value) else: setattr(instance, attr, value) @@ -379,6 +381,13 @@ class Case(ModelSerializer): required=False ) + collection = ResourceRelatedField( + queryset=collection_models.Collection.objects.all(), + many=False, + required=False, + allow_null=False + ) + class Meta: resource_name = 'cases' model = models.Case @@ -389,6 +398,7 @@ class Meta: 'widgets', 'parameter_aliases', 'parameters', + 'collection', 'stubs' ] diff --git a/workflow/views.py b/workflow/views.py index 8ec2091..7e8ec55 100644 --- a/workflow/views.py +++ b/workflow/views.py @@ -1,4 +1,13 @@ -from rest_framework import viewsets +""" +Workflow Views +""" + + +# Imports +# ############################################################################# + + +from rest_framework.viewsets import ModelViewSet from rest_framework import status from rest_framework.response import Response from rest_framework.renderers import JSONRenderer @@ -8,7 +17,11 @@ from workflow import serializers -class Workflow(viewsets.ModelViewSet): +# ViewSets +# ############################################################################# + + +class Workflow(ModelViewSet): queryset = models.Workflow.objects.all() serializer_class = serializers.Workflow @@ -20,7 +33,7 @@ def get_queryset(self): return self.queryset -class Section(viewsets.ModelViewSet): +class Section(ModelViewSet): queryset = models.Section.objects.all() serializer_class = serializers.Section @@ -29,7 +42,7 @@ def get_queryset(self): return self.queryset -class Widget(viewsets.ModelViewSet): +class Widget(ModelViewSet): queryset = models.Widget.objects.all() serializer_class = serializers.Widget @@ -38,7 +51,7 @@ def get_queryset(self): return self.queryset -class ParameterAlias(viewsets.ModelViewSet): +class ParameterAlias(ModelViewSet): queryset = models.ParameterAlias.objects.all() serializer_class = serializers.ParameterAlias @@ -47,13 +60,13 @@ def get_queryset(self): return self.queryset -class ParameterStub(viewsets.ModelViewSet): +class ParameterStub(ModelViewSet): queryset = models.ParameterStub.objects.all() serializer_class = serializers.ParameterStub -class Parameter(viewsets.ModelViewSet): +class Parameter(ModelViewSet): queryset = models.Parameter.objects.all() serializer_class = serializers.Parameter @@ -75,7 +88,7 @@ def get_queryset(self): return queryset -class Case(viewsets.ModelViewSet): +class Case(ModelViewSet): queryset = models.Case.objects.all() serializer_class = serializers.Case @@ -83,12 +96,14 @@ def get_queryset(self): queryset = self.queryset collection_id = self.request.query_params.get('collection') if collection_id: - queryset = queryset.filter(workflow__collections=collection_id).order_by('-id') + queryset = queryset.filter(collection=collection_id).order_by('-id') return queryset # This logic belongs in workflow.models maybe? def perform_create(self, serializer): case = serializer.save() + if not case.collection: + case.collection = self.request.query_params.get('collection') for stub in case.workflow.parameter_stubs.all(): case_stub = models.CaseStub() case_stub.case = case @@ -119,3 +134,7 @@ def perform_create(self, serializer): for parameter_alias in case.workflow.parameter_aliases.all(): parameter_alias.cases.add(case) parameter_alias.save() + + +# EOF +# #############################################################################