Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Course publish without metadata #2015

Closed
wants to merge 15 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions localdev/configs/ocw-course-site-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,11 @@ collections:
- label: "Extra Course Numbers (comma separated list)"
name: "extra_course_numbers"
widget: "string"
- label: "Hide Course Download"
name: "hide_download"
required: true
widget: "boolean"
help: "Enable this setting to hide the course download button"
- label: Course Image
name: course_image
widget: relation
Expand Down
20 changes: 17 additions & 3 deletions static/js/components/PublishDrawer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ describe("PublishDrawer", () => {
urlField: "draft_url",
publishDateField: "draft_publish_date",
publishStatusField: "draft_publish_status",
hasSiteMetaData: "has_site_metadata",
idx: 0,
},
{
Expand All @@ -105,6 +106,7 @@ describe("PublishDrawer", () => {
urlField: "live_url",
publishDateField: "publish_date",
publishStatusField: "live_publish_status",
hasSiteMetaData: "has_site_metadata",
idx: 1,
},
])(
Expand All @@ -117,6 +119,7 @@ describe("PublishDrawer", () => {
urlField,
publishDateField,
publishStatusField,
hasSiteMetaData,
idx,
}) => {
;[true, false].forEach((visible) => {
Expand Down Expand Up @@ -194,6 +197,16 @@ describe("PublishDrawer", () => {
expect(wrapper.find(".btn-publish").prop("disabled")).toBe(true)
})

it("disables publish button in production if no metadata is set", async () => {
website[hasSiteMetaData] = false
const { wrapper } = await render()
await simulateClickPublish(wrapper, action)
wrapper.update()
expect(wrapper.find(".btn-publish").prop("disabled")).toBe(
action === "production",
)
})

it("render only the preview button if user is not an admin", async () => {
website["is_admin"] = false
const { wrapper } = await render()
Expand Down Expand Up @@ -240,9 +253,10 @@ describe("PublishDrawer", () => {
const { wrapper } = await render()
await simulateClickPublish(wrapper, action)
wrapper.update()
expect(
wrapper.find("PublishForm").find(".btn-publish").prop("disabled"),
).toBeFalsy()
website.has_site_metadata &&
expect(
wrapper.find("PublishForm").find(".btn-publish").prop("disabled"),
).toBeFalsy()
await act(async () => {
wrapper.find("PublishForm").find(".btn-publish").simulate("submit")
})
Expand Down
6 changes: 4 additions & 2 deletions static/js/components/PublishDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ const getPublishingInfo = (
const PublishingOption: React.FC<PublishingOptionProps> = (props) => {
const { publishingEnv, selected, onSelect, website, onPublishSuccess } = props
const publishingInfo = getPublishingInfo(website, publishingEnv)

const isPublishDisabled =
!publishingInfo.hasUnpublishedChanges ||
(!website.has_site_metadata && publishingEnv === PublishingEnv.Production)
const [{ isPending }, publish] = useMutation(
(payload: WebsitePublishPayload) =>
websitePublishAction(website.name, publishingEnv, payload),
Expand Down Expand Up @@ -123,7 +125,7 @@ const PublishingOption: React.FC<PublishingOptionProps> = (props) => {
)}
<PublishForm
onSubmit={handlePublish}
disabled={!publishingInfo.hasUnpublishedChanges}
disabled={isPublishDisabled}
website={website}
option={publishingEnv}
/>
Expand Down
7 changes: 7 additions & 0 deletions static/js/resources/ocw-course-site-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,13 @@
"name": "extra_course_numbers",
"widget": "string"
},
{
"help": "Enable this setting to hide the course download button",
"label": "Hide Course Download",
"name": "hide_download",
"required": true,
"widget": "boolean"
},
{
"collection": "resource",
"display_field": "title",
Expand Down
1 change: 1 addition & 0 deletions static/js/types/websites.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ export interface WebsiteStatus {
synced_on: string | null
sync_errors: Array<string> | null
unpublished: boolean
has_site_metadata: boolean
}

export type Website = WebsiteStatus & {
Expand Down
2 changes: 2 additions & 0 deletions static/js/util/factories/websites.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ export const makeWebsiteDetail = (
url_suggestion: "[sitemetadata:primary_course_number]-[sitemetdata:title]",
s3_path: `courses/${casual.word}`,
unpublished: false,
has_site_metadata: false,
...overrides,
})

Expand All @@ -249,6 +250,7 @@ export const makeWebsiteStatus = (
synced_on: null,
sync_errors: null,
unpublished: false,
has_site_metadata: website.has_site_metadata,
}
}

Expand Down
3 changes: 3 additions & 0 deletions websites/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,9 @@ def get_content_warnings(website):

messages = []

if not website.has_site_metadata:
messages.append("The course is missing metadata.")

if len(missing_youtube_ids_titles) > 0:
messages.append(
f"The following video resources require YouTube IDs: {', '.join(missing_youtube_ids_titles)}" # noqa: E501
Expand Down
48 changes: 41 additions & 7 deletions websites/api_test.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
"""Tests for websites API functionality"""

from pathlib import Path
from uuid import UUID

import factory
import pytest
import yaml
from django.conf import settings
from mitol.common.utils import now_in_utc

from content_sync.constants import VERSION_DRAFT, VERSION_LIVE
Expand Down Expand Up @@ -41,6 +44,7 @@
PreviewOrPublishSuccessMessage,
)
from websites.models import Website
from websites.serializers_test import VALID_METADATA

pytestmark = pytest.mark.django_db

Expand Down Expand Up @@ -487,11 +491,31 @@ def test_update_unpublished_website_status(status, version):
@pytest.mark.parametrize("has_missing_captions", [True, False])
@pytest.mark.parametrize("has_truncatable_text", [True, False])
@pytest.mark.parametrize("is_draft", [True, False])
def test_get_content_warnings(
mocker, has_missing_ids, has_missing_captions, has_truncatable_text, is_draft
@pytest.mark.parametrize("valid_metadata", [True, False])
def test_get_content_warnings( # pylint: disable=too-many-arguments # noqa: PLR0913
mocker,
has_missing_ids,
has_missing_captions,
has_truncatable_text,
is_draft,
valid_metadata,
):
"""get_content_warnings should return expected warning messages"""
website = WebsiteFactory.create()
site_config_path = "localdev/configs/ocw-course-site-config.yml"
starter = WebsiteStarterFactory(
config=yaml.load(
(Path(settings.BASE_DIR) / site_config_path).read_text(),
Loader=yaml.SafeLoader,
)
)
website = WebsiteFactory.create(starter=starter)
metadata = {**VALID_METADATA}
if not valid_metadata:
del metadata["course_title"]

WebsiteContentFactory.create(
type="sitemetadata", website=website, metadata=metadata
)
video_content = WebsiteContentFactory.create_batch(3, website=website)
no_yt_ids = video_content[0:2] if has_missing_ids else []
no_caps = video_content[1:3] if has_missing_captions else []
Expand All @@ -515,26 +539,36 @@ def test_get_content_warnings(
)
warnings = get_content_warnings(website)
warnings_len = 0
if not valid_metadata:
warnings_len += 1
if has_missing_ids:
warnings_len += 1
for content in no_yt_ids:
assert content.title in warnings[0]
assert content.title in warnings[1 if not valid_metadata else 0]
if has_missing_captions:
warnings_len += 1
for content in no_caps:
assert content.title in warnings[1 if has_missing_ids else 0]
assert (
content.title
in warnings[int(not valid_metadata) + int(has_missing_ids)]
)
if has_truncatable_text:
warnings_len += 1
assert (
video_content[2].title
in warnings[int(has_missing_ids) + int(has_missing_captions)]
in warnings[
int(not valid_metadata)
+ int(has_missing_ids)
+ int(has_missing_captions)
]
)
if is_draft:
warnings_len += 1
assert len(warnings) == warnings_len
assert video_content[1].title in warnings[warnings_len - 1]
if (
not has_missing_ids
valid_metadata
and not has_missing_ids
and not has_missing_captions
and not has_truncatable_text
and not is_draft
Expand Down
16 changes: 16 additions & 0 deletions websites/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
from websites.site_config_api import ConfigItem, SiteConfig
from websites.utils import (
get_dict_field,
is_metadata_has_required_fields,
permissions_group_name_for_role,
set_dict_field,
)
Expand Down Expand Up @@ -282,6 +283,21 @@ def s3_path(self):
]
return "/".join([part.strip("/") for part in url_parts if part])

@property
def has_site_metadata(self):
"""
Get True when required fields are set in WebsiteContent metadata
otherwise False
"""
site_metadata = self.websitecontent_set.filter(type="sitemetadata")
return bool(
site_metadata
and site_metadata[0].metadata
and is_metadata_has_required_fields(
SiteConfig(self.starter.config), site_metadata[0].metadata
)
)

class Meta:
permissions = (
("publish_website", "Publish or unpublish a website"),
Expand Down
5 changes: 5 additions & 0 deletions websites/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ class WebsiteDetailSerializer(
live_url = serializers.SerializerMethodField(read_only=True)
draft_url = serializers.SerializerMethodField(read_only=True)
unpublished = serializers.ReadOnlyField()
has_site_metadata = serializers.ReadOnlyField()

def get_is_admin(self, obj):
"""Determine if the request user is an admin"""
Expand Down Expand Up @@ -297,6 +298,7 @@ class Meta:
"sync_errors",
"synced_on",
"content_warnings",
"has_site_metadata",
]
read_only_fields = [
"uuid",
Expand Down Expand Up @@ -329,6 +331,8 @@ class WebsiteStatusSerializer(
):
"""Serializer for website status fields"""

has_site_metadata = serializers.ReadOnlyField()

class Meta:
model = Website
fields = [
Expand All @@ -349,6 +353,7 @@ class Meta:
"synced_on",
"content_warnings",
"url_suggestion",
"has_site_metadata",
]
read_only_fields = fields

Expand Down
44 changes: 44 additions & 0 deletions websites/serializers_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
from types import SimpleNamespace

import pytest
import yaml
from dateutil.parser import parse as parse_date
from django.conf import settings
from django.core.files.uploadedfile import SimpleUploadedFile
from django.db.models import CharField, Value
from mitol.common.utils import now_in_utc
Expand Down Expand Up @@ -175,6 +177,48 @@ def test_website_status_serializer(mocker, settings, drive_folder, warnings):
assert serialized_data.get(key) == value


VALID_METADATA = {
"status": True,
"course_title": "example",
"primary_course_number": "1",
"department_numbers": ["3"],
"hide_download": True,
}


@pytest.mark.parametrize(
"missing_field",
[
None,
"course_title",
"primary_course_number",
"department_numbers",
"hide_download",
],
)
@pytest.mark.parametrize(
"serializer_class", [WebsiteDetailSerializer, WebsiteStatusSerializer]
)
def test_website_content_has_metadata(mocker, missing_field, serializer_class):
"""has_site_metadata should be true if we have metadata in WebsiteContent model"""
site_config_path = "localdev/configs/ocw-course-site-config.yml"
starter = WebsiteStarterFactory(
config=yaml.load(
(Path(settings.BASE_DIR) / site_config_path).read_text(),
Loader=yaml.SafeLoader,
)
)
website = WebsiteFactory.create(starter=starter)
metadata = {**VALID_METADATA}
if missing_field:
del metadata[missing_field]
WebsiteContentFactory.create(
type="sitemetadata", website=website, metadata=metadata
)
serialized_data = serializer_class(instance=website).data
assert serialized_data["has_site_metadata"] == bool(not missing_field)


@pytest.mark.parametrize("has_starter", [True, False])
@pytest.mark.parametrize("drive_folder", [None, "abc123"])
@pytest.mark.parametrize("drive_credentials", [None, {"creds: True"}])
Expand Down
28 changes: 28 additions & 0 deletions websites/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from django.db.models import Q

from websites import constants
from websites.site_config_api import SiteConfig


def permissions_group_name_for_role(role, website):
Expand Down Expand Up @@ -96,3 +97,30 @@ def resource_reference_field_filter(

def is_test_site(site_name: str) -> bool:
return site_name in settings.OCW_TEST_SITE_SLUGS


def is_required_or_min(field):
"""
Get True if field have either 'required' property set to True or 'min'
property set to > 0
"""
return ("required" in field or "min" in field) and (
field.get("required") or (field.get("min") and int(field.get("min")) > 0)
)


def get_config_required_fields(config: SiteConfig):
"""Retrieve the 'required' fields in the WebsiteStarter."""
config_item = config.find_item_by_name("metadata")
metadata_fields = config_item.item.get("files")[0].get("fields")
return [field.get("name") for field in metadata_fields if is_required_or_min(field)]


def is_metadata_has_required_fields(config, metadata):
"""Validate the presence of all required fields in the WebsiteContent metadata."""
required_fields = get_config_required_fields(config)
return all(
field in metadata
and ((isinstance(metadata[field], bool) and True) or (bool(metadata[field])))
for field in required_fields
)