Skip to content

Experiment admin

experiment.admin#

ExperimentAdmin #

Bases: InlineActionsModelAdminMixin, NestedModelAdmin

Source code in experiment/admin.py
class ExperimentAdmin(InlineActionsModelAdminMixin, NestedModelAdmin):
    list_display = (
        "name",
        "slug_link",
        "remarks",
        "active",
    )
    fields = [
        "slug",
        "active",
        "theme_config",
    ]
    inline_actions = ["experimenter_dashboard", "duplicate"]
    form = ExperimentForm
    inlines = [
        ExperimentTranslatedContentInline,
        PhaseInline,
        SocialMediaConfigInline,
    ]

    class Media:
        css = {"all": ("experiment_admin.css",)}

    def name(self, obj):
        content = obj.get_fallback_content()

        return content.name if content else "No name"

    def redirect_to_overview(self):
        return redirect(reverse("admin:experiment_experiment_changelist"))

    def slug_link(self, obj):
        dev_mode = settings.DEBUG is True
        url = f"http://localhost:3000/{obj.slug}" if dev_mode else f"/{obj.slug}"

        return format_html(
            f'<a href="{url}" target="_blank" rel="noopener noreferrer" title="Open {obj.slug} experiment group in new tab" >{obj.slug}&nbsp;<small>&#8599;</small></a>'
        )

    slug_link.short_description = "Slug"

    def duplicate(self, request, obj, parent_obj=None):
        """Duplicate an experiment"""

        if "_duplicate" in request.POST:
            # Get slug from the form
            extension = request.POST.get("slug-extension")
            if extension == "":
                extension = "copy"
            slug_extension = f"-{extension}"

            # Validate slug
            if not extension.isalnum():
                messages.add_message(
                    request,
                    messages.ERROR,
                    f"{extension} is nog a valid slug extension. Only alphanumeric characters are allowed.",
                )
            if extension.lower() != extension:
                messages.add_message(
                    request,
                    messages.ERROR,
                    f"{extension} is nog a valid slug extension. Only lowercase characters are allowed.",
                )
            # Check for duplicate slugs
            for exp in Experiment.objects.all():
                if exp.slug == f"{obj.slug}{slug_extension}":
                    messages.add_message(
                        request,
                        messages.ERROR,
                        f"An experiment with slug: {obj.slug}{slug_extension} already exists. Please choose a different slug extension.",
                    )
            for as_block in obj.associated_blocks():
                for block in Block.objects.all():
                    if f"{as_block.slug}{slug_extension}" == block.slug:
                        messages.add_message(
                            request,
                            messages.ERROR,
                            f"A block with slug: {block.slug}{slug_extension} already exists. Please choose a different slug extension.",
                        )
            # Return to form with error messages
            if len(messages.get_messages(request)) != 0:
                return render(
                    request,
                    "duplicate-experiment.html",
                    context={"exp": obj},
                )

            # order_by is inserted here to prevent a query error
            exp_contents = obj.translated_content.order_by("name").all()
            # order_by is inserted here to prevent a query error
            exp_phases = obj.phases.order_by("index").all()

            # Duplicate Experiment object
            exp_copy = obj
            exp_copy.pk = None
            exp_copy._state.adding = True
            exp_copy.slug = f"{obj.slug}{slug_extension}"
            exp_copy.save()

            # Duplicate experiment translated content objects
            for content in exp_contents:
                exp_content_copy = content
                exp_content_copy.pk = None
                exp_content_copy._state.adding = True
                exp_content_copy.experiment = exp_copy
                exp_content_copy.save()

            # Duplicate phases
            for phase in exp_phases:
                these_blocks = Block.objects.filter(phase=phase)

                phase_copy = phase
                phase_copy.pk = None
                phase_copy._state.adding = True
                phase_copy.save()

                # Duplicate blocks in this phase
                for block in these_blocks:
                    # order_by is inserted here to prevent a query error
                    block_contents = block.translated_contents.order_by('name').all()
                    these_playlists = block.playlists.all()
                    question_series = QuestionSeries.objects.filter(block=block)

                    block_copy = block
                    block_copy.pk = None
                    block_copy._state.adding = True
                    block_copy.slug = f"{block.slug}{slug_extension}"
                    block_copy.phase = phase_copy
                    block_copy.save()
                    block_copy.playlists.set(these_playlists)

                    # Duplicate Block translated content objects
                    for content in block_contents:
                        block_content_copy = content
                        block_content_copy.pk = None
                        block_content_copy._state.adding = True
                        block_content_copy.block = block_copy
                        block_content_copy.save()

                    # Duplicate the Block QuestionSeries
                    for series in question_series:
                        all_in_series = QuestionInSeries.objects.filter(
                            question_series=series
                        )
                        these_questions = series.questions.all()
                        series_copy = series
                        series_copy.pk = None
                        series_copy._state.adding = True
                        series_copy.block = block_copy
                        series_copy.index = block.index
                        series_copy.save()

                        # Duplicate the QuestionSeries QuestionInSeries
                        for in_series in all_in_series:
                            in_series_copy = in_series
                            in_series_copy.pk = None
                            in_series_copy._state.adding = True
                            in_series_copy.question_series = series
                            in_series_copy.save()
                        series_copy.questions.set(these_questions)

            return self.redirect_to_overview()

        # Go back to experiment overview
        if "_back" in request.POST:
            return self.redirect_to_overview()

        # Show experiment duplicate form
        return render(
            request,
            "duplicate-experiment.html",
            context={"exp": obj},
        )

    def experimenter_dashboard(self, request, obj, parent_obj=None):
        """Open researchers dashboard for an experiment"""

        if "_export" in request.POST:
            blockId = request.POST.get("export-block")
            return export_json_results(blockId)

        all_blocks = obj.associated_blocks()
        all_participants = obj.current_participants()
        all_sessions = obj.export_sessions()
        all_feedback = obj.export_feedback()
        collect_data = {
            "participant_count": len(all_participants),
            "session_count": len(all_sessions),
            "feedback_count": len(all_feedback),
        }

        blocks = [
            {
                "id": block.id,
                "slug": block.slug,
                "name": block,
                "started": len(all_sessions.filter(block=block)),
                "finished": len(
                    all_sessions.filter(
                        block=block,
                        finished_at__isnull=False,
                    )
                ),
                "participant_count": len(block.current_participants()),
                "participants": block.current_participants(),
            }
            for block in all_blocks
        ]

        return render(
            request,
            "experiment-dashboard.html",
            context={
                "experiment": obj,
                "blocks": blocks,
                "sessions": all_sessions,
                "participants": all_participants,
                "feedback": all_feedback,
                "collect_data": collect_data,
            },
        )

    def remarks(self, obj):
        remarks_array = []

        # Check if there is any translated content
        content = obj.translated_content.first()

        if not content:
            remarks_array.append(
                {
                    "level": "warning",
                    "message": "📝 No content",
                    "title": "Add content in at least one language to this experiment.",
                }
            )

        has_consent = (
            obj.translated_content.filter(consent__isnull=False)
            .exclude(consent="")
            .first()
        )

        if not has_consent:
            remarks_array.append(
                {
                    "level": "info",
                    "message": "📋 No consent form",
                    "title": "You may want to add a consent form (approved by an ethical board) to this experiment.",
                }
            )

        missing_content_block_translations = check_missing_translations(obj)

        if missing_content_block_translations:
            remarks_array.append(
                {
                    "level": "warning",
                    "message": "🌍 Missing block content",
                    "title": missing_content_block_translations,
                }
            )

        if not remarks_array:
            remarks_array.append(
                {
                    "level": "success",
                    "message": "✅ All good",
                    "title": "No issues found.",
                }
            )

        # TODO: Check if all theme configs support the same languages as the experiment
        # Implement this when the theme configs have been updated to support multiple languages

        # TODO: Check if all social media configs support the same languages as the experiment
        # Implement this when the social media configs have been updated to support multiple languages

        return format_html(
            "\n".join(
                [
                    f'<span class="badge badge-{remark["level"]} whitespace-nowrap text-xs mt-1" title="{remark.get("title") if remark.get("title") else remark["message"]}">{remark["message"]}</span><br>'
                    for remark in remarks_array
                ]
            )
        )

    def save_model(self, request, obj, form, change):
        # Save the model
        super().save_model(request, obj, form, change)

        # Check for missing translations after saving
        missing_content_blocks = get_missing_content_blocks(obj)

        if missing_content_blocks:
            for block, missing_languages in missing_content_blocks:
                missing_language_flags = [
                    get_flag_emoji(language) for language in missing_languages
                ]
                self.message_user(
                    request,
                    f"Block {block.slug} does not have content in {', '.join(missing_language_flags)}",
                    level=messages.WARNING,
                )

duplicate(request, obj, parent_obj=None) #

Duplicate an experiment

Source code in experiment/admin.py
def duplicate(self, request, obj, parent_obj=None):
    """Duplicate an experiment"""

    if "_duplicate" in request.POST:
        # Get slug from the form
        extension = request.POST.get("slug-extension")
        if extension == "":
            extension = "copy"
        slug_extension = f"-{extension}"

        # Validate slug
        if not extension.isalnum():
            messages.add_message(
                request,
                messages.ERROR,
                f"{extension} is nog a valid slug extension. Only alphanumeric characters are allowed.",
            )
        if extension.lower() != extension:
            messages.add_message(
                request,
                messages.ERROR,
                f"{extension} is nog a valid slug extension. Only lowercase characters are allowed.",
            )
        # Check for duplicate slugs
        for exp in Experiment.objects.all():
            if exp.slug == f"{obj.slug}{slug_extension}":
                messages.add_message(
                    request,
                    messages.ERROR,
                    f"An experiment with slug: {obj.slug}{slug_extension} already exists. Please choose a different slug extension.",
                )
        for as_block in obj.associated_blocks():
            for block in Block.objects.all():
                if f"{as_block.slug}{slug_extension}" == block.slug:
                    messages.add_message(
                        request,
                        messages.ERROR,
                        f"A block with slug: {block.slug}{slug_extension} already exists. Please choose a different slug extension.",
                    )
        # Return to form with error messages
        if len(messages.get_messages(request)) != 0:
            return render(
                request,
                "duplicate-experiment.html",
                context={"exp": obj},
            )

        # order_by is inserted here to prevent a query error
        exp_contents = obj.translated_content.order_by("name").all()
        # order_by is inserted here to prevent a query error
        exp_phases = obj.phases.order_by("index").all()

        # Duplicate Experiment object
        exp_copy = obj
        exp_copy.pk = None
        exp_copy._state.adding = True
        exp_copy.slug = f"{obj.slug}{slug_extension}"
        exp_copy.save()

        # Duplicate experiment translated content objects
        for content in exp_contents:
            exp_content_copy = content
            exp_content_copy.pk = None
            exp_content_copy._state.adding = True
            exp_content_copy.experiment = exp_copy
            exp_content_copy.save()

        # Duplicate phases
        for phase in exp_phases:
            these_blocks = Block.objects.filter(phase=phase)

            phase_copy = phase
            phase_copy.pk = None
            phase_copy._state.adding = True
            phase_copy.save()

            # Duplicate blocks in this phase
            for block in these_blocks:
                # order_by is inserted here to prevent a query error
                block_contents = block.translated_contents.order_by('name').all()
                these_playlists = block.playlists.all()
                question_series = QuestionSeries.objects.filter(block=block)

                block_copy = block
                block_copy.pk = None
                block_copy._state.adding = True
                block_copy.slug = f"{block.slug}{slug_extension}"
                block_copy.phase = phase_copy
                block_copy.save()
                block_copy.playlists.set(these_playlists)

                # Duplicate Block translated content objects
                for content in block_contents:
                    block_content_copy = content
                    block_content_copy.pk = None
                    block_content_copy._state.adding = True
                    block_content_copy.block = block_copy
                    block_content_copy.save()

                # Duplicate the Block QuestionSeries
                for series in question_series:
                    all_in_series = QuestionInSeries.objects.filter(
                        question_series=series
                    )
                    these_questions = series.questions.all()
                    series_copy = series
                    series_copy.pk = None
                    series_copy._state.adding = True
                    series_copy.block = block_copy
                    series_copy.index = block.index
                    series_copy.save()

                    # Duplicate the QuestionSeries QuestionInSeries
                    for in_series in all_in_series:
                        in_series_copy = in_series
                        in_series_copy.pk = None
                        in_series_copy._state.adding = True
                        in_series_copy.question_series = series
                        in_series_copy.save()
                    series_copy.questions.set(these_questions)

        return self.redirect_to_overview()

    # Go back to experiment overview
    if "_back" in request.POST:
        return self.redirect_to_overview()

    # Show experiment duplicate form
    return render(
        request,
        "duplicate-experiment.html",
        context={"exp": obj},
    )

experimenter_dashboard(request, obj, parent_obj=None) #

Open researchers dashboard for an experiment

Source code in experiment/admin.py
def experimenter_dashboard(self, request, obj, parent_obj=None):
    """Open researchers dashboard for an experiment"""

    if "_export" in request.POST:
        blockId = request.POST.get("export-block")
        return export_json_results(blockId)

    all_blocks = obj.associated_blocks()
    all_participants = obj.current_participants()
    all_sessions = obj.export_sessions()
    all_feedback = obj.export_feedback()
    collect_data = {
        "participant_count": len(all_participants),
        "session_count": len(all_sessions),
        "feedback_count": len(all_feedback),
    }

    blocks = [
        {
            "id": block.id,
            "slug": block.slug,
            "name": block,
            "started": len(all_sessions.filter(block=block)),
            "finished": len(
                all_sessions.filter(
                    block=block,
                    finished_at__isnull=False,
                )
            ),
            "participant_count": len(block.current_participants()),
            "participants": block.current_participants(),
        }
        for block in all_blocks
    ]

    return render(
        request,
        "experiment-dashboard.html",
        context={
            "experiment": obj,
            "blocks": blocks,
            "sessions": all_sessions,
            "participants": all_participants,
            "feedback": all_feedback,
            "collect_data": collect_data,
        },
    )