Merge branch 'develop' into feat/course-feature-toggles
This commit is contained in:
commit
e64bab918e
|
|
@ -549,8 +549,6 @@ type AssignmentCompletionObjectType {
|
||||||
submitted_at: DateTime
|
submitted_at: DateTime
|
||||||
evaluation_submitted_at: DateTime
|
evaluation_submitted_at: DateTime
|
||||||
evaluation_user: UserObjectType
|
evaluation_user: UserObjectType
|
||||||
evaluation_points: Float
|
|
||||||
evaluation_max_points: Float
|
|
||||||
evaluation_passed: Boolean
|
evaluation_passed: Boolean
|
||||||
edoniq_extended_time_flag: Boolean!
|
edoniq_extended_time_flag: Boolean!
|
||||||
assignment_user: UserObjectType!
|
assignment_user: UserObjectType!
|
||||||
|
|
@ -561,6 +559,8 @@ type AssignmentCompletionObjectType {
|
||||||
additional_json_data: JSONString!
|
additional_json_data: JSONString!
|
||||||
task_completion_data: GenericScalar
|
task_completion_data: GenericScalar
|
||||||
learning_content_page_id: ID
|
learning_content_page_id: ID
|
||||||
|
evaluation_points: Float
|
||||||
|
evaluation_max_points: Float
|
||||||
}
|
}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -24,5 +24,8 @@ fi
|
||||||
# Create Prüfungslehrgang
|
# Create Prüfungslehrgang
|
||||||
python /app/manage.py create_vermittler_pruefung
|
python /app/manage.py create_vermittler_pruefung
|
||||||
|
|
||||||
|
# Create Motorfahrzeug Prüfungslehrgang
|
||||||
|
python /app/manage.py create_motorfahrzeug_pruefung
|
||||||
|
|
||||||
# Set the command to run supervisord
|
# Set the command to run supervisord
|
||||||
/home/django/.local/bin/supervisord -c /app/supervisord.conf
|
/home/django/.local/bin/supervisord -c /app/supervisord.conf
|
||||||
|
|
|
||||||
|
|
@ -737,6 +737,7 @@ CONSTANCE_CONFIG = {
|
||||||
"Default value is empty and will not send any emails. (No regex support!)",
|
"Default value is empty and will not send any emails. (No regex support!)",
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
TRACKING_TAG = env("IT_TRACKING_TAG", default="")
|
||||||
|
|
||||||
if APP_ENVIRONMENT == "local":
|
if APP_ENVIRONMENT == "local":
|
||||||
# http://whitenoise.evans.io/en/latest/django.html#using-whitenoise-in-development
|
# http://whitenoise.evans.io/en/latest/django.html#using-whitenoise-in-development
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ from vbv_lernwelt.core.views import (
|
||||||
check_rate_limit,
|
check_rate_limit,
|
||||||
cypress_reset_view,
|
cypress_reset_view,
|
||||||
generate_web_component_icons,
|
generate_web_component_icons,
|
||||||
|
iterativ_test_coursesessions_reset_view,
|
||||||
permission_denied_view,
|
permission_denied_view,
|
||||||
rate_limit_exceeded_view,
|
rate_limit_exceeded_view,
|
||||||
vue_home,
|
vue_home,
|
||||||
|
|
@ -209,6 +210,13 @@ urlpatterns = [
|
||||||
name="t2l_sync",
|
name="t2l_sync",
|
||||||
),
|
),
|
||||||
|
|
||||||
|
# iterativ Test course sessions
|
||||||
|
path(
|
||||||
|
r"api/core/resetiterativsessions/",
|
||||||
|
iterativ_test_coursesessions_reset_view,
|
||||||
|
name="iterativ_test_coursesessions_reset_view",
|
||||||
|
),
|
||||||
|
|
||||||
path("server/graphql/",
|
path("server/graphql/",
|
||||||
csrf_exempt(GraphQLView.as_view(graphiql=True, schema=schema))),
|
csrf_exempt(GraphQLView.as_view(graphiql=True, schema=schema))),
|
||||||
# testing and debug
|
# testing and debug
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,10 @@ class AssignmentCompletionObjectType(DjangoObjectType):
|
||||||
task_completion_data = GenericScalar()
|
task_completion_data = GenericScalar()
|
||||||
learning_content_page_id = graphene.ID(source="learning_content_page_id")
|
learning_content_page_id = graphene.ID(source="learning_content_page_id")
|
||||||
|
|
||||||
|
# rounded to sensible representation
|
||||||
|
evaluation_points = graphene.Float()
|
||||||
|
evaluation_max_points = graphene.Float()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = AssignmentCompletion
|
model = AssignmentCompletion
|
||||||
fields = (
|
fields = (
|
||||||
|
|
@ -34,12 +38,20 @@ class AssignmentCompletionObjectType(DjangoObjectType):
|
||||||
"evaluation_user",
|
"evaluation_user",
|
||||||
"additional_json_data",
|
"additional_json_data",
|
||||||
"edoniq_extended_time_flag",
|
"edoniq_extended_time_flag",
|
||||||
"evaluation_points",
|
|
||||||
"evaluation_passed",
|
"evaluation_passed",
|
||||||
"evaluation_max_points",
|
|
||||||
"task_completion_data",
|
"task_completion_data",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def resolve_evaluation_points(self, info):
|
||||||
|
if self.evaluation_points:
|
||||||
|
return round(self.evaluation_points, 1) # noqa
|
||||||
|
return None
|
||||||
|
|
||||||
|
def resolve_evaluation_max_points(self, info):
|
||||||
|
if self.evaluation_max_points:
|
||||||
|
return round(self.evaluation_max_points, 1) # noqa
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class AssignmentObjectType(DjangoObjectType):
|
class AssignmentObjectType(DjangoObjectType):
|
||||||
tasks = JSONStreamField()
|
tasks = JSONStreamField()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,336 @@
|
||||||
|
from datetime import datetime, time, timedelta
|
||||||
|
|
||||||
|
import djclick as click
|
||||||
|
import structlog
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from vbv_lernwelt.assignment.models import Assignment, AssignmentCompletion
|
||||||
|
from vbv_lernwelt.core.admin import User
|
||||||
|
from vbv_lernwelt.course.consts import (
|
||||||
|
COURSE_VERSICHERUNGSVERMITTLERIN_FR_ID,
|
||||||
|
COURSE_VERSICHERUNGSVERMITTLERIN_ID,
|
||||||
|
COURSE_VERSICHERUNGSVERMITTLERIN_IT_ID,
|
||||||
|
)
|
||||||
|
from vbv_lernwelt.course.models import (
|
||||||
|
Course,
|
||||||
|
CourseCompletion,
|
||||||
|
CourseSession,
|
||||||
|
CourseSessionUser,
|
||||||
|
)
|
||||||
|
from vbv_lernwelt.course_session.models import (
|
||||||
|
CourseSessionAssignment,
|
||||||
|
CourseSessionAttendanceCourse,
|
||||||
|
CourseSessionEdoniqTest,
|
||||||
|
)
|
||||||
|
from vbv_lernwelt.course_session_group.models import CourseSessionGroup
|
||||||
|
from vbv_lernwelt.feedback.models import FeedbackResponse
|
||||||
|
from vbv_lernwelt.learning_mentor.models import LearningMentor
|
||||||
|
from vbv_lernwelt.learnpath.models import Circle
|
||||||
|
from vbv_lernwelt.notify.models import Notification
|
||||||
|
|
||||||
|
logger = structlog.get_logger(__name__)
|
||||||
|
from vbv_lernwelt.importer.services import (
|
||||||
|
create_or_update_course_session,
|
||||||
|
get_uk_course,
|
||||||
|
LP_DATA,
|
||||||
|
TRANSLATIONS,
|
||||||
|
)
|
||||||
|
|
||||||
|
IT_VV_TEST_COURSE = "Iterativ VV Testkurs"
|
||||||
|
IT_UK_TEST_COURSE = "Iterativ üK Testkurs"
|
||||||
|
IT_UK_TEST_REGION = "Iterativ Region"
|
||||||
|
TIME_FORMAT = "%d.%m.%Y, %H:%M"
|
||||||
|
PASSWORD = "KqaDm3-x8zhCKHLWDV_oiqFrYWHg"
|
||||||
|
|
||||||
|
logger = structlog.get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
def command():
|
||||||
|
create_or_update_uk()
|
||||||
|
create_or_update_vv()
|
||||||
|
|
||||||
|
|
||||||
|
def create_or_update_uk(language="de"):
|
||||||
|
uk_course = get_uk_course(language)
|
||||||
|
uk_circle_keys = [
|
||||||
|
"Kickoff",
|
||||||
|
"Basis",
|
||||||
|
"Fahrzeug",
|
||||||
|
"Haushalt Teil 1",
|
||||||
|
"Haushalt Teil 2",
|
||||||
|
]
|
||||||
|
|
||||||
|
data = create_uk_data(language)
|
||||||
|
create_or_update_course_session(
|
||||||
|
uk_course,
|
||||||
|
data,
|
||||||
|
language,
|
||||||
|
circle_keys=uk_circle_keys,
|
||||||
|
)
|
||||||
|
cs = CourseSession.objects.get(import_id=data["ID"])
|
||||||
|
|
||||||
|
members, trainer, regionenleiter = get_or_create_users_uk()
|
||||||
|
delete_cs_data(cs, members + [trainer, regionenleiter])
|
||||||
|
|
||||||
|
add_to_course_session(cs, members)
|
||||||
|
add_trainers_to_course_session(cs, [trainer], uk_circle_keys, language)
|
||||||
|
create_and_add_to_cs_group(cs.course, IT_UK_TEST_REGION, [cs], regionenleiter)
|
||||||
|
|
||||||
|
|
||||||
|
def create_or_update_vv(language="de"):
|
||||||
|
vv_course = get_vv_course(language)
|
||||||
|
|
||||||
|
cs, _created = CourseSession.objects.get_or_create(
|
||||||
|
course=vv_course, import_id=IT_VV_TEST_COURSE
|
||||||
|
)
|
||||||
|
cs.title = IT_VV_TEST_COURSE
|
||||||
|
cs.save()
|
||||||
|
|
||||||
|
create_or_update_assignment_course_session(cs)
|
||||||
|
members, member_with_mentor, mentor = get_or_create_users_vv()
|
||||||
|
delete_cs_data(cs, members + [member_with_mentor, mentor])
|
||||||
|
|
||||||
|
add_to_course_session(cs, members + [member_with_mentor])
|
||||||
|
add_mentor_to_course_session(cs, [(mentor, member_with_mentor)])
|
||||||
|
|
||||||
|
|
||||||
|
def delete_cs_data(cs: CourseSession, users: list[User]):
|
||||||
|
if cs:
|
||||||
|
CourseCompletion.objects.filter(course_session=cs).delete()
|
||||||
|
Notification.objects.filter(course_session=cs).delete()
|
||||||
|
AssignmentCompletion.objects.filter(course_session=cs).delete()
|
||||||
|
CourseSessionAttendanceCourse.objects.filter(course_session=cs).update(
|
||||||
|
attendance_user_list=[]
|
||||||
|
)
|
||||||
|
CourseSessionEdoniqTest.objects.filter(course_session=cs).delete()
|
||||||
|
CourseSessionUser.objects.filter(course_session=cs).delete()
|
||||||
|
learning_mentor_ids = (
|
||||||
|
LearningMentor.objects.filter(participants__course_session=cs)
|
||||||
|
.values_list("id", flat=True)
|
||||||
|
.distinct()
|
||||||
|
| LearningMentor.objects.filter(mentor__in=users)
|
||||||
|
.values_list("id", flat=True)
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
# cannot call delete on distinct objects
|
||||||
|
LearningMentor.objects.filter(id__in=list(learning_mentor_ids)).delete()
|
||||||
|
else:
|
||||||
|
logger.info("no_course_session_found", import_id=cs.import_id)
|
||||||
|
|
||||||
|
FeedbackResponse.objects.filter(feedback_user__in=users).delete()
|
||||||
|
|
||||||
|
|
||||||
|
def add_to_course_session(
|
||||||
|
course_session: CourseSession,
|
||||||
|
members: list[User],
|
||||||
|
role=CourseSessionUser.Role.MEMBER,
|
||||||
|
):
|
||||||
|
if course_session:
|
||||||
|
for user in members:
|
||||||
|
csu, _created = CourseSessionUser.objects.get_or_create(
|
||||||
|
course_session_id=course_session.id, user_id=user.id, role=role
|
||||||
|
)
|
||||||
|
csu.save()
|
||||||
|
|
||||||
|
|
||||||
|
def add_mentor_to_course_session(
|
||||||
|
course_session: CourseSession, mentor_mentee_pairs: list[tuple[User, User]]
|
||||||
|
):
|
||||||
|
for mentor, mentee in mentor_mentee_pairs:
|
||||||
|
lm = LearningMentor.objects.create(
|
||||||
|
course=course_session.course,
|
||||||
|
mentor=mentor,
|
||||||
|
)
|
||||||
|
lm.participants.add(
|
||||||
|
CourseSessionUser.objects.get(
|
||||||
|
user__id=mentee.id,
|
||||||
|
course_session=course_session,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def add_trainers_to_course_session(
|
||||||
|
course_session: CourseSession,
|
||||||
|
trainers: list[User],
|
||||||
|
circle_keys: list[str],
|
||||||
|
language,
|
||||||
|
):
|
||||||
|
add_to_course_session(course_session, trainers, CourseSessionUser.Role.EXPERT)
|
||||||
|
for user in trainers:
|
||||||
|
for circle_key in circle_keys:
|
||||||
|
circle_name = LP_DATA[circle_key][language]["title"]
|
||||||
|
circle = Circle.objects.filter(
|
||||||
|
slug=f"{course_session.course.slug}-lp-circle-{circle_name.lower()}"
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if course_session and circle:
|
||||||
|
csu = CourseSessionUser.objects.filter(
|
||||||
|
course_session_id=course_session.id, user_id=user.id
|
||||||
|
).first()
|
||||||
|
if csu:
|
||||||
|
csu.expert.add(circle)
|
||||||
|
csu.save()
|
||||||
|
|
||||||
|
|
||||||
|
def get_or_create_users_uk():
|
||||||
|
members = [
|
||||||
|
_create_or_update_user(
|
||||||
|
f"teilnehmer{n}.uk@iterativ.ch", "Teilnehmer üK", "Iterativ", PASSWORD, "de"
|
||||||
|
)
|
||||||
|
for n in range(1, 10)
|
||||||
|
]
|
||||||
|
trainer = _create_or_update_user(
|
||||||
|
"trainer1.uk@iterativ.ch", "Trainer üK", "Iterativ", PASSWORD, "de"
|
||||||
|
)
|
||||||
|
regionenleiter = _create_or_update_user(
|
||||||
|
"regionenleiter1.uk@iterativ.ch",
|
||||||
|
"Regionenleiter üK",
|
||||||
|
"Iterativ",
|
||||||
|
PASSWORD,
|
||||||
|
"de",
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
members,
|
||||||
|
trainer,
|
||||||
|
regionenleiter,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_or_create_users_vv():
|
||||||
|
members = [
|
||||||
|
_create_or_update_user(
|
||||||
|
f"teilnehmer{n}.vv@iterativ.ch", "Teilnehmer VV", "Iterativ", PASSWORD, "de"
|
||||||
|
)
|
||||||
|
for n in range(1, 10)
|
||||||
|
]
|
||||||
|
member_with_mentor = _create_or_update_user(
|
||||||
|
"teilnehmer1.vv.lb@iterativ.ch",
|
||||||
|
"Teilnehmer VV mit LB",
|
||||||
|
"Iterativ",
|
||||||
|
PASSWORD,
|
||||||
|
"de",
|
||||||
|
)
|
||||||
|
mentor = _create_or_update_user(
|
||||||
|
"lernbegleitung1.vv@iterativ.ch",
|
||||||
|
"Lernbegleitung VV",
|
||||||
|
"Iterativ",
|
||||||
|
PASSWORD,
|
||||||
|
"de",
|
||||||
|
)
|
||||||
|
return members, member_with_mentor, mentor
|
||||||
|
|
||||||
|
|
||||||
|
def _create_or_update_user(email, first_name, last_name, password, language):
|
||||||
|
try:
|
||||||
|
user = User.objects.get(email=email)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
user = User(
|
||||||
|
email=email,
|
||||||
|
username=email,
|
||||||
|
)
|
||||||
|
|
||||||
|
user.email = email
|
||||||
|
user.first_name = first_name or user.first_name
|
||||||
|
user.last_name = last_name or user.last_name
|
||||||
|
user.username = email
|
||||||
|
user.language = language
|
||||||
|
user.set_password(password)
|
||||||
|
user.save()
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
def create_uk_data(language):
|
||||||
|
return {
|
||||||
|
"Klasse": IT_UK_TEST_COURSE,
|
||||||
|
"ID": IT_UK_TEST_COURSE,
|
||||||
|
"Generation": 2024,
|
||||||
|
"Region": "Bern",
|
||||||
|
"Sprache": language,
|
||||||
|
f"Kickoff {TRANSLATIONS[language]['start']}": timezone.make_aware(
|
||||||
|
datetime.combine((timezone.now() + timedelta(weeks=2)).date(), time(9, 0))
|
||||||
|
).strftime("%d.%m.%Y, %H:%M"),
|
||||||
|
f"Kickoff {TRANSLATIONS[language]['ende']}": timezone.make_aware(
|
||||||
|
datetime.combine((timezone.now() + timedelta(weeks=2)).date(), time(16, 0))
|
||||||
|
).strftime("%d.%m.%Y, %H:%M"),
|
||||||
|
f"Kickoff {TRANSLATIONS[language]['raum']}": "Raum 1",
|
||||||
|
f"Kickoff {TRANSLATIONS[language]['standort']}": "Bern",
|
||||||
|
f"Kickoff {TRANSLATIONS[language]['adresse']}": "Musterstrasse 1",
|
||||||
|
f"Basis {TRANSLATIONS[language]['start']}": timezone.make_aware(
|
||||||
|
datetime.combine((timezone.now() + timedelta(weeks=4)).date(), time(9, 0))
|
||||||
|
).strftime("%d.%m.%Y, %H:%M"),
|
||||||
|
f"Basis {TRANSLATIONS[language]['ende']}": timezone.make_aware(
|
||||||
|
datetime.combine((timezone.now() + timedelta(weeks=4)).date(), time(16, 0))
|
||||||
|
).strftime("%d.%m.%Y, %H:%M"),
|
||||||
|
f"Basis {TRANSLATIONS[language]['raum']}": "Raum 1",
|
||||||
|
f"Basis {TRANSLATIONS[language]['standort']}": "Bern",
|
||||||
|
f"Basis {TRANSLATIONS[language]['adresse']}": "Musterstrasse 1",
|
||||||
|
f"Fahrzeug {TRANSLATIONS[language]['start']}": timezone.make_aware(
|
||||||
|
datetime.combine((timezone.now() + timedelta(weeks=6)).date(), time(9, 0))
|
||||||
|
).strftime("%d.%m.%Y, %H:%M"),
|
||||||
|
f"Fahrzeug {TRANSLATIONS[language]['ende']}": timezone.make_aware(
|
||||||
|
datetime.combine((timezone.now() + timedelta(weeks=6)).date(), time(16, 0))
|
||||||
|
).strftime("%d.%m.%Y, %H:%M"),
|
||||||
|
f"Fahrzeug {TRANSLATIONS[language]['raum']}": "Raum 1",
|
||||||
|
f"Fahrzeug {TRANSLATIONS[language]['standort']}": "Bern",
|
||||||
|
f"Fahrzeug {TRANSLATIONS[language]['adresse']}": "Musterstrasse 1",
|
||||||
|
f"Haushalt Teil 1 {TRANSLATIONS[language]['start']}": timezone.make_aware(
|
||||||
|
datetime.combine((timezone.now() + timedelta(weeks=8)).date(), time(9, 0))
|
||||||
|
).strftime("%d.%m.%Y, %H:%M"),
|
||||||
|
f"Haushalt Teil 1 {TRANSLATIONS[language]['ende']}": timezone.make_aware(
|
||||||
|
datetime.combine((timezone.now() + timedelta(weeks=8)).date(), time(16, 0))
|
||||||
|
).strftime("%d.%m.%Y, %H:%M"),
|
||||||
|
f"Haushalt Teil 1 {TRANSLATIONS[language]['raum']}": "Raum 1",
|
||||||
|
f"Haushalt Teil 1 {TRANSLATIONS[language]['standort']}": "Bern",
|
||||||
|
f"Haushalt Teil 1 {TRANSLATIONS[language]['adresse']}": "Musterstrasse 1",
|
||||||
|
f"Haushalt Teil 2 {TRANSLATIONS[language]['start']}": timezone.make_aware(
|
||||||
|
datetime.combine((timezone.now() + timedelta(weeks=10)).date(), time(9, 0))
|
||||||
|
).strftime("%d.%m.%Y, %H:%M"),
|
||||||
|
f"Haushalt Teil 2 {TRANSLATIONS[language]['ende']}": timezone.make_aware(
|
||||||
|
datetime.combine((timezone.now() + timedelta(weeks=10)).date(), time(16, 0))
|
||||||
|
).strftime("%d.%m.%Y, %H:%M"),
|
||||||
|
f"Haushalt Teil 2 {TRANSLATIONS[language]['raum']}": "Raum 1",
|
||||||
|
f"Haushalt Teil 2 {TRANSLATIONS[language]['standort']}": "Bern",
|
||||||
|
f"Haushalt Teil 2 {TRANSLATIONS[language]['adresse']}": "Musterstrasse 1",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def create_and_add_to_cs_group(
|
||||||
|
course: Course, name: str, course_sessions: list[CourseSession], supervisor: User
|
||||||
|
):
|
||||||
|
region, _ = CourseSessionGroup.objects.get_or_create(
|
||||||
|
name=name,
|
||||||
|
course=course,
|
||||||
|
)
|
||||||
|
|
||||||
|
for cs in course_sessions:
|
||||||
|
region.course_session.add(cs)
|
||||||
|
|
||||||
|
region.supervisor.add(supervisor)
|
||||||
|
|
||||||
|
|
||||||
|
def get_vv_course(language: str) -> Course:
|
||||||
|
if language == "fr":
|
||||||
|
course_id = COURSE_VERSICHERUNGSVERMITTLERIN_FR_ID
|
||||||
|
elif language == "it":
|
||||||
|
course_id = COURSE_VERSICHERUNGSVERMITTLERIN_IT_ID
|
||||||
|
else:
|
||||||
|
course_id = COURSE_VERSICHERUNGSVERMITTLERIN_ID
|
||||||
|
|
||||||
|
return Course.objects.get(id=course_id)
|
||||||
|
|
||||||
|
|
||||||
|
def create_or_update_assignment_course_session(cs: CourseSession):
|
||||||
|
# not nice but works for now
|
||||||
|
for assignment in Assignment.objects.all():
|
||||||
|
if assignment.get_course().id == cs.course.id:
|
||||||
|
logger.debug(
|
||||||
|
"create_course_session_assigments",
|
||||||
|
assignment=assignment,
|
||||||
|
label="reset_test_courses",
|
||||||
|
)
|
||||||
|
for lca in assignment.learningcontentassignment_set.all():
|
||||||
|
_csa, _created = CourseSessionAssignment.objects.get_or_create(
|
||||||
|
course_session=cs,
|
||||||
|
learning_content=lca,
|
||||||
|
)
|
||||||
|
|
@ -58,6 +58,12 @@ def vue_home(request, *args):
|
||||||
|
|
||||||
# render index.html from `npm run build`
|
# render index.html from `npm run build`
|
||||||
content = loader.render_to_string("vue/index.html", context={}, request=request)
|
content = loader.render_to_string("vue/index.html", context={}, request=request)
|
||||||
|
# inject Plausible tracking tag
|
||||||
|
if settings.TRACKING_TAG:
|
||||||
|
content = content.replace(
|
||||||
|
"</head>",
|
||||||
|
f"\n{settings.TRACKING_TAG}\n</head>",
|
||||||
|
)
|
||||||
return HttpResponse(content)
|
return HttpResponse(content)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -179,6 +185,17 @@ def cypress_reset_view(request):
|
||||||
return HttpResponseRedirect("/server/admin/")
|
return HttpResponseRedirect("/server/admin/")
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(["POST"])
|
||||||
|
@authentication_classes((authentication.SessionAuthentication,))
|
||||||
|
@permission_classes((IsAdminUser,))
|
||||||
|
def iterativ_test_coursesessions_reset_view(request):
|
||||||
|
call_command(
|
||||||
|
"reset_iterativ_test_sessions",
|
||||||
|
)
|
||||||
|
|
||||||
|
return HttpResponseRedirect("/server/admin/")
|
||||||
|
|
||||||
|
|
||||||
@django_view_authentication_exempt
|
@django_view_authentication_exempt
|
||||||
def generate_web_component_icons(request):
|
def generate_web_component_icons(request):
|
||||||
svg_files = []
|
svg_files = []
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,15 @@ from vbv_lernwelt.course.models import (
|
||||||
CourseSession,
|
CourseSession,
|
||||||
CourseSessionUser,
|
CourseSessionUser,
|
||||||
)
|
)
|
||||||
|
from vbv_lernwelt.feedback.services import (
|
||||||
|
get_feedbacks_for_course_sessions,
|
||||||
|
get_feedbacks_for_courses,
|
||||||
|
)
|
||||||
from vbv_lernwelt.learnpath.models import Circle
|
from vbv_lernwelt.learnpath.models import Circle
|
||||||
|
|
||||||
|
get_feedbacks_for_course_sessions.short_description = "Feedback export"
|
||||||
|
get_feedbacks_for_courses.short_description = "Feedback export"
|
||||||
|
|
||||||
|
|
||||||
@admin.register(CourseConfiguration)
|
@admin.register(CourseConfiguration)
|
||||||
class CourseConfigurationAdmin(admin.ModelAdmin):
|
class CourseConfigurationAdmin(admin.ModelAdmin):
|
||||||
|
|
@ -27,6 +34,7 @@ class CourseAdmin(admin.ModelAdmin):
|
||||||
"category_name",
|
"category_name",
|
||||||
"slug",
|
"slug",
|
||||||
]
|
]
|
||||||
|
actions = [get_feedbacks_for_courses]
|
||||||
|
|
||||||
|
|
||||||
@admin.register(CourseSession)
|
@admin.register(CourseSession)
|
||||||
|
|
@ -41,6 +49,7 @@ class CourseSessionAdmin(admin.ModelAdmin):
|
||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
]
|
]
|
||||||
|
actions = [get_feedbacks_for_course_sessions]
|
||||||
|
|
||||||
|
|
||||||
@admin.register(CourseSessionUser)
|
@admin.register(CourseSessionUser)
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,14 @@ COURSE_UK_TRAINING_IT = -9
|
||||||
COURSE_VERSICHERUNGSVERMITTLERIN_FR_ID = -10
|
COURSE_VERSICHERUNGSVERMITTLERIN_FR_ID = -10
|
||||||
COURSE_VERSICHERUNGSVERMITTLERIN_IT_ID = -11
|
COURSE_VERSICHERUNGSVERMITTLERIN_IT_ID = -11
|
||||||
COURSE_VERSICHERUNGSVERMITTLERIN_PRUEFUNG_ID = -12
|
COURSE_VERSICHERUNGSVERMITTLERIN_PRUEFUNG_ID = -12
|
||||||
|
COURSE_MOTORFAHRZEUG_PRUEFUNG_ID = -13
|
||||||
|
|
||||||
VV_COURSE_IDS = [
|
VV_COURSE_IDS = [
|
||||||
COURSE_VERSICHERUNGSVERMITTLERIN_ID,
|
COURSE_VERSICHERUNGSVERMITTLERIN_ID,
|
||||||
COURSE_VERSICHERUNGSVERMITTLERIN_FR_ID,
|
COURSE_VERSICHERUNGSVERMITTLERIN_FR_ID,
|
||||||
COURSE_VERSICHERUNGSVERMITTLERIN_IT_ID,
|
COURSE_VERSICHERUNGSVERMITTLERIN_IT_ID,
|
||||||
COURSE_VERSICHERUNGSVERMITTLERIN_PRUEFUNG_ID,
|
COURSE_VERSICHERUNGSVERMITTLERIN_PRUEFUNG_ID,
|
||||||
|
COURSE_MOTORFAHRZEUG_PRUEFUNG_ID,
|
||||||
]
|
]
|
||||||
|
|
||||||
UK_COURSE_IDS = [
|
UK_COURSE_IDS = [
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ from vbv_lernwelt.core.constants import TEST_MENTOR1_USER_ID
|
||||||
from vbv_lernwelt.core.create_default_users import default_users
|
from vbv_lernwelt.core.create_default_users import default_users
|
||||||
from vbv_lernwelt.core.models import User
|
from vbv_lernwelt.core.models import User
|
||||||
from vbv_lernwelt.course.consts import (
|
from vbv_lernwelt.course.consts import (
|
||||||
|
COURSE_MOTORFAHRZEUG_PRUEFUNG_ID,
|
||||||
COURSE_TEST_ID,
|
COURSE_TEST_ID,
|
||||||
COURSE_UK,
|
COURSE_UK,
|
||||||
COURSE_UK_FR,
|
COURSE_UK_FR,
|
||||||
|
|
@ -93,6 +94,7 @@ from vbv_lernwelt.importer.services import (
|
||||||
)
|
)
|
||||||
from vbv_lernwelt.learning_mentor.models import LearningMentor
|
from vbv_lernwelt.learning_mentor.models import LearningMentor
|
||||||
from vbv_lernwelt.learnpath.create_vv_new_learning_path import (
|
from vbv_lernwelt.learnpath.create_vv_new_learning_path import (
|
||||||
|
create_vv_motorfahrzeug_pruefung_learning_path,
|
||||||
create_vv_new_learning_path,
|
create_vv_new_learning_path,
|
||||||
create_vv_pruefung_learning_path,
|
create_vv_pruefung_learning_path,
|
||||||
)
|
)
|
||||||
|
|
@ -307,6 +309,34 @@ def create_versicherungsvermittlerin_pruefung_course(
|
||||||
create_vv_pruefung_learning_path(course_id=course_id)
|
create_vv_pruefung_learning_path(course_id=course_id)
|
||||||
|
|
||||||
|
|
||||||
|
def create_motorfahrzeug_pruefung_course(
|
||||||
|
course_id=COURSE_MOTORFAHRZEUG_PRUEFUNG_ID, language="de"
|
||||||
|
):
|
||||||
|
names = {
|
||||||
|
"de": "Motorfahrzeug Versicherungsvermittler/-in VBV Prüfung",
|
||||||
|
"fr": "Véhicules à moteur Intermédiaire d’assurance AFA Examen",
|
||||||
|
"it": "Veicolo a motore Intermediario/a assicurativo/a AFA Esame",
|
||||||
|
}
|
||||||
|
# Versicherungsvermittler/in mit neuen Circles
|
||||||
|
course = create_versicherungsvermittlerin_with_categories(
|
||||||
|
course_id=course_id,
|
||||||
|
title=names[language],
|
||||||
|
)
|
||||||
|
|
||||||
|
# assignments create assignments parent page
|
||||||
|
_assignment_list_page = AssignmentListPageFactory(
|
||||||
|
parent=course.coursepage,
|
||||||
|
)
|
||||||
|
|
||||||
|
create_vv_new_competence_profile(course_id=course_id)
|
||||||
|
create_default_media_library(course_id=course_id)
|
||||||
|
create_vv_reflection(course_id=course_id)
|
||||||
|
|
||||||
|
CourseSession.objects.create(course_id=course_id, title=names[language])
|
||||||
|
|
||||||
|
create_vv_motorfahrzeug_pruefung_learning_path(course_id=course_id)
|
||||||
|
|
||||||
|
|
||||||
def create_course_uk_de(course_id=COURSE_UK, lang="de"):
|
def create_course_uk_de(course_id=COURSE_UK, lang="de"):
|
||||||
names = {
|
names = {
|
||||||
"de": "Überbetriebliche Kurse",
|
"de": "Überbetriebliche Kurse",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import djclick as click
|
||||||
|
|
||||||
|
from vbv_lernwelt.course.consts import COURSE_MOTORFAHRZEUG_PRUEFUNG_ID
|
||||||
|
from vbv_lernwelt.course.management.commands.create_default_courses import (
|
||||||
|
create_motorfahrzeug_pruefung_course,
|
||||||
|
)
|
||||||
|
from vbv_lernwelt.course.models import Course
|
||||||
|
|
||||||
|
ADMIN_EMAILS = ["info@iterativ.ch", "admin"]
|
||||||
|
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
def command():
|
||||||
|
print(
|
||||||
|
"Creating Motorfahrzeug Vermittler Prüfung course",
|
||||||
|
COURSE_MOTORFAHRZEUG_PRUEFUNG_ID,
|
||||||
|
)
|
||||||
|
|
||||||
|
if Course.objects.filter(id=COURSE_MOTORFAHRZEUG_PRUEFUNG_ID).exists():
|
||||||
|
print("Course already exists, skipping")
|
||||||
|
return
|
||||||
|
|
||||||
|
create_motorfahrzeug_pruefung_course()
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import djclick as click
|
||||||
|
import structlog
|
||||||
|
|
||||||
|
from vbv_lernwelt.feedback.services import export_feedback
|
||||||
|
|
||||||
|
logger = structlog.get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
@click.argument("course_session_id")
|
||||||
|
@click.option(
|
||||||
|
"--save-as-file/--no-save-as-file",
|
||||||
|
default=True,
|
||||||
|
help="`save-as-file` to save the file, `no-save-as-file` returns bytes. Default is `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
|
||||||
|
export_feedback([course_session_id], save_as_file)
|
||||||
|
|
@ -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,47 @@ 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?"),
|
||||||
|
]
|
||||||
|
|
||||||
|
UK_FEEDBACK_QUESTIONS = [
|
||||||
|
("satisfaction", "Zufriedenheit insgesamt"),
|
||||||
|
("goal_attainment", "Zielerreichung insgesamt"),
|
||||||
|
(
|
||||||
|
"proficiency",
|
||||||
|
"Wie beurteilst du deine Sicherheit bezüglichen den Themen nach dem Kurs?",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"preparation_task_clarity",
|
||||||
|
"Waren die Vorbereitungsaufträge klar und verständlich?",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"instructor_competence",
|
||||||
|
"Wie beurteilst du die Themensicherheit und Fachkompetenz des Kursleiters/der Kursleiterin?",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"instructor_respect",
|
||||||
|
"Wurden Fragen und Anregungen der Kursteilnehmenden ernst genommen und aufgegriffen?",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"instructor_open_feedback",
|
||||||
|
"Was möchtest du dem Kursleiter/der Kursleiterin sonst noch sagen?",
|
||||||
|
),
|
||||||
|
("would_recommend", "Würdest du den Kurs 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 +147,104 @@ def initial_data_for_feedback_page(
|
||||||
"feedback_type": "vv",
|
"feedback_type": "vv",
|
||||||
}
|
}
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def export_feedback(course_session_ids: list[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__in=course_session_ids,
|
||||||
|
submitted=True,
|
||||||
|
).order_by("circle", "course_session", "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_ids": course_session_ids,
|
||||||
|
"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)
|
||||||
|
|
||||||
|
if len(data) == 0:
|
||||||
|
return sheet
|
||||||
|
|
||||||
|
# we instruct the users not to mix exports of different courses, so we can assume the questions are the same and of the first type
|
||||||
|
question_data = (
|
||||||
|
UK_FEEDBACK_QUESTIONS
|
||||||
|
if data[0].data["feedback_type"] == "uk"
|
||||||
|
else VV_FEEDBACK_QUESTIONS
|
||||||
|
)
|
||||||
|
|
||||||
|
# add header
|
||||||
|
sheet.cell(row=1, column=1, value="Durchführung")
|
||||||
|
sheet.cell(row=1, column=2, value="Datum")
|
||||||
|
questions = [q[1] for q in question_data]
|
||||||
|
for col_idx, title in enumerate(questions, start=3):
|
||||||
|
sheet.cell(row=1, column=col_idx, value=title)
|
||||||
|
|
||||||
|
_add_rows(sheet, data, question_data)
|
||||||
|
return sheet
|
||||||
|
|
||||||
|
|
||||||
|
def _add_rows(sheet, data, question_data):
|
||||||
|
for row_idx, feedback in enumerate(data, start=2):
|
||||||
|
sheet.cell(row=row_idx, column=1, value=feedback.course_session.title)
|
||||||
|
sheet.cell(
|
||||||
|
row=row_idx, column=2, value=feedback.updated_at.date().strftime("%d.%m.%Y")
|
||||||
|
)
|
||||||
|
for col_idx, question in enumerate(question_data, start=3):
|
||||||
|
response = feedback.data.get(question[0], "")
|
||||||
|
sheet.cell(row=row_idx, column=col_idx, value=response)
|
||||||
|
|
||||||
|
|
||||||
|
def make_export_filename(name: str = "feedback_export"):
|
||||||
|
today_date = datetime.today().strftime("%Y-%m-%d")
|
||||||
|
return f"{name}_{today_date}.xlsx"
|
||||||
|
|
||||||
|
|
||||||
|
# used as admin action, that's why it's not in the views.py
|
||||||
|
def get_feedbacks_for_course_sessions(_modeladmin, _request, queryset):
|
||||||
|
file_name = "feedback_export_durchfuehrungen"
|
||||||
|
return _handle_feedback_export_action(queryset, file_name)
|
||||||
|
|
||||||
|
|
||||||
|
def get_feedbacks_for_courses(_modeladmin, _request, queryset):
|
||||||
|
course_sessions = CourseSession.objects.filter(course__in=queryset)
|
||||||
|
file_name = "feedback_export_lehrgaenge"
|
||||||
|
return _handle_feedback_export_action(course_sessions, file_name)
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_feedback_export_action(course_seesions, file_name):
|
||||||
|
excel_bytes = export_feedback(course_seesions, False)
|
||||||
|
|
||||||
|
response = HttpResponse(
|
||||||
|
content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||||
|
)
|
||||||
|
response[
|
||||||
|
"Content-Disposition"
|
||||||
|
] = f"attachment; filename={make_export_filename(file_name)}"
|
||||||
|
response.write(excel_bytes)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,11 @@ from vbv_lernwelt.competence.factories import (
|
||||||
)
|
)
|
||||||
from vbv_lernwelt.competence.models import ActionCompetence
|
from vbv_lernwelt.competence.models import ActionCompetence
|
||||||
from vbv_lernwelt.core.admin import User
|
from vbv_lernwelt.core.admin import User
|
||||||
from vbv_lernwelt.course.consts import COURSE_VERSICHERUNGSVERMITTLERIN_ID
|
from vbv_lernwelt.course.consts import (
|
||||||
|
COURSE_MOTORFAHRZEUG_PRUEFUNG_ID,
|
||||||
|
COURSE_VERSICHERUNGSVERMITTLERIN_ID,
|
||||||
|
COURSE_VERSICHERUNGSVERMITTLERIN_PRUEFUNG_ID,
|
||||||
|
)
|
||||||
from vbv_lernwelt.course.models import CourseCategory, CoursePage
|
from vbv_lernwelt.course.models import CourseCategory, CoursePage
|
||||||
from vbv_lernwelt.learnpath.models import LearningUnitPerformanceFeedbackType
|
from vbv_lernwelt.learnpath.models import LearningUnitPerformanceFeedbackType
|
||||||
from vbv_lernwelt.learnpath.tests.learning_path_factories import (
|
from vbv_lernwelt.learnpath.tests.learning_path_factories import (
|
||||||
|
|
@ -89,7 +93,7 @@ def create_vv_new_learning_path(
|
||||||
|
|
||||||
|
|
||||||
def create_vv_pruefung_learning_path(
|
def create_vv_pruefung_learning_path(
|
||||||
course_id=COURSE_VERSICHERUNGSVERMITTLERIN_ID, user=None
|
course_id=COURSE_VERSICHERUNGSVERMITTLERIN_PRUEFUNG_ID, user=None
|
||||||
):
|
):
|
||||||
if user is None:
|
if user is None:
|
||||||
user = User.objects.get(username="info@iterativ.ch")
|
user = User.objects.get(username="info@iterativ.ch")
|
||||||
|
|
@ -108,6 +112,25 @@ def create_vv_pruefung_learning_path(
|
||||||
Page.objects.update(owner=user)
|
Page.objects.update(owner=user)
|
||||||
|
|
||||||
|
|
||||||
|
def create_vv_motorfahrzeug_pruefung_learning_path(
|
||||||
|
course_id=COURSE_MOTORFAHRZEUG_PRUEFUNG_ID, user=None
|
||||||
|
):
|
||||||
|
if user is None:
|
||||||
|
user = User.objects.get(username="info@iterativ.ch")
|
||||||
|
|
||||||
|
course_page = CoursePage.objects.get(course_id=course_id)
|
||||||
|
lp = LearningPathFactory(
|
||||||
|
title="Lernpfad",
|
||||||
|
parent=course_page,
|
||||||
|
)
|
||||||
|
|
||||||
|
TopicFactory(title="Fahrzeug", parent=lp)
|
||||||
|
create_circle_fahrzeug(lp, course_page=course_page)
|
||||||
|
|
||||||
|
# all pages belong to 'admin' by default
|
||||||
|
Page.objects.update(owner=user)
|
||||||
|
|
||||||
|
|
||||||
def create_circle_basis(lp, title="Basis", course_page=None):
|
def create_circle_basis(lp, title="Basis", course_page=None):
|
||||||
circle = CircleFactory(
|
circle = CircleFactory(
|
||||||
title=title,
|
title=title,
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,14 @@
|
||||||
<a href="{% url 'edoniq_export_students_and_trainers' %}" class="btn btn-primary">Teilnehmer
|
<a href="{% url 'edoniq_export_students_and_trainers' %}" class="btn btn-primary">Teilnehmer
|
||||||
und Trainer exportieren</a>
|
und Trainer exportieren</a>
|
||||||
|
|
||||||
|
<hr style="margin: 24px 0">
|
||||||
|
|
||||||
|
<form action="/api/core/resetiterativsessions/" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<p>Zurücksetzen der Iterativ Testdurchführungen (üK: "Iterativ üK Testkurs", VV: "Iterativ VV Testkurs")</p>
|
||||||
|
<button class="btn">Iterativ Testdurchführungen zurücksetzen</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
<hr style="margin: 24px 0">
|
<hr style="margin: 24px 0">
|
||||||
|
|
||||||
<form action="/api/core/cypressreset/" method="post">
|
<form action="/api/core/cypressreset/" method="post">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue