Move code to service, add admin action
This commit is contained in:
parent
603c0544c2
commit
798c159ee7
|
|
@ -10,9 +10,6 @@ from django.views import defaults as default_views
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from django_ratelimit.exceptions import Ratelimited
|
from django_ratelimit.exceptions import Ratelimited
|
||||||
from graphene_django.views import GraphQLView
|
from graphene_django.views import GraphQLView
|
||||||
from wagtail import urls as wagtail_urls
|
|
||||||
from wagtail.admin import urls as wagtailadmin_urls
|
|
||||||
from wagtail.documents import urls as media_library_urls
|
|
||||||
|
|
||||||
from vbv_lernwelt.api.directory import list_entities
|
from vbv_lernwelt.api.directory import list_entities
|
||||||
from vbv_lernwelt.api.user import (
|
from vbv_lernwelt.api.user import (
|
||||||
|
|
@ -55,7 +52,6 @@ from vbv_lernwelt.edoniq_test.views import (
|
||||||
from vbv_lernwelt.feedback.views import (
|
from vbv_lernwelt.feedback.views import (
|
||||||
get_expert_feedbacks_for_course,
|
get_expert_feedbacks_for_course,
|
||||||
get_feedback_for_circle,
|
get_feedback_for_circle,
|
||||||
export_feedback_for_course_session,
|
|
||||||
)
|
)
|
||||||
from vbv_lernwelt.files.views import presign
|
from vbv_lernwelt.files.views import presign
|
||||||
from vbv_lernwelt.importer.views import (
|
from vbv_lernwelt.importer.views import (
|
||||||
|
|
@ -65,6 +61,9 @@ from vbv_lernwelt.importer.views import (
|
||||||
)
|
)
|
||||||
from vbv_lernwelt.media_files.views import user_image
|
from vbv_lernwelt.media_files.views import user_image
|
||||||
from vbv_lernwelt.notify.views import email_notification_settings
|
from vbv_lernwelt.notify.views import email_notification_settings
|
||||||
|
from wagtail import urls as wagtail_urls
|
||||||
|
from wagtail.admin import urls as wagtailadmin_urls
|
||||||
|
from wagtail.documents import urls as media_library_urls
|
||||||
|
|
||||||
|
|
||||||
class SignedIntConverter(IntConverter):
|
class SignedIntConverter(IntConverter):
|
||||||
|
|
@ -172,9 +171,6 @@ urlpatterns = [
|
||||||
name='storage_presign'),
|
name='storage_presign'),
|
||||||
|
|
||||||
# feedback
|
# feedback
|
||||||
path(r'api/core/feedback/export/<str:course_session_id>/',
|
|
||||||
export_feedback_for_course_session,
|
|
||||||
name='export_feedback_for_course_session'),
|
|
||||||
path(r'api/core/feedback/<str:course_session_id>/summary/',
|
path(r'api/core/feedback/<str:course_session_id>/summary/',
|
||||||
get_expert_feedbacks_for_course,
|
get_expert_feedbacks_for_course,
|
||||||
name='feedback_summary'),
|
name='feedback_summary'),
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from vbv_lernwelt.course.models import Course, CourseSession, CourseSessionUser
|
from vbv_lernwelt.course.models import Course, CourseSession, CourseSessionUser
|
||||||
|
from vbv_lernwelt.feedback.services import get_feedbacks_for_course
|
||||||
from vbv_lernwelt.learnpath.models import Circle
|
from vbv_lernwelt.learnpath.models import Circle
|
||||||
|
|
||||||
|
get_feedbacks_for_course.short_description = "Feedback export"
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Course)
|
@admin.register(Course)
|
||||||
class CourseAdmin(admin.ModelAdmin):
|
class CourseAdmin(admin.ModelAdmin):
|
||||||
|
|
@ -26,6 +29,7 @@ class CourseSessionAdmin(admin.ModelAdmin):
|
||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
]
|
]
|
||||||
|
actions = [get_feedbacks_for_course]
|
||||||
|
|
||||||
|
|
||||||
@admin.register(CourseSessionUser)
|
@admin.register(CourseSessionUser)
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,7 @@
|
||||||
from datetime import datetime
|
|
||||||
from io import BytesIO
|
|
||||||
from itertools import groupby
|
|
||||||
from operator import attrgetter
|
|
||||||
|
|
||||||
import djclick as click
|
import djclick as click
|
||||||
import structlog
|
import structlog
|
||||||
from openpyxl import Workbook
|
|
||||||
|
|
||||||
from vbv_lernwelt.feedback.models import FeedbackResponse
|
from vbv_lernwelt.feedback.services import export_feedback
|
||||||
|
|
||||||
logger = structlog.get_logger(__name__)
|
|
||||||
|
|
||||||
VV_FEEDBACK_QUESTIONS = [
|
|
||||||
("satisfaction", "Zufriedenheit insgesamt"),
|
|
||||||
("goal_attainment", "Zielerreichung insgesamt"),
|
|
||||||
(
|
|
||||||
"proficiency",
|
|
||||||
"Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Circle?",
|
|
||||||
),
|
|
||||||
("preparation_task_clarity", "Waren die Praxisaufträge klar und verständlich?"),
|
|
||||||
("would_recommend", "Würdest du den Circle weiterempfehlen?"),
|
|
||||||
("course_positive_feedback", "Was hat dir besonders gut gefallen?"),
|
|
||||||
("course_negative_feedback", "Wo siehst du Verbesserungspotential?"),
|
|
||||||
]
|
|
||||||
|
|
||||||
logger = structlog.get_logger(__name__)
|
logger = structlog.get_logger(__name__)
|
||||||
|
|
||||||
|
|
@ -37,60 +16,3 @@ logger = structlog.get_logger(__name__)
|
||||||
def command(course_session_id, save_as_file):
|
def command(course_session_id, save_as_file):
|
||||||
# using the output from call_command was a bit cumbersome, so this is just a wrapper for the actual function
|
# using the output from call_command was a bit cumbersome, so this is just a wrapper for the actual function
|
||||||
export_feedback(course_session_id, save_as_file)
|
export_feedback(course_session_id, save_as_file)
|
||||||
|
|
||||||
|
|
||||||
def export_feedback(course_session_id: str, save_as_file: bool):
|
|
||||||
wb = Workbook()
|
|
||||||
|
|
||||||
# remove the first sheet is just easier than keeping track of the active sheet
|
|
||||||
wb.remove_sheet(wb.active)
|
|
||||||
|
|
||||||
feedbacks = FeedbackResponse.objects.filter(
|
|
||||||
course_session_id=course_session_id
|
|
||||||
).order_by("circle")
|
|
||||||
grouped_feedbacks = groupby(feedbacks, key=attrgetter("circle"))
|
|
||||||
|
|
||||||
for circle, group_feedbacks in grouped_feedbacks:
|
|
||||||
group_feedbacks = list(group_feedbacks)
|
|
||||||
logger.debug(
|
|
||||||
"export_feedback_for_circle",
|
|
||||||
data={
|
|
||||||
"circle": circle.title,
|
|
||||||
"course_session_id": course_session_id,
|
|
||||||
"count": len(group_feedbacks),
|
|
||||||
},
|
|
||||||
label="feedback_export",
|
|
||||||
)
|
|
||||||
create_sheet(wb, circle.title, group_feedbacks)
|
|
||||||
|
|
||||||
if save_as_file:
|
|
||||||
wb.save(make_export_filename())
|
|
||||||
else:
|
|
||||||
output = BytesIO()
|
|
||||||
wb.save(output)
|
|
||||||
|
|
||||||
output.seek(0)
|
|
||||||
return output.getvalue()
|
|
||||||
|
|
||||||
|
|
||||||
def create_sheet(wb: Workbook, title: str, data: list[FeedbackResponse]):
|
|
||||||
sheet = wb.create_sheet(title=title)
|
|
||||||
# add header
|
|
||||||
questions = [q[1] for q in VV_FEEDBACK_QUESTIONS]
|
|
||||||
for col_idx, title in enumerate(questions, start=2):
|
|
||||||
sheet.cell(row=1, column=col_idx, value=title)
|
|
||||||
|
|
||||||
add_rows(sheet, data)
|
|
||||||
return sheet
|
|
||||||
|
|
||||||
|
|
||||||
def add_rows(sheet, data):
|
|
||||||
for row_idx, feedback in enumerate(data, start=2):
|
|
||||||
for col_idx, question in enumerate(VV_FEEDBACK_QUESTIONS, start=2):
|
|
||||||
response = feedback.data.get(question[0], "")
|
|
||||||
sheet.cell(row=row_idx, column=col_idx, value=response)
|
|
||||||
|
|
||||||
|
|
||||||
def make_export_filename():
|
|
||||||
today_date = datetime.today().strftime("%Y-%m-%d")
|
|
||||||
return f"feedback_export_{today_date}.xlsx"
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,12 @@
|
||||||
|
from datetime import datetime
|
||||||
|
from io import BytesIO
|
||||||
|
from itertools import groupby
|
||||||
|
from operator import attrgetter
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from openpyxl import Workbook
|
||||||
|
|
||||||
from vbv_lernwelt.core.models import User
|
from vbv_lernwelt.core.models import User
|
||||||
from vbv_lernwelt.course.models import CourseCompletionStatus, CourseSession
|
from vbv_lernwelt.course.models import CourseCompletionStatus, CourseSession
|
||||||
|
|
@ -13,6 +19,19 @@ from vbv_lernwelt.learnpath.models import (
|
||||||
|
|
||||||
logger = structlog.get_logger(__name__)
|
logger = structlog.get_logger(__name__)
|
||||||
|
|
||||||
|
VV_FEEDBACK_QUESTIONS = [
|
||||||
|
("satisfaction", "Zufriedenheit insgesamt"),
|
||||||
|
("goal_attainment", "Zielerreichung insgesamt"),
|
||||||
|
(
|
||||||
|
"proficiency",
|
||||||
|
"Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Circle?",
|
||||||
|
),
|
||||||
|
("preparation_task_clarity", "Waren die Praxisaufträge klar und verständlich?"),
|
||||||
|
("would_recommend", "Würdest du den Circle weiterempfehlen?"),
|
||||||
|
("course_positive_feedback", "Was hat dir besonders gut gefallen?"),
|
||||||
|
("course_negative_feedback", "Wo siehst du Verbesserungspotential?"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def update_feedback_response(
|
def update_feedback_response(
|
||||||
feedback_user: User,
|
feedback_user: User,
|
||||||
|
|
@ -100,3 +119,78 @@ def initial_data_for_feedback_page(
|
||||||
"feedback_type": "vv",
|
"feedback_type": "vv",
|
||||||
}
|
}
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def export_feedback(course_session_id: str, save_as_file: bool):
|
||||||
|
wb = Workbook()
|
||||||
|
|
||||||
|
# remove the first sheet is just easier than keeping track of the active sheet
|
||||||
|
wb.remove_sheet(wb.active)
|
||||||
|
|
||||||
|
feedbacks = FeedbackResponse.objects.filter(
|
||||||
|
course_session_id=course_session_id,
|
||||||
|
submitted=True,
|
||||||
|
).order_by("circle", "updated_at")
|
||||||
|
grouped_feedbacks = groupby(feedbacks, key=attrgetter("circle"))
|
||||||
|
|
||||||
|
for circle, group_feedbacks in grouped_feedbacks:
|
||||||
|
group_feedbacks = list(group_feedbacks)
|
||||||
|
logger.debug(
|
||||||
|
"export_feedback_for_circle",
|
||||||
|
data={
|
||||||
|
"circle": circle.id,
|
||||||
|
"course_session_id": course_session_id,
|
||||||
|
"count": len(group_feedbacks),
|
||||||
|
},
|
||||||
|
label="feedback_export",
|
||||||
|
)
|
||||||
|
_create_sheet(wb, circle.title, group_feedbacks)
|
||||||
|
|
||||||
|
if save_as_file:
|
||||||
|
wb.save(make_export_filename())
|
||||||
|
else:
|
||||||
|
output = BytesIO()
|
||||||
|
wb.save(output)
|
||||||
|
|
||||||
|
output.seek(0)
|
||||||
|
return output.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
def _create_sheet(wb: Workbook, title: str, data: list[FeedbackResponse]):
|
||||||
|
sheet = wb.create_sheet(title=title)
|
||||||
|
# add header
|
||||||
|
sheet.cell(row=1, column=1, value="Datum")
|
||||||
|
questions = [q[1] for q in VV_FEEDBACK_QUESTIONS]
|
||||||
|
for col_idx, title in enumerate(questions, start=2):
|
||||||
|
sheet.cell(row=1, column=col_idx, value=title)
|
||||||
|
|
||||||
|
_add_rows(sheet, data)
|
||||||
|
return sheet
|
||||||
|
|
||||||
|
|
||||||
|
def _add_rows(sheet, data):
|
||||||
|
for row_idx, feedback in enumerate(data, start=2):
|
||||||
|
sheet.cell(
|
||||||
|
row=row_idx, column=1, value=feedback.updated_at.date().strftime("%d.%m.%Y")
|
||||||
|
)
|
||||||
|
for col_idx, question in enumerate(VV_FEEDBACK_QUESTIONS, start=2):
|
||||||
|
response = feedback.data.get(question[0], "")
|
||||||
|
sheet.cell(row=row_idx, column=col_idx, value=response)
|
||||||
|
|
||||||
|
|
||||||
|
def make_export_filename():
|
||||||
|
today_date = datetime.today().strftime("%Y-%m-%d")
|
||||||
|
return f"feedback_export_{today_date}.xlsx"
|
||||||
|
|
||||||
|
|
||||||
|
# used as admin action, that's why it's not in the views.py
|
||||||
|
def get_feedbacks_for_course(_modeladmin, _request, queryset):
|
||||||
|
excel_bytes = export_feedback(queryset.first(), False)
|
||||||
|
|
||||||
|
response = HttpResponse(
|
||||||
|
content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||||
|
)
|
||||||
|
response["Content-Disposition"] = f"attachment; filename={make_export_filename()}"
|
||||||
|
response.write(excel_bytes)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,10 @@
|
||||||
import itertools
|
import itertools
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from django.http import HttpResponse
|
from rest_framework.decorators import api_view
|
||||||
from rest_framework import authentication
|
|
||||||
from rest_framework.decorators import api_view, authentication_classes, permission_classes
|
|
||||||
from rest_framework.exceptions import PermissionDenied
|
from rest_framework.exceptions import PermissionDenied
|
||||||
from rest_framework.permissions import IsAdminUser
|
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from vbv_lernwelt.feedback.management.commands.export_feedback import export_feedback, make_export_filename
|
|
||||||
from vbv_lernwelt.feedback.models import FeedbackResponse
|
from vbv_lernwelt.feedback.models import FeedbackResponse
|
||||||
from vbv_lernwelt.feedback.utils import feedback_users
|
from vbv_lernwelt.feedback.utils import feedback_users
|
||||||
from vbv_lernwelt.iam.permissions import is_course_session_expert
|
from vbv_lernwelt.iam.permissions import is_course_session_expert
|
||||||
|
|
@ -82,16 +78,3 @@ def get_feedback_for_circle(request, course_session_id, circle_id):
|
||||||
feedback_data["questions"][field].append(data)
|
feedback_data["questions"][field].append(data)
|
||||||
|
|
||||||
return Response(status=200, data=feedback_data)
|
return Response(status=200, data=feedback_data)
|
||||||
|
|
||||||
|
|
||||||
@api_view(["GET"])
|
|
||||||
@authentication_classes((authentication.SessionAuthentication,))
|
|
||||||
@permission_classes((IsAdminUser,))
|
|
||||||
def export_feedback_for_course_session(request, course_session_id):
|
|
||||||
excel_bytes = export_feedback(course_session_id, False)
|
|
||||||
|
|
||||||
response = HttpResponse(content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
|
|
||||||
response['Content-Disposition'] = f"attachment; filename={make_export_filename()}"
|
|
||||||
response.write(excel_bytes)
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue