Add answers to surveys

This commit is contained in:
Ramon Wenger 2019-06-27 17:56:29 +02:00
parent f9642ff49e
commit 9b85560795
10 changed files with 355 additions and 2 deletions

View File

@ -0,0 +1,20 @@
mutation UpdateAnswer($input:UpdateAnswerInput!) {
updateAnswer(input:$input){
answer {
id
data
}
}
}
# input
#{
# "input": {
# "answer": {
# "surveyId": "U3VydmV5Tm9kZTox",
# "data": "{\"some\": \"json\"}"
# },
# }
#}

161
client/src/survey.config.js Normal file
View File

@ -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'
}
}
};

View File

@ -15,6 +15,8 @@ from objectives.mutations import ObjectiveMutations
from objectives.schema import ObjectivesQuery from objectives.schema import ObjectivesQuery
from portfolio.mutations import PortfolioMutations from portfolio.mutations import PortfolioMutations
from portfolio.schema import PortfolioQuery from portfolio.schema import PortfolioQuery
from surveys.schema import SurveysQuery
from surveys.mutations import SurveysMutations
from rooms.mutations import RoomMutations from rooms.mutations import RoomMutations
from rooms.schema import RoomsQuery from rooms.schema import RoomsQuery
from users.schema import UsersQuery from users.schema import UsersQuery
@ -22,7 +24,7 @@ from users.mutations import ProfileMutations
class Query(UsersQuery, RoomsQuery, ObjectivesQuery, BookQuery, AssignmentsQuery, StudentSubmissionQuery, class Query(UsersQuery, RoomsQuery, ObjectivesQuery, BookQuery, AssignmentsQuery, StudentSubmissionQuery,
BasicKnowledgeQuery, PortfolioQuery, MyActivityQuery, graphene.ObjectType): BasicKnowledgeQuery, PortfolioQuery, MyActivityQuery, SurveysQuery, graphene.ObjectType):
node = relay.Node.Field() node = relay.Node.Field()
if settings.DEBUG: if settings.DEBUG:
@ -30,7 +32,7 @@ class Query(UsersQuery, RoomsQuery, ObjectivesQuery, BookQuery, AssignmentsQuery
class Mutation(BookMutations, RoomMutations, AssignmentMutations, ObjectiveMutations, CoreMutations, PortfolioMutations, class Mutation(BookMutations, RoomMutations, AssignmentMutations, ObjectiveMutations, CoreMutations, PortfolioMutations,
ProfileMutations, graphene.ObjectType): ProfileMutations, SurveysMutations, graphene.ObjectType):
if settings.DEBUG: if settings.DEBUG:
debug = graphene.Field(DjangoDebug, name='__debug') debug = graphene.Field(DjangoDebug, name='__debug')

View File

@ -56,6 +56,12 @@ def get_graphql_mutation(filename):
return mutation 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): def get_by_id_or_slug(model, **kwargs):
slug = kwargs.get('slug') slug = kwargs.get('slug')
id = kwargs.get('id') id = kwargs.get('id')

View File

@ -1,3 +1,40 @@
import json
import logging
from django.contrib import admin 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. # 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

6
server/surveys/inputs.py Normal file
View File

@ -0,0 +1,6 @@
import graphene
from graphene import InputObjectType
class UpdateAnswerArgument(InputObjectType):
survey_id = graphene.ID(required=True)
data = graphene.String(required=True)

View File

@ -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')),
],
),
]

View File

@ -1,3 +1,4 @@
from django.contrib.auth import get_user_model
from django.db import models from django.db import models
from django.contrib.postgres.fields import JSONField from django.contrib.postgres.fields import JSONField
@ -8,3 +9,11 @@ class Survey(models.Model):
def __str__(self): def __str__(self):
return self.title 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)

View File

@ -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()

49
server/surveys/schema.py Normal file
View File

@ -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)