Skip to content

Commit

Permalink
Added: SocialMediaConfig models, admin form, serializer & frontend in…
Browse files Browse the repository at this point in the history
…tegration (#1166)

* feat: Add SocialMediaConfigForm for managing social media sharing settings

* feat: Add serializer for SocialMediaConfig for ExperimentCollection serializer

* feat: Add SocialMediaConfig interface to ExperimentCollection

* feat: Update ExperimentCollectionDashboard and Header component to include social media sharing settings

* refactor: Alter SocialMediaConfig model to include all the social media / share options supported by the frontend

* refactor: Convert Social component to TypeScript

* chore: Re-create migration after applying the migrations from #1164

* test: Add Social component unit tests

* refactor: Use consistent test id naming

* test: Add test for experiment collection with social media config, thereby also testing the social media config serializer
  • Loading branch information
drikusroor authored Jul 2, 2024
1 parent 79282db commit 865dd72
Show file tree
Hide file tree
Showing 12 changed files with 300 additions and 44 deletions.
22 changes: 18 additions & 4 deletions backend/experiment/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from django.utils import timezone
from django.core import serializers
from django.shortcuts import render, redirect
from django.forms import CheckboxSelectMultiple, ModelForm, TextInput
from django.forms import CheckboxSelectMultiple
from django.http import HttpResponse
from inline_actions.admin import InlineActionsModelAdminMixin
from django.urls import reverse
Expand All @@ -20,14 +20,16 @@
ExperimentCollection,
Phase,
Feedback,
GroupedExperiment
GroupedExperiment,
SocialMediaConfig,
)
from question.admin import QuestionSeriesInline
from experiment.forms import (
ExperimentCollectionForm,
ExperimentForm,
ExportForm,
TemplateForm,
SocialMediaConfigForm,
EXPORT_TEMPLATES,
)
from section.models import Section, Song
Expand Down Expand Up @@ -201,6 +203,12 @@ class PhaseInline(admin.StackedInline):
inlines = [GroupedExperimentInline]


class SocialMediaConfigInline(admin.StackedInline):
form = SocialMediaConfigForm
model = SocialMediaConfig
extra = 0


class ExperimentCollectionAdmin(InlineActionsModelAdminMixin, admin.ModelAdmin):
list_display = ('name', 'slug_link', 'description_excerpt',
'dashboard', 'phases', 'active')
Expand All @@ -209,7 +217,10 @@ class ExperimentCollectionAdmin(InlineActionsModelAdminMixin, admin.ModelAdmin):
'about_content']
inline_actions = ['dashboard']
form = ExperimentCollectionForm
inlines = [PhaseInline]
inlines = [
PhaseInline,
SocialMediaConfigInline,
]

def slug_link(self, obj):
dev_mode = settings.DEBUG is True
Expand Down Expand Up @@ -245,7 +256,10 @@ def dashboard(self, request, obj, parent_obj=None):
'id': exp.id,
'name': exp.name,
'started': len(all_sessions.filter(experiment=exp)),
'finished': len(all_sessions.filter(experiment=exp, finished_at__isnull=False)),
'finished': len(all_sessions.filter(
experiment=exp,
finished_at__isnull=False,
)),
'participant_count': len(exp.current_participants()),
'participants': exp.current_participants()
} for exp in all_experiments]
Expand Down
17 changes: 15 additions & 2 deletions backend/experiment/forms.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from django.forms import CheckboxSelectMultiple, ModelForm, ChoiceField, Form, MultipleChoiceField, ModelMultipleChoiceField, Select, TypedMultipleChoiceField, CheckboxSelectMultiple, TextInput
from experiment.models import ExperimentCollection, Experiment
from django.contrib.postgres.forms import SimpleArrayField
from experiment.models import ExperimentCollection, Experiment, SocialMediaConfig
from experiment.rules import EXPERIMENT_RULES


Expand Down Expand Up @@ -178,7 +179,7 @@ def clean_playlists(self):

if not playlists:
return self.cleaned_data['playlists']

playlist_errors = []

# Validate playlists
Expand Down Expand Up @@ -237,3 +238,15 @@ class TemplateForm(Form):
class QuestionSeriesAdminForm(ModelForm):
class Media:
js = ["questionseries_admin.js"]


class SocialMediaConfigForm(ModelForm):
channels = MultipleChoiceField(
widget=CheckboxSelectMultiple,
choices=SocialMediaConfig.SOCIAL_MEDIA_CHANNELS,
required=False
)

class Meta:
model = SocialMediaConfig
fields = '__all__'
26 changes: 26 additions & 0 deletions backend/experiment/migrations/0041_socialmediaconfig.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Generated by Django 4.2.11 on 2024-07-02 08:46

import django.contrib.postgres.fields
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('experiment', '0040_alter_phase_options_remove_phase_order_phase_index_and_more'),
]

operations = [
migrations.CreateModel(
name='SocialMediaConfig',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('tags', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), blank=True, default=list, help_text='List of tags for social media sharing', size=None)),
('url', models.URLField(blank=True, help_text='URL to be shared on social media. If empty, the experiment URL will be used.')),
('content', models.TextField(blank=True, default='I scored {points} points in {experiment_name}!', help_text='Content for social media sharing. Use {points} and {experiment_name} as placeholders.')),
('channels', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('facebook', 'Facebook'), ('whatsapp', 'WhatsApp'), ('twitter', 'Twitter'), ('weibo', 'Weibo'), ('share', 'Share'), ('clipboard', 'Clipboard')], max_length=20), blank=True, default=list, help_text='Selected social media channels for sharing', size=None)),
('experiment_collection', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='social_media_config', to='experiment.experimentcollection')),
],
),
]
59 changes: 59 additions & 0 deletions backend/experiment/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.contrib.postgres.fields import ArrayField
from typing import List, Dict, Tuple, Any
from experiment.standards.iso_languages import ISO_LANGUAGES
from theme.models import ThemeConfig
from image.models import Image
from session.models import Session
from typing import Optional

from .validators import markdown_html_validator, experiment_slug_validator

Expand Down Expand Up @@ -41,6 +43,7 @@ class ExperimentCollection(models.Model):
dashboard = models.BooleanField(default=False)
about_content = models.TextField(blank=True, default='')
active = models.BooleanField(default=True)
social_media_config: Optional['SocialMediaConfig']

def __str__(self):
return self.name or self.slug
Expand Down Expand Up @@ -321,3 +324,59 @@ def add_default_question_series(self):
class Feedback(models.Model):
text = models.TextField()
experiment = models.ForeignKey(Experiment, on_delete=models.CASCADE)


class SocialMediaConfig(models.Model):
experiment_collection = models.OneToOneField(
ExperimentCollection,
on_delete=models.CASCADE,
related_name='social_media_config'
)

tags = ArrayField(
models.CharField(max_length=100),
blank=True,
default=list,
help_text=_("List of tags for social media sharing")
)

url = models.URLField(
blank=True,
help_text=_("URL to be shared on social media. If empty, the experiment URL will be used.")
)

content = models.TextField(
blank=True,
help_text=_("Content for social media sharing. Use {points} and {experiment_name} as placeholders."),
default="I scored {points} points in {experiment_name}!"
)

SOCIAL_MEDIA_CHANNELS = [
('facebook', _('Facebook')),
('whatsapp', _('WhatsApp')),
('twitter', _('Twitter')),
('weibo', _('Weibo')),
('share', _('Share')),
('clipboard', _('Clipboard')),
]
channels = ArrayField(
models.CharField(max_length=20, choices=SOCIAL_MEDIA_CHANNELS),
blank=True,
default=list,
help_text=_("Selected social media channels for sharing")
)

def get_content(self, score: int = None, experiment_name: str = None):
if self.content:
return self.content

if not score or not experiment_name:
raise ValueError("score and experiment_name are required")

return _("I scored {points} points in {experiment_name}").format(
score=self.experiment_collection.points,
experiment_name=self.experiment_collection.name
)

def __str__(self):
return f"Social Media for {self.experiment_collection.name}"
18 changes: 17 additions & 1 deletion backend/experiment/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from participant.models import Participant
from session.models import Session
from theme.serializers import serialize_theme
from .models import Experiment, ExperimentCollection, Phase, GroupedExperiment
from .models import Experiment, ExperimentCollection, Phase, GroupedExperiment, SocialMediaConfig


def serialize_actions(actions):
Expand Down Expand Up @@ -44,9 +44,25 @@ def serialize_experiment_collection(
filter_name='markdown'
)

if experiment_collection.social_media_config:
serialized['socialMedia'] = serialize_social_media_config(
experiment_collection.social_media_config
)

return serialized


def serialize_social_media_config(
social_media_config: SocialMediaConfig
) -> dict:
return {
'tags': social_media_config.tags,
'url': social_media_config.url,
'content': social_media_config.content,
'channels': social_media_config.channels,
}


def serialize_phase(
phase: Phase,
participant: Participant
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<div class="tab-content active" id="tab-content-edit">
<textarea rows="{{ widget.attrs.rows|default:'16' }}" name="{{ widget.name }}" value="{{ widget.value|default:'' }}" placeholder="{{ widget.placeholder|default:''}}" {% include "django/forms/widgets/attrs.html" %}>{{ widget.value|default:'' }}</textarea>
</div>

<div class="tab-content" id="tab-content-preview">
<markdown-preview id="markdownPreview"></markdown-preview>
</div>
Expand Down Expand Up @@ -65,7 +65,7 @@
display: block;
}

textarea {
.tab-content textarea {
width: 100%;
min-height: 256px !important;
height: 100%;
Expand Down Expand Up @@ -103,7 +103,7 @@
font-family: inherit;
box-sizing: border-box;
}
h1, h2, h3, h4, h5, h6 {
display: block;
color: var(--text-fg);
Expand Down Expand Up @@ -286,8 +286,8 @@

renderMarkdown(textarea, markdownPreview);
});

});

</script>
{% endblock %}
{% endblock %}
21 changes: 19 additions & 2 deletions backend/experiment/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
ExperimentCollection,
Phase,
GroupedExperiment,
SocialMediaConfig,
)
from experiment.rules.hooked import Hooked
from participant.models import Participant
Expand All @@ -28,8 +29,9 @@ def setUpTestData(cls):
collection = ExperimentCollection.objects.create(
name='Test Series',
slug='test_series',
theme_config=theme_config
theme_config=theme_config,
)
collection.social_media_config = create_social_media_config(collection)
introductory_phase = Phase.objects.create(
name='introduction',
series=collection,
Expand Down Expand Up @@ -108,6 +110,10 @@ def test_get_experiment_collection(self):
self.assertEqual(len(response_json['theme']['header']['score']), 3)
self.assertEqual(response_json.get('theme').get('footer').get(
'disclaimer'), '<p>Test Disclaimer</p>')
self.assertEqual(response_json.get('socialMedia').get('url'), 'https://www.example.com')
self.assertEqual(response_json.get('socialMedia').get('content'), 'Test Content')
self.assertEqual(response_json.get('socialMedia').get('tags'), ['aml', 'toontjehoger'])
self.assertEqual(response_json.get('socialMedia').get('channels'), ['facebook', 'twitter', 'weibo'])

def test_get_experiment_collection_not_found(self):
# if ExperimentCollection does not exist, return 404
Expand Down Expand Up @@ -257,7 +263,7 @@ def test_get_experiment(self):
)


def create_theme_config():
def create_theme_config() -> ThemeConfig:
theme_config = ThemeConfig.objects.create(
name='test_theme',
description='Test Theme',
Expand All @@ -278,3 +284,14 @@ def create_theme_config():
footer_config.logos.add(Image.objects.create(file='test-logo.jpg'))

return theme_config


def create_social_media_config(
collection: ExperimentCollection) -> SocialMediaConfig:
return SocialMediaConfig.objects.create(
experiment_collection=collection,
url='https://www.example.com',
content='Test Content',
channels=['facebook', 'twitter', 'weibo'],
tags=['aml', 'toontjehoger']
)
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@ interface ExperimentCollectionDashboardProps {

export const ExperimentCollectionDashboard: React.FC<ExperimentCollectionDashboardProps> = ({ experimentCollection, participantIdUrl, totalScore }) => {

const { dashboard, description, name } = experimentCollection;
const { dashboard, description } = experimentCollection;
const { nextExperimentButtonText, aboutButtonText } = experimentCollection.theme?.header || { nextExperimentButtonText: "", aboutButtonText: "" };

const scoreDisplayConfig = experimentCollection.theme?.header?.score;
const nextBlockSlug = experimentCollection.nextExperiment?.slug;
const showHeader = experimentCollection.theme?.header;
const socialMediaConfig = experimentCollection.socialMediaConfig;

const getExperimentHref = (slug: string) => `/${slug}${participantIdUrl ? `?participant_id=${participantIdUrl}` : ""}`;

Expand All @@ -32,11 +33,11 @@ export const ExperimentCollectionDashboard: React.FC<ExperimentCollectionDashboa
nextBlockSlug={nextBlockSlug}
collectionSlug={experimentCollection.slug}
totalScore={totalScore}
name={name}
description={description}
scoreDisplayConfig={scoreDisplayConfig}
nextBlockButtonText={nextExperimentButtonText}
aboutButtonText={aboutButtonText}
socialMediaConfig={socialMediaConfig}
/>
)}
{/* Experiments */}
Expand Down
Loading

0 comments on commit 865dd72

Please sign in to comment.