diff --git a/client/src/graphql/gql/mutations/updateAnswer.gql b/client/src/graphql/gql/mutations/updateAnswer.gql new file mode 100644 index 00000000..a080a191 --- /dev/null +++ b/client/src/graphql/gql/mutations/updateAnswer.gql @@ -0,0 +1,20 @@ +mutation UpdateAnswer($input:UpdateAnswerInput!) { + updateAnswer(input:$input){ + answer { + id + data + } + } +} + + +# input + +#{ +# "input": { +# "answer": { +# "surveyId": "U3VydmV5Tm9kZTox", +# "data": "{\"some\": \"json\"}" +# }, +# } +#} diff --git a/client/src/survey.config.js b/client/src/survey.config.js new file mode 100644 index 00000000..a6b2213b --- /dev/null +++ b/client/src/survey.config.js @@ -0,0 +1,161 @@ +export const css = { + 'root': 'survey', + 'header': '', + 'body': '', + 'bodyEmpty': '', + 'footer': '', + 'navigationButton': 'button button--primary', + 'completedPage': '', + 'navigation': { + 'complete': 'button button--primary', + 'prev': 'button button--primary', + 'next': 'button button--primary', + 'start': 'button button--primary' + }, + 'progress': 'progress center-block mx-auto mb-4', + 'progressBar': 'progress-bar', + 'page': { + 'root': '', + 'title': '', + 'survey__page-description': '' + }, + 'pageTitle': '', + 'pageDescription': 'small', + 'row': 'sv_row', + 'question': { + 'mainRoot': 'survey__question question', + 'flowRoot': 'sv_q_flow sv_qstn', + 'titleLeftRoot': 'sv_qstn_left', + 'title': 'survey__question-title', + 'number': 'sv_q_num', + 'survey__question-description': 'small', + 'comment': 'survey__question-input skillbox-input', + 'required': '', + 'titleRequired': '', + 'hasError': 'has-error', + 'indent': 20 + }, + 'panel': { + 'title': 'survey__panel-title', + 'description': 'small survey__panel-description', + 'container': 'sv_p_container' + }, + 'error': { + 'root': 'alert alert-danger', + 'icon': 'glyphicon glyphicon-exclamation-sign', + 'item': '', + 'locationTop': 'sv_qstn_error_top', + 'locationBottom': 'sv_qstn_error_bottom' + }, + 'boolean': { + 'root': 'sv_qbln form-inline checkbox', + 'item': '', + 'label': '', + 'materialDecorator': 'checkbox-material' + }, + 'checkbox': { + 'root': 'sv_qcbc sv_qcbx form-inline', + 'item': 'checkbox', + 'itemControl': '', + 'controlLabel': '', + 'materialDecorator': 'checkbox-material', + 'other': 'sv_q_checkbox_other skillbox-input', + 'column': 'sv_q_select_column' + }, + 'comment': 'survey__input skillbox-input', + 'dropdown': { + 'root': '', + 'control': 'skillbox-input', + 'other': 'sv_q_dd_other skillbox-input' + }, + 'html': { + 'root': '' + }, + 'matrix': { + 'root': 'table table-striped', + 'label': 'sv_q_m_label', + 'cellText': 'sv_q_m_cell_text', + 'cellTextSelected': 'sv_q_m_cell_selected bg-primary', + 'cellLabel': 'sv_q_m_cell_label' + }, + 'matrixdropdown': { + 'root': 'table' + }, + 'matrixdynamic': { + 'root': 'table', + 'button': 'button', + 'buttonAdd': '', + 'buttonRemove': '', + 'iconAdd': '', + 'iconRemove': '' + }, + 'paneldynamic': { + 'root': '', + 'button': 'button', + 'buttonPrev': '', + 'buttonNext': '', + 'buttonAdd': '', + 'buttonRemove': '' + }, + 'multipletext': { + 'root': 'table', + 'itemTitle': '', + 'itemValue': 'sv_q_mt_item_value skillbox-input' + }, + 'radiogroup': { + 'root': 'sv_qcbc form-inline', + 'item': 'radio', + 'label': '', + 'itemControl': '', + 'controlLabel': '', + 'materialDecorator': 'circle', + 'other': 'sv_q_radiogroup_other skillbox-input', + 'clearButton': 'sv_q_radiogroup_clear button', + 'column': 'sv_q_select_column' + }, + 'imagepicker': { + 'root': 'sv_imgsel', + 'item': 'sv_q_imgsel', + 'label': 'sv_q_imgsel_label', + 'itemControl': 'sv_q_imgsel_control_item', + 'image': 'sv_q_imgsel_image', + 'itemText': 'sv_q_imgsel_text', + 'clearButton': 'sv_q_radiogroup_clear' + }, + 'rating': { + 'root': 'btn-group', + 'item': 'btn btn-default btn-secondary', + 'selected': 'active', + 'minText': 'sv_q_rating_min_text', + 'itemText': 'sv_q_rating_item_text', + 'maxText': 'sv_q_rating_max_text' + }, + 'text': 'survey__input skillbox-input', + 'expression': 'survey__input skillbox-input', + 'file': { + 'root': 'sv_q_file', + 'placeholderInput': 'sv_q_file_placeholder', + 'preview': 'sv_q_file_preview', + 'removeButton': 'sv_q_file_remove_button', + 'fileInput': 'sv_q_file_input', + 'removeFile': 'sv_q_file_remove' + }, + 'saveData': { + 'root': '', + 'saving': 'alert alert-info', + 'error': 'alert alert-danger', + 'success': 'alert alert-success', + 'saveAgainButton': '' + }, + 'window': { + 'root': 'modal-content', + 'body': 'modal-body', + 'header': { + 'root': 'modal-header panel-title', + 'title': 'pull-left', + 'button': 'glyphicon pull-right', + 'buttonExpanded': 'glyphicon pull-right glyphicon-chevron-up', + 'buttonCollapsed': 'glyphicon pull-right glyphicon-chevron-down' + } + } +}; diff --git a/server/api/schema.py b/server/api/schema.py index 961cdf35..9c39b4db 100644 --- a/server/api/schema.py +++ b/server/api/schema.py @@ -15,6 +15,8 @@ from objectives.mutations import ObjectiveMutations from objectives.schema import ObjectivesQuery from portfolio.mutations import PortfolioMutations from portfolio.schema import PortfolioQuery +from surveys.schema import SurveysQuery +from surveys.mutations import SurveysMutations from rooms.mutations import RoomMutations from rooms.schema import RoomsQuery from users.schema import UsersQuery @@ -22,7 +24,7 @@ from users.mutations import ProfileMutations class Query(UsersQuery, RoomsQuery, ObjectivesQuery, BookQuery, AssignmentsQuery, StudentSubmissionQuery, - BasicKnowledgeQuery, PortfolioQuery, MyActivityQuery, graphene.ObjectType): + BasicKnowledgeQuery, PortfolioQuery, MyActivityQuery, SurveysQuery, graphene.ObjectType): node = relay.Node.Field() if settings.DEBUG: @@ -30,7 +32,7 @@ class Query(UsersQuery, RoomsQuery, ObjectivesQuery, BookQuery, AssignmentsQuery class Mutation(BookMutations, RoomMutations, AssignmentMutations, ObjectiveMutations, CoreMutations, PortfolioMutations, - ProfileMutations, graphene.ObjectType): + ProfileMutations, SurveysMutations, graphene.ObjectType): if settings.DEBUG: debug = graphene.Field(DjangoDebug, name='__debug') diff --git a/server/api/utils.py b/server/api/utils.py index 6cd70ac4..a77a2dcd 100644 --- a/server/api/utils.py +++ b/server/api/utils.py @@ -56,6 +56,12 @@ def get_graphql_mutation(filename): return mutation +def get_by_id(model, **kwargs): + id = kwargs.get('id') + if id is not None: + return get_object(model, id) + return None + def get_by_id_or_slug(model, **kwargs): slug = kwargs.get('slug') id = kwargs.get('id') diff --git a/server/surveys/admin.py b/server/surveys/admin.py index 8c38f3f3..b7873b3a 100644 --- a/server/surveys/admin.py +++ b/server/surveys/admin.py @@ -1,3 +1,40 @@ +import json +import logging + from django.contrib import admin +from django.contrib.postgres.fields import JSONField +from django.forms import widgets + +logger = logging.getLogger(__name__) # Register your models here. +from surveys.models import Survey, Answer + + +class PrettyJSONWidget(widgets.Textarea): + + def format_value(self, value): + try: + value = json.dumps(json.loads(value), indent=2, sort_keys=True) + # these lines will try to adjust size of TextArea to fit to content + row_lengths = [len(r) for r in value.split('\n')] + self.attrs['rows'] = min(max(len(row_lengths) + 2, 10), 30) + self.attrs['cols'] = min(max(max(row_lengths) + 2, 40), 120) + return value + except Exception as e: + logger.warning("Error while formatting JSON: {}".format(e)) + return super(PrettyJSONWidget, self).format_value(value) + + +class JSONAdmin(admin.ModelAdmin): + formfield_overrides = { + JSONField: {'widget': PrettyJSONWidget} + } + +@admin.register(Survey) +class SurveyAdmin(JSONAdmin): + pass + +@admin.register(Answer) +class AnswerAdmin(JSONAdmin): + pass diff --git a/server/surveys/inputs.py b/server/surveys/inputs.py new file mode 100644 index 00000000..11a45b9c --- /dev/null +++ b/server/surveys/inputs.py @@ -0,0 +1,6 @@ +import graphene +from graphene import InputObjectType + +class UpdateAnswerArgument(InputObjectType): + survey_id = graphene.ID(required=True) + data = graphene.String(required=True) diff --git a/server/surveys/migrations/0002_answer.py b/server/surveys/migrations/0002_answer.py new file mode 100644 index 00000000..21bdf24b --- /dev/null +++ b/server/surveys/migrations/0002_answer.py @@ -0,0 +1,26 @@ +# Generated by Django 2.0.6 on 2019-06-27 14:35 + +from django.conf import settings +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('surveys', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Answer', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('data', django.contrib.postgres.fields.jsonb.JSONField()), + ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('survey', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='surveys.Survey')), + ], + ), + ] diff --git a/server/surveys/models.py b/server/surveys/models.py index 423da6fd..1f137f71 100644 --- a/server/surveys/models.py +++ b/server/surveys/models.py @@ -1,3 +1,4 @@ +from django.contrib.auth import get_user_model from django.db import models from django.contrib.postgres.fields import JSONField @@ -8,3 +9,11 @@ class Survey(models.Model): def __str__(self): return self.title + +class Answer(models.Model): + owner = models.ForeignKey(get_user_model(), on_delete=models.CASCADE, related_name='answers') + data = JSONField() + survey = models.ForeignKey(Survey, on_delete=models.CASCADE, related_name='answers') + + def __str__(self): + return '{} - {}'.format(self.owner.username, self.survey.title) diff --git a/server/surveys/mutations.py b/server/surveys/mutations.py new file mode 100644 index 00000000..0fb962e8 --- /dev/null +++ b/server/surveys/mutations.py @@ -0,0 +1,37 @@ +import graphene +import json +from graphene import relay + +from api.utils import get_object +from surveys.inputs import UpdateAnswerArgument +from surveys.models import Survey, Answer +from surveys.schema import AnswerNode + + +class UpdateAnswer(relay.ClientIDMutation): + class Input: + answer = graphene.Argument(UpdateAnswerArgument) + + answer = graphene.Field(AnswerNode) + + @classmethod + def mutate_and_get_payload(cls, root, info, **kwargs): + user = info.context.user + + answer = kwargs.get('answer') + survey_id = answer.get('survey_id') + data = json.loads(answer.get('data')) + survey = get_object(Survey, survey_id) + + try: + answer = survey.answers.get(owner=user) + answer.data = data + answer.save() + except Answer.DoesNotExist: + answer = Answer.objects.create(owner=user, survey=survey, data=data) + + return cls(answer=answer) + + +class SurveysMutations: + update_answer = UpdateAnswer.Field() diff --git a/server/surveys/schema.py b/server/surveys/schema.py new file mode 100644 index 00000000..701e76cd --- /dev/null +++ b/server/surveys/schema.py @@ -0,0 +1,49 @@ +import graphene +from graphene import relay +from graphene_django import DjangoObjectType +from graphene_django.filter import DjangoFilterConnectionField + +from api.utils import get_by_id +from surveys.models import Answer +from .models import Survey + + +class AnswerNode(DjangoObjectType): + pk = graphene.Int() + + class Meta: + model = Answer + interfaces = (relay.Node,) + + def resolve_pk(self, *args, **kwargs): + return self.id + + +class SurveyNode(DjangoObjectType): + pk = graphene.Int() + answer = graphene.Field(AnswerNode) + + class Meta: + model = Survey + filter_fields = [] + interfaces = (relay.Node,) + + def resolve_pk(self, *args, **kwargs): + return self.id + + def resolve_answer(self, info, **kwargs): + user = info.context.user + try: + return Answer.objects.get(owner=user, survey=self) + except Answer.DoesNotExist: + return None + +class SurveysQuery(object): + survey = graphene.Field(SurveyNode, id=graphene.ID()) + surveys = DjangoFilterConnectionField(SurveyNode) + + def resolve_surveys(self, info, **kwargs): + return Survey.objects.all() + + def resolve_survey(self, info, **kwargs): + return get_by_id(Survey, **kwargs)