Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
79e88e1
Add additional scenarios to gherkin for sync resources workflow
marcellamaki Feb 22, 2021
c576dd1
Replace ResponsiveDialog by KModal
MisRob Apr 7, 2021
6ae713b
Added more granular steps
radinamatic Apr 26, 2021
b797762
Feature name and description
radinamatic Apr 26, 2021
f6b9d0d
Push Copyright year to 2021
sophianyberg Apr 27, 2021
3160648
Add feature_flags field to User modal
bjester Apr 27, 2021
a4cf2c3
Merge pull request #1 from radinamatic/update-gherkin-scenario-syncing
marcellamaki Apr 28, 2021
594b025
Add migration check to tests.
rtibbles Apr 29, 2021
7305303
Feature flag manipulation in Studio admin
bjester Apr 29, 2021
bf1f600
Remove dead code, update comment
bjester Apr 29, 2021
0fa3121
Enhance email regex, add copious examples to ensure it is not too res…
rtibbles Apr 30, 2021
88bf110
Merge pull request #3128 from rtibbles/migrations-ci-check
bjester May 3, 2021
26319f9
Merge pull request #3130 from rtibbles/email_validation
rtibbles May 3, 2021
0c39552
Fix feature flag validation exceptions and test
bjester May 3, 2021
eade831
Add description and way to exclude feature from non-dev
bjester May 3, 2021
42bcc11
Fix python requirements
bjester May 3, 2021
edd44c7
Fix loading of static files through Jest
bjester May 3, 2021
1ae860b
Add to garbage_collect command cleanup for feature flags
bjester May 3, 2021
9ee7563
Fix admin Vuex test
bjester May 3, 2021
d889c0c
Fix python test and ensure no additional properties are allowed + com…
bjester May 3, 2021
b5e457e
Use setData for model fetches as well as collection fetches.
rtibbles May 3, 2021
9dde3fd
Merge pull request #3129 from bjester/feature-flags
rtibbles May 3, 2021
8dc7464
Bump rsa from 4.1 to 4.7
dependabot[bot] May 3, 2021
4b74a84
Merge pull request #3131 from learningequality/dependabot/pip/rsa-4.7
rtibbles May 4, 2021
b7be4bd
Merge pull request #3132 from rtibbles/set_data_model
bjester May 4, 2021
28a2d18
Add web format
sairina Feb 2, 2021
acaaf32
Add translated webm strings
sairina Feb 2, 2021
bd245cc
Add test for ThumbnailGenerator.vue
sairina Feb 17, 2021
1320a7f
Add test for FilePreview for webm files
sairina Feb 17, 2021
f7ea6f2
Add test for uploader for webm
sairina Feb 17, 2021
c786b43
Add webm format to test
sairina Feb 17, 2021
56e3ab2
Add le-utils version 0.1.27
sairina Feb 19, 2021
966baaf
Fix linting
sairina Feb 19, 2021
53dbba7
Complete le-utils upgrade
sairina Feb 19, 2021
20f28f7
Fix linting
sairina Feb 19, 2021
ff76194
Requirements update
bjester May 4, 2021
c5b24dd
Resolve migration conflicts
bjester May 4, 2021
07fb3ca
Add migrations for format preset changes
bjester May 4, 2021
f164515
Merge pull request #2973 from sairina/webm
bjester May 4, 2021
0e9d222
Bump rsa from 4.0 to 4.7
dependabot[bot] May 4, 2021
f2332f6
User-scoped channel quiz feature
bjester May 4, 2021
dcd5729
Change copy
bjester May 4, 2021
ba5b51d
Linting fixes
bjester May 4, 2021
e1ac641
Merge pull request #3137 from bjester/channel-quiz
rtibbles May 5, 2021
f0f4f66
Merge pull request #3136 from learningequality/dependabot/pip/rsa-4.7
rtibbles May 5, 2021
64fc46a
Bump lodash from 4.17.19 to 4.17.21
dependabot[bot] May 8, 2021
58bc34f
Merge pull request #3139 from learningequality/dependabot/npm_and_yar…
rtibbles May 8, 2021
7f9f17e
Bump hosted-git-info from 2.8.8 to 2.8.9
dependabot[bot] May 10, 2021
d3054cc
Merge pull request #3116 from sophianyberg/patch-1
bjester May 10, 2021
80cf7f2
Merge pull request #3140 from learningequality/dependabot/npm_and_yar…
bjester May 10, 2021
88308b4
Merge pull request #3075 from MisRob/kds-kmodal-responsivedialog-info…
bjester May 10, 2021
6514720
Fix merge conflict in User viewset
bjester May 10, 2021
979a544
Merge pull request #3141 from bjester/merge-down-prerelease-2021-05-10
rtibbles May 10, 2021
6d50d19
Upgrade le utils for QTI preset.
rtibbles May 11, 2021
8f72bce
Merge pull request #3143 from rtibbles/leutils0.1.30
bjester May 11, 2021
7463483
Merge pull request #3142 from bjester/sprint-release-2021-05-10
rtibbles May 12, 2021
228ca86
Merge pull request #2983 from marcellamaki/update-gherkin-scenario-sy…
marcellamaki May 13, 2021
9c52517
Check if user is anonymous before checking if admin.
rtibbles May 17, 2021
0c35528
Merge pull request #3151 from rtibbles/is_admin_is_anon
bjester May 17, 2021
11b5324
Copy translations for string
bjester May 18, 2021
73d11c7
Merge pull request #3154 from bjester/sprint-release-2021-05-10-strings
bjester May 18, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/pythontest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,6 @@ jobs:
pip install pip-tools
pip-sync requirements.txt requirements-dev.txt
- name: Test pytest
run: pytest --cov-report=xml --cov=./
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1
run: |
sh -c './contentcuration/manage.py makemigrations --check'
pytest
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
The MIT License (MIT)

Copyright (c) 2015 Foundation for Learning Equality (internal apps)
Copyright (c) 2021 Foundation for Learning Equality (internal apps)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
Empty file.
29 changes: 29 additions & 0 deletions contentcuration/contentcuration/constants/feature_flags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import json
import os

import jsonschema
from django.core.exceptions import ValidationError


def _schema():
"""
Loads JSON schema file
"""
file = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../static/feature_flags.json')
with open(file) as f:
data = json.load(f)
return data


SCHEMA = _schema()


def validate(data):
"""
:param data: Dictionary of data to validate
:raises: ValidationError: When invalid
"""
try:
jsonschema.validate(instance=data, schema=SCHEMA)
except jsonschema.ValidationError as e:
raise ValidationError("Invalid feature flags data") from e
20 changes: 20 additions & 0 deletions contentcuration/contentcuration/db/models/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,23 @@ class ArrayRemove(Func):
"""
function = "ARRAY_REMOVE"
arity = 2


class JSONObjectKeys(Func):
"""
Returns result set of all JSON object keys for a JSONB field

Example:
.annotate(
my_field=JSONObjectKeys("some_jsonb_col_name")
)
.values("my_field")
.distinct()
=> my_field
-------------
key1
other_key
...
"""
function = "JSONB_OBJECT_KEYS"
arity = 1
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,42 @@
</p>
<UserStorage :value="user.disk_space" :userId="userId" />

<!-- Feature flags -->
<h2 class="mb-2 mt-5">
Feature flags
</h2>
<VDataTable
:headers="featureFlagHeaders"
:items="featureFlags"
class="user-table"
hide-actions
>
<template #items="{ item }">
<tr>
<td>{{ item.title }}</td>
<td>{{ item.description }}</td>
<td>
<KSwitch
:value="featureFlagValue(item.key)"
:disabled="loading"
title="Toggle feature"
@input="handleFeatureFlagChange(item.key, $event)"
/>
</td>
</tr>
</template>
</VDataTable>

<!-- Policies -->
<h2 class="mb-2 mt-5">
Policies accepted
</h2>
<VDataTable :headers="policyHeaders" :items="policies" hide-actions>
<VDataTable
:headers="policyHeaders"
:items="policies"
class="user-table"
hide-actions
>
<template #items="{ item }">
<tr>
<td>{{ item.name }}</td>
Expand Down Expand Up @@ -139,7 +170,12 @@
import FullscreenModal from 'shared/views/FullscreenModal';
import DetailsRow from 'shared/views/details/DetailsRow';
import Banner from 'shared/views/Banner';
import { createPolicyKey, policyDates, requiredPolicies } from 'shared/constants';
import {
createPolicyKey,
policyDates,
requiredPolicies,
FeatureFlagsSchema,
} from 'shared/constants';

function getPolicyDate(dateString) {
const [date, time] = dateString.split(' ');
Expand Down Expand Up @@ -270,6 +306,29 @@
{ text: 'Signed on', align: 'left', sortable: false },
];
},
featureFlags() {
return Object.entries(FeatureFlagsSchema.properties)
.map(([key, schema]) => ({
key,
...schema,
}))
.filter(featureFlag => {
// Exclude those with `$env` flag that doesn't match current env
return !featureFlag['$env'] || featureFlag['$env'] === process.env.NODE_ENV;
});
},
featureFlagHeaders() {
return [
{ text: 'Feature', align: 'left', sortable: false },
{ text: 'Description', align: 'left', sortable: false },
{ text: 'Visibility', align: 'left', sortable: false },
];
},
featureFlagValue() {
return function(key) {
return this.loading ? false : this.details.feature_flags[key] || false;
};
},
},
beforeRouteEnter(to, from, next) {
next(vm => {
Expand All @@ -283,7 +342,7 @@
return this.load();
},
methods: {
...mapActions('userAdmin', ['loadUser', 'loadUserDetails']),
...mapActions('userAdmin', ['loadUser', 'loadUserDetails', 'updateUser']),
channelUrl(channel) {
return window.Urls.channel(channel.id);
},
Expand All @@ -309,6 +368,31 @@
this.$store.dispatch('errors/handleAxiosError', error);
});
},
handleFeatureFlagChange(key, value) {
// Don't try to update on server if it hasn't changed
if (Boolean(this.details.feature_flags[key]) === value) {
return;
}

// Merge current state
const update = { [key]: value };
this.details.feature_flags = {
...(this.details.feature_flags || {}),
...update,
};

// Only send updated values since it will require validation and lingering
// flags could exist in user's flags data
return this.updateUser({
id: this.userId,
feature_flags: update,
}).then(() => {
this.$store.dispatch(
'showSnackbarSimple',
value ? 'Feature enabled' : 'Feature disabled'
);
});
},
},
};

Expand All @@ -317,4 +401,8 @@

<style lang="less" scoped>

.user-table {
max-width: 1024px;
}

</style>
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ describe('user admin actions', () => {
});
it('loadUserDetails should call client.get with get_user_details', () => {
return store.dispatch('userAdmin/loadUserDetails', userId).then(() => {
expect(client.get).toHaveBeenCalledWith('get_user_details');
expect(client.get).toHaveBeenCalledWith('admin_users_metadata');
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function loadUser(context, id) {
}

export function loadUserDetails(context, id) {
return client.get(window.Urls.get_user_details(id)).then(response => {
return client.get(window.Urls.admin_users_metadata(id)).then(response => {
return response.data;
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,14 @@
:label="$tr('randomizeQuestionLabel')"
:indeterminate="!isUnique(randomizeOrder)"
/>

<!-- Feature flag: Channel quizzes -->
<Checkbox
v-if="allowChannelQuizzes"
v-model="channelQuiz"
:label="$tr('channelQuizzesLabel')"
:indeterminate="!oneSelected"
/>
</VFlex>
</VLayout>

Expand Down Expand Up @@ -308,7 +316,7 @@
import VisibilityDropdown from 'shared/views/VisibilityDropdown';
import Checkbox from 'shared/views/form/Checkbox';
import { ContentKindsNames } from 'shared/leUtils/ContentKinds';
import { NEW_OBJECT } from 'shared/constants';
import { NEW_OBJECT, FeatureFlagKeys, ContentModalities } from 'shared/constants';

// Define an object to act as the place holder for non unique values.
const nonUniqueValue = {};
Expand Down Expand Up @@ -479,6 +487,16 @@
},
},
thumbnailEncoding: generateGetterSetter('thumbnail_encoding'),
channelQuiz: {
get() {
const options = this.getExtraFieldsValueFromNodes('options') || {};
return options.modality === ContentModalities.QUIZ;
},
set(val) {
const options = { modality: val ? ContentModalities.QUIZ : null };
this.updateExtraFields({ options });
},
},

/* COMPUTED PROPS */
disableAuthEdits() {
Expand Down Expand Up @@ -521,6 +539,9 @@
newContent() {
return !this.nodes.some(n => n[NEW_OBJECT]);
},
allowChannelQuizzes() {
return this.$store.getters.hasFeatureEnabled(FeatureFlagKeys.channel_quizzes);
},
},
watch: {
nodes: {
Expand Down Expand Up @@ -656,6 +677,7 @@
tagsLabel: 'Tags',
noTagsFoundText: 'No results found for "{text}". Press \'Enter\' key to create a new tag',
randomizeQuestionLabel: 'Randomize question order for learners',
channelQuizzesLabel: 'Allow as a channel quiz',
},
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@
);
},
isVideo() {
return this.file.file_format === 'mp4';
return this.file.file_format === 'mp4' || this.file.file_format === 'webm';
},
isAudio() {
return this.file.file_format === 'mp3';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ describe('filePreview', () => {
expect(wrapper.vm.isPreviewable).toBe(previewable);
}
test('mp4', true);
test('webm', true);
test('pdf', true);
test('mp3', true);
test('epub', true);
Expand All @@ -55,5 +56,10 @@ describe('filePreview', () => {
wrapper.setData({ fullscreen: true });
wrapper.find('[data-test="closefullscreen"]').trigger('click');
expect(wrapper.vm.fullscreen).toBe(false);

let wrapperWebm = makeWrapper({ file_format: 'webm' });
wrapperWebm.setData({ fullscreen: true });
wrapperWebm.find('[data-test="closefullscreen"]').trigger('click');
expect(wrapperWebm.vm.fullscreen).toBe(false);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ describe('thumbnailGenerator', () => {
videoWrapper.vm.generate();
expect(generateVideoThumbnail).toHaveBeenCalled();

let videoWrapperWebm = makeWrapper('test.webm');
videoWrapperWebm.setMethods({ fileExists, generateVideoThumbnail });
videoWrapperWebm.vm.generate();
expect(generateVideoThumbnail).toHaveBeenCalled();

let generateAudioThumbnail = jest.fn();
let audioWrapper = makeWrapper('test.mp3');
audioWrapper.setMethods({ generateAudioThumbnail });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
return this.width / ASPECT_RATIO;
},
isVideo() {
return this.filePath.endsWith('mp4');
return this.filePath.endsWith('mp4') || this.filePath.endsWith('webm');
},
isAudio() {
return this.filePath.endsWith('mp3');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,9 @@ function generateContentNodeData({
if (extra_fields.randomize !== undefined) {
contentNodeData.extra_fields.randomize = extra_fields.randomize;
}
if (extra_fields.options) {
contentNodeData.extra_fields.options = extra_fields.options;
}
}
if (prerequisite !== NOVALUE) {
contentNodeData.prerequisite = prerequisite;
Expand All @@ -282,11 +285,21 @@ export function updateContentNode(context, { id, ...payload } = {}) {

// Don't overwrite existing extra_fields data
if (contentNodeData.extra_fields) {
const extraFields = node.extra_fields || {};

// Don't overwrite existing options data
if (contentNodeData.extra_fields.options) {
contentNodeData.extra_fields.options = {
...(extraFields.options || {}),
...contentNodeData.extra_fields.options,
};
}

contentNodeData = {
...contentNodeData,
extra_fields: {
...(node.extra_fields || {}),
...(contentNodeData.extra_fields || {}),
...extraFields,
...contentNodeData.extra_fields,
},
};
}
Expand Down
16 changes: 16 additions & 0 deletions contentcuration/contentcuration/frontend/shared/constants.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import featureFlagsSchema from 'static/feature_flags.json';

export const ContentDefaults = {
author: 'author',
provider: 'provider',
Expand Down Expand Up @@ -152,3 +154,17 @@ export const ValidationErrors = {
NO_VALID_PRIMARY_FILES: 'NO_VALID_PRIMARY_FILES',
...fileErrors,
};

export const FeatureFlagsSchema = featureFlagsSchema;

export const FeatureFlagKeys = Object.keys(FeatureFlagsSchema.properties).reduce(
(featureFlags, featureFlag) => {
featureFlags[featureFlag] = featureFlag;
return featureFlags;
},
{}
);

export const ContentModalities = {
QUIZ: 'QUIZ',
};
Loading