Skip to content
Open
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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ migrate:
# 4) Remove the management command from this `deploy-migrate` recipe
# 5) Repeat!
deploy-migrate:
echo "Nothing to do here!"
python contentcuration/manage.py backfill_channel_license_audits
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, we don't need to run this migration in the scope of this PR, we will handle it in #5593! 👐


contentnodegc:
python contentcuration/manage.py garbage_collect
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
import { computed, ref } from 'vue';
import { computed } from 'vue';

const MOCK_DEFAULTS = {
isLoading: computed(() => false),
isFinished: computed(() => true),
invalidLicenses: computed(() => []),
specialPermissions: computed(() => []),
includedLicenses: computed(() => []),
isAuditing: ref(false),
hasAuditData: computed(() => false),
auditTaskId: ref(null),
error: ref(null),
checkAndTriggerAudit: jest.fn(),
triggerAudit: jest.fn(),
fetchPublishedData: jest.fn(),
};

export function useLicenseAuditMock(overrides = {}) {
Expand Down
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that we are getting this data from the versionDetail object, we can just remove this whole composable, and extract the information directly on the SubmitToCommunityLibrary side panel component.

Original file line number Diff line number Diff line change
@@ -1,128 +1,51 @@
import { computed, ref, unref, watch } from 'vue';
import { Channel } from 'shared/data/resources';

export function useLicenseAudit(channelRef, channelVersionRef) {
const isAuditing = ref(false);
const auditTaskId = ref(null);
const auditError = ref(null);
const publishedData = ref(null);

watch(
() => unref(channelRef)?.published_data,
newPublishedData => {
if (newPublishedData) {
publishedData.value = newPublishedData;
if (isAuditing.value) {
isAuditing.value = false;
auditError.value = null;
}
}
},
{ immediate: true, deep: true },
);

const currentVersionData = computed(() => {
const version = unref(channelVersionRef);
if (!publishedData.value || version == null) {
return undefined;
}
return publishedData.value[version];
});
import { computed, unref } from 'vue';

export function useLicenseAudit(versionDetailRef, isLoadingRef, isFinishedRef) {
const hasAuditData = computed(() => {
const versionData = currentVersionData.value;
const versionData = unref(versionDetailRef);
if (!versionData) {
return false;
}

return (
'non_distributable_licenses_included' in versionData &&
'special_permissions_included' in versionData
'non_distributable_included_licenses' in versionData &&
'included_special_permissions' in versionData
);
});

const invalidLicenses = computed(() => {
const versionData = currentVersionData.value;
return versionData?.non_distributable_licenses_included || [];
const versionData = unref(versionDetailRef);
return versionData?.non_distributable_included_licenses || [];
});

const specialPermissions = computed(() => {
const versionData = currentVersionData.value;
return versionData?.special_permissions_included || [];
const versionData = unref(versionDetailRef);
return versionData?.included_special_permissions || [];
});

const includedLicenses = computed(() => {
const versionData = currentVersionData.value;
const versionData = unref(versionDetailRef);
return versionData?.included_licenses || [];
});

const isAuditComplete = computed(() => {
return publishedData.value !== null && hasAuditData.value;
});

async function triggerAudit() {
if (isAuditing.value) return;

try {
isAuditing.value = true;
auditError.value = null;

const channelId = unref(channelRef)?.id;
if (!channelId) {
throw new Error('Channel ID is required to trigger audit');
}

const response = await Channel.auditLicenses(channelId);
auditTaskId.value = response.task_id;
} catch (error) {
isAuditing.value = false;
auditError.value = error;
throw error;
}
}

async function fetchPublishedData() {
const channelId = unref(channelRef)?.id;
if (!channelId) return;

try {
const data = await Channel.getPublishedData(channelId);
publishedData.value = data;
} catch (error) {
auditError.value = error;
throw error;
}
}

async function checkAndTriggerAudit() {
if (!publishedData.value) {
await fetchPublishedData();
}

if (hasAuditData.value || isAuditing.value) {
return;
}

await triggerAudit();
}

return {
isLoading: computed(() => {
if (isAuditComplete.value || auditError.value) return false;
return isAuditing.value;
const loading = unref(isLoadingRef);
if (typeof loading === 'boolean') {
return loading;
}
return !unref(versionDetailRef);
}),
isFinished: computed(() => {
Comment on lines +33 to +39
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we won't need to care about isLoading anymore, as this is the same loading variable than the one we load in the general loading data state.

const finished = unref(isFinishedRef);
if (typeof finished === 'boolean') {
return finished;
}
return Boolean(unref(versionDetailRef));
}),
isFinished: computed(() => isAuditComplete.value),
isAuditing,
invalidLicenses,
specialPermissions,
includedLicenses,
hasAuditData,
auditTaskId,
error: auditError,

checkAndTriggerAudit,
triggerAudit,
fetchPublishedData,
currentVersionData,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -439,8 +439,7 @@
isFinished: licenseAuditIsFinished,
invalidLicenses,
includedLicenses,
checkAndTriggerAudit: checkAndTriggerLicenseAudit,
} = useLicenseAudit(props.channel, currentChannelVersion);
} = useLicenseAudit(versionDetail, publishedDataIsLoading, publishedDataIsFinished);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can remove this call, and compute invalidLicenses and includedLicenses directly here; we won't need the licenseAuditIsLoading and licenseAuditIsFinished properties anymore.

const allSpecialPermissionsChecked = ref(true);
Expand Down Expand Up @@ -470,7 +469,6 @@
watch(isPublishing, async (newIsPublishing, oldIsPublishing) => {
if (oldIsPublishing === true && newIsPublishing === false) {
await fetchPublishedData();
await checkAndTriggerLicenseAudit();
}
});
Expand All @@ -479,7 +477,6 @@
if (!isPublishing.value) {
await fetchPublishedData();
await checkAndTriggerLicenseAudit();
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1416,10 +1416,6 @@ export const Channel = new CreateModelResource({
const response = await client.get(window.Urls.channel_version_detail(id));
return response.data;
},
async auditLicenses(id) {
const response = await client.post(window.Urls.channel_audit_licenses(id));
return response.data;
},
});

function getChannelFromChannelScope() {
Expand Down
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can remove this command; we will handle this more broadly in #5593

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, after we remove this, we can remove the audit_channel_licenses.py module too!

Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
from django.core.management.base import BaseCommand

from contentcuration.models import Channel
from contentcuration.models import ChannelVersion
from contentcuration.utils.audit_channel_licenses import audit_channel_version


class Command(BaseCommand):
help = "Backfill license audit results for published channels."

def add_arguments(self, parser):
parser.add_argument(
"--channel-id",
action="append",
dest="channel_ids",
help="Channel ID to backfill (repeatable).",
)
parser.add_argument(
"--limit",
type=int,
default=None,
help="Limit number of channels to process.",
)
parser.add_argument(
"--offset",
type=int,
default=0,
help="Offset into the channel list.",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Report what would be updated without saving.",
)

def handle(self, *args, **options):
channel_ids = options.get("channel_ids")
limit = options.get("limit")
offset = options.get("offset") or 0
dry_run = options.get("dry_run")

queryset = Channel.objects.filter(
main_tree__published=True,
version__gt=0,
).order_by("id")

if channel_ids:
queryset = queryset.filter(id__in=channel_ids)

if offset:
queryset = queryset[offset:]

if limit:
queryset = queryset[:limit]

processed = 0
failures = 0

for channel in queryset.iterator():
processed += 1
try:
channel_version, _ = ChannelVersion.objects.get_or_create(
channel=channel,
version=channel.version,
)
if channel.version_info_id != channel_version.id:
if dry_run:
self.stdout.write(
f"Would set version_info for channel {channel.id}"
)
else:
Channel.objects.filter(pk=channel.pk).update(
version_info=channel_version
)

if dry_run:
self.stdout.write(
f"Would backfill audit results for channel {channel.id} "
f"version {channel_version.version}"
)
continue

audit_channel_version(channel_version)
self.stdout.write(
f"Backfilled audit results for channel {channel.id} "
f"version {channel_version.version}"
)
except Exception as error: # noqa: BLE001
failures += 1
self.stderr.write(f"Failed to backfill channel {channel.id}: {error}")

self.stdout.write(
f"Backfill complete. Processed={processed} Failures={failures}"
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
("contentcuration", "0161_update_channelversion_choices"),
]

operations = [
migrations.RenameField(
model_name="channelversion",
old_name="non_distributable_licenses_included",
new_name="non_distributable_included_licenses",
),
migrations.RenameField(
model_name="channelversion",
old_name="special_permissions_included",
new_name="included_special_permissions",
),
]
4 changes: 2 additions & 2 deletions contentcuration/contentcuration/models.py
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, apologies for the misdirection, we had already changed the name of these propoerties, so we can keep with the non_distributable_licenses_included and special_permissions_included identifiers! :)

Original file line number Diff line number Diff line change
Expand Up @@ -1542,12 +1542,12 @@ class ChannelVersion(models.Model):
null=True,
blank=True,
)
non_distributable_licenses_included = ArrayField(
non_distributable_included_licenses = ArrayField(
models.IntegerField(choices=get_license_choices()),
null=True,
blank=True,
)
special_permissions_included = models.ManyToManyField(
included_special_permissions = models.ManyToManyField(
"AuditedSpecialPermissionsLicense",
related_name="channel_versions",
blank=True,
Expand Down
6 changes: 0 additions & 6 deletions contentcuration/contentcuration/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
from contentcuration.models import Change
from contentcuration.models import ContentNode
from contentcuration.models import User
from contentcuration.utils.audit_channel_licenses import audit_channel_licenses
from contentcuration.utils.csv_writer import write_user_csv
from contentcuration.utils.nodes import calculate_resource_size
from contentcuration.utils.nodes import generate_diff
Expand Down Expand Up @@ -166,8 +165,3 @@ def sendcustomemails_task(subject, message, query):
@app.task(name="ensure_versioned_database_exists_task")
def ensure_versioned_database_exists_task(channel_id, channel_version):
ensure_versioned_database_exists(channel_id, channel_version)


@app.task(name="audit-channel-licenses")
def audit_channel_licenses_task(channel_id, user_id):
audit_channel_licenses(channel_id, user_id)
Loading