From b636a4253a2c2757a573fe4fc91a855d06ec9f53 Mon Sep 17 00:00:00 2001 From: Duane Nykamp Date: Fri, 20 Dec 2024 22:14:25 -0600 Subject: [PATCH 01/12] add classification short name --- .../migration.sql | 12 +++++++++ server/prisma/schema.prisma | 1 + server/prisma/seed.ts | 25 +++++++++++++------ server/src/types.ts | 1 + 4 files changed, 31 insertions(+), 8 deletions(-) create mode 100644 server/prisma/migrations/20241219013935_classification_system_short_name/migration.sql diff --git a/server/prisma/migrations/20241219013935_classification_system_short_name/migration.sql b/server/prisma/migrations/20241219013935_classification_system_short_name/migration.sql new file mode 100644 index 000000000..091b61afd --- /dev/null +++ b/server/prisma/migrations/20241219013935_classification_system_short_name/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - A unique constraint covering the columns `[shortName]` on the table `classificationSystems` will be added. If there are existing duplicate values, this will fail. + - Added the required column `shortName` to the `classificationSystems` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE `classificationSystems` ADD COLUMN `shortName` VARCHAR(191) NOT NULL; + +-- CreateIndex +CREATE UNIQUE INDEX `classificationSystems_shortName_key` ON `classificationSystems`(`shortName`); diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index edd2bd9df..4500b6955 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -247,6 +247,7 @@ model contentClassifications { model classificationSystems { id Int @id @default(autoincrement()) name String @unique + shortName String @unique categoryLabel String subCategoryLabel String descriptionLabel String diff --git a/server/prisma/seed.ts b/server/prisma/seed.ts index 460377942..bbf8fee3a 100644 --- a/server/prisma/seed.ts +++ b/server/prisma/seed.ts @@ -87,6 +87,7 @@ async function main() { // Classifications async function upsertClassificationSystem( name: string, + shortName: string, categoryLabel: string, subCategoryLabel: string, descriptionLabel: string, @@ -96,6 +97,7 @@ async function main() { where: { name }, update: { name, + shortName, categoryLabel, subCategoryLabel, descriptionLabel, @@ -103,6 +105,7 @@ async function main() { }, create: { name, + shortName, categoryLabel, subCategoryLabel, descriptionLabel, @@ -188,6 +191,7 @@ async function main() { async function addClassificationFromData({ name, + shortName, categoryLabel, subCategoryLabel, descriptionLabel, @@ -195,6 +199,7 @@ async function main() { sortIndex, }: { name: string; + shortName: string; categoryLabel: string; subCategoryLabel: string; descriptionLabel: string; @@ -203,6 +208,7 @@ async function main() { }) { const systemId = await upsertClassificationSystem( name, + shortName, categoryLabel, subCategoryLabel, descriptionLabel, @@ -315,6 +321,7 @@ async function main() { await addClassificationFromData({ name: "Common Core", + shortName: "Common Core", categoryLabel: "Grade", subCategoryLabel: "Cluster", descriptionLabel: "Standard", @@ -324,6 +331,7 @@ async function main() { await addClassificationFromData({ name: "Minnesota Academic Standards in Math", + shortName: "MN Math", categoryLabel: "Grade", subCategoryLabel: "Standard", descriptionLabel: "Benchmark", @@ -333,6 +341,7 @@ async function main() { await addClassificationFromData({ name: "WeBWorK taxonomy", + shortName: "WeBWorK", categoryLabel: "Subject", subCategoryLabel: "Chapter", descriptionLabel: "Section", @@ -343,7 +352,7 @@ async function main() { await prisma.licenses.upsert({ where: { code: "CCBYSA" }, update: { - name: "Creative Commons Attribution-ShareAlike", + name: "Creative Commons Attribution-ShareAlike 4.0", description: "This license requires that reusers give credit to the creator. It allows reusers to distribute, remix, adapt, and build upon the material in any medium or format, even for commercial purposes. If others remix, adapt, or build upon the material, they must license the modified material under identical terms.", imageURL: "/creative_commons_by_sa.png", @@ -353,7 +362,7 @@ async function main() { }, create: { code: "CCBYSA", - name: "Creative Commons Attribution-ShareAlike", + name: "Creative Commons Attribution-ShareAlike 4.0", description: "This license requires that reusers give credit to the creator. It allows reusers to distribute, remix, adapt, and build upon the material in any medium or format, even for commercial purposes. If others remix, adapt, or build upon the material, they must license the modified material under identical terms.", imageURL: "/creative_commons_by_sa.png", @@ -366,7 +375,7 @@ async function main() { await prisma.licenses.upsert({ where: { code: "CCBYNCSA" }, update: { - name: "Creative Commons Attribution-NonCommercial-ShareAlike", + name: "Creative Commons Attribution-NonCommercial-ShareAlike 4.0", description: "This license requires that reusers give credit to the creator. It allows reusers to distribute, remix, adapt, and build upon the material in any medium or format, for noncommercial purposes only. If others modify or adapt the material, they must license the modified material under identical terms.", imageURL: "/creative_commons_by_nc_sa.png", @@ -376,7 +385,7 @@ async function main() { }, create: { code: "CCBYNCSA", - name: "Creative Commons Attribution-NonCommercial-ShareAlike", + name: "Creative Commons Attribution-NonCommercial-ShareAlike 4.0", description: "This license requires that reusers give credit to the creator. It allows reusers to distribute, remix, adapt, and build upon the material in any medium or format, for noncommercial purposes only. If others modify or adapt the material, they must license the modified material under identical terms.", imageURL: "/creative_commons_by_nc_sa.png", @@ -389,16 +398,16 @@ async function main() { await prisma.licenses.upsert({ where: { code: "CCDUAL" }, update: { - name: "Dual license Creative Commons Attribution-ShareAlike OR Attribution-NonCommercial-ShareAlike", + name: "Dual license Creative Commons Attribution-ShareAlike 4.0 OR Attribution-NonCommercial-ShareAlike 4.0", description: - "Allow reusers to use either the Creative Commons Attribution-ShareAlike license or the Creative Commons Attribution-NonCommercial-ShareAlike license.", + "Allow reusers to use either the Creative Commons Attribution-ShareAlike 4.0 license or the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 license.", sortIndex: 1, }, create: { code: "CCDUAL", - name: "Dual license Creative Commons Attribution-ShareAlike OR Attribution-NonCommercial-ShareAlike", + name: "Dual license Creative Commons Attribution-ShareAlike 4.0 OR Attribution-NonCommercial-ShareAlike 4.0", description: - "Allow reusers to use either the Creative Commons Attribution-ShareAlike license or the Creative Commons Attribution-NonCommercial-ShareAlike license.", + "Allow reusers to use either the Creative Commons Attribution-ShareAlike 4.0 license or the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 license.", sortIndex: 1, }, }); diff --git a/server/src/types.ts b/server/src/types.ts index 423baca72..471bf43fa 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -30,6 +30,7 @@ export type ContentClassification = { system: { id: number; name: string; + shortName: string; categoryLabel: string; subCategoryLabel: string; descriptionLabel: string; From 7b6afae20fb28104ad0aa4dba5e392e305768988 Mon Sep 17 00:00:00 2001 From: Duane Nykamp Date: Fri, 20 Dec 2024 22:17:38 -0600 Subject: [PATCH 02/12] add classification info to api outputs rework remix info in api outputs --- server/src/index.ts | 6 +- server/src/model.test.ts | 36 ++++++------ server/src/model.ts | 118 ++++++++++++++++++++++++++++++++++++--- server/src/types.ts | 24 ++++++++ server/src/utils/uuid.ts | 20 ++++++- 5 files changed, 174 insertions(+), 30 deletions(-) diff --git a/server/src/index.ts b/server/src/index.ts index 563fe67cd..7c6b8ac22 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -106,6 +106,7 @@ import { allAssignmentScoresConvertUUID, studentDataConvertUUID, isEqualUUID, + docRemixesConvertUUID, } from "./utils/uuid"; import { LicenseCode, UserInfo } from "./types"; @@ -1440,12 +1441,11 @@ app.get( const activityId = toUUID(req.params.activityId); try { - const data = await getActivityRemixes({ + const { docRemixes } = await getActivityRemixes({ activityId, loggedInUserId, }); - // TODO: process to convert UUIDs - res.send(data); + res.send({ docRemixes: docRemixes.map(docRemixesConvertUUID) }); } catch (e) { if (e instanceof Prisma.PrismaClientKnownRequestError) { res.sendStatus(404); diff --git a/server/src/model.test.ts b/server/src/model.test.ts index efe1364d4..80d00501a 100644 --- a/server/src/model.test.ts +++ b/server/src/model.test.ts @@ -2658,13 +2658,13 @@ test("contributor history shows only documents user can view", async () => { docIds: [docId1], loggedInUserId: ownerId1, }) - )[0].documentVersions.flatMap((v) => v.contributorHistory); + )[0].documentVersions.flatMap((v) => v.remixes); expect(docRemixes.length).eq(2); expect(docRemixes[0].docId).eqls(docId5); - expect(docRemixes[0].document.activity.id).eqls(activityId5); + expect(docRemixes[0].activity.id).eqls(activityId5); expect(docRemixes[0].withLicenseCode).eq("CCDUAL"); expect(docRemixes[1].docId).eqls(docId4); - expect(docRemixes[1].document.activity.id).eqls(activityId4); + expect(docRemixes[1].activity.id).eqls(activityId4); expect(docRemixes[1].withLicenseCode).eq("CCDUAL"); // owner 1 just sees direct remix from activity 1 into activity 5 @@ -2673,10 +2673,10 @@ test("contributor history shows only documents user can view", async () => { docIds: [docId1], loggedInUserId: ownerId1, }) - )[0].documentVersions.flatMap((v) => v.contributorHistory); + )[0].documentVersions.flatMap((v) => v.remixes); expect(docRemixes.length).eq(1); expect(docRemixes[0].docId).eqls(docId5); - expect(docRemixes[0].document.activity.id).eqls(activityId5); + expect(docRemixes[0].activity.id).eqls(activityId5); expect(docRemixes[0].withLicenseCode).eq("CCDUAL"); // owner2 just sees activity 1 and 2 in history of activity 4 @@ -2700,13 +2700,13 @@ test("contributor history shows only documents user can view", async () => { docIds: [docId1], loggedInUserId: ownerId2, }) - )[0].documentVersions.flatMap((v) => v.contributorHistory); + )[0].documentVersions.flatMap((v) => v.remixes); expect(docRemixes.length).eq(2); expect(docRemixes[0].docId).eqls(docId4); - expect(docRemixes[0].document.activity.id).eqls(activityId4); + expect(docRemixes[0].activity.id).eqls(activityId4); expect(docRemixes[0].withLicenseCode).eq("CCDUAL"); expect(docRemixes[1].docId).eqls(docId2); - expect(docRemixes[1].document.activity.id).eqls(activityId2); + expect(docRemixes[1].activity.id).eqls(activityId2); expect(docRemixes[1].withLicenseCode).eq("CCDUAL"); // owner 2 sees direct remix of activity 1 into 2 @@ -2715,10 +2715,10 @@ test("contributor history shows only documents user can view", async () => { docIds: [docId1], loggedInUserId: ownerId2, }) - )[0].documentVersions.flatMap((v) => v.contributorHistory); + )[0].documentVersions.flatMap((v) => v.remixes); expect(docRemixes.length).eq(1); expect(docRemixes[0].docId).eqls(docId2); - expect(docRemixes[0].document.activity.id).eqls(activityId2); + expect(docRemixes[0].activity.id).eqls(activityId2); expect(docRemixes[0].withLicenseCode).eq("CCDUAL"); // owner3 sees activity 1, 2 and 3 in history of activity 4 @@ -2745,19 +2745,19 @@ test("contributor history shows only documents user can view", async () => { docIds: [docId1], loggedInUserId: ownerId3, }) - )[0].documentVersions.flatMap((v) => v.contributorHistory); + )[0].documentVersions.flatMap((v) => v.remixes); expect(docRemixes.length).eq(4); expect(docRemixes[0].docId).eqls(docId5); - expect(docRemixes[0].document.activity.id).eqls(activityId5); + expect(docRemixes[0].activity.id).eqls(activityId5); expect(docRemixes[0].withLicenseCode).eq("CCDUAL"); expect(docRemixes[1].docId).eqls(docId4); - expect(docRemixes[1].document.activity.id).eqls(activityId4); + expect(docRemixes[1].activity.id).eqls(activityId4); expect(docRemixes[1].withLicenseCode).eq("CCDUAL"); expect(docRemixes[2].docId).eqls(docId3); - expect(docRemixes[2].document.activity.id).eqls(activityId3); + expect(docRemixes[2].activity.id).eqls(activityId3); expect(docRemixes[2].withLicenseCode).eq("CCDUAL"); expect(docRemixes[3].docId).eqls(docId2); - expect(docRemixes[3].document.activity.id).eqls(activityId2); + expect(docRemixes[3].activity.id).eqls(activityId2); expect(docRemixes[3].withLicenseCode).eq("CCDUAL"); // owner 3 sees direct remixes of activity 1 into 2 and 5 @@ -2766,13 +2766,13 @@ test("contributor history shows only documents user can view", async () => { docIds: [docId1], loggedInUserId: ownerId3, }) - )[0].documentVersions.flatMap((v) => v.contributorHistory); + )[0].documentVersions.flatMap((v) => v.remixes); expect(docRemixes.length).eq(2); expect(docRemixes[0].docId).eqls(docId5); - expect(docRemixes[0].document.activity.id).eqls(activityId5); + expect(docRemixes[0].activity.id).eqls(activityId5); expect(docRemixes[0].withLicenseCode).eq("CCDUAL"); expect(docRemixes[1].docId).eqls(docId2); - expect(docRemixes[1].document.activity.id).eqls(activityId2); + expect(docRemixes[1].activity.id).eqls(activityId2); expect(docRemixes[1].withLicenseCode).eq("CCDUAL"); }); diff --git a/server/src/model.ts b/server/src/model.ts index 44681c749..f7b23a679 100644 --- a/server/src/model.ts +++ b/server/src/model.ts @@ -8,6 +8,7 @@ import { ContentClassification, ContentStructure, DocHistory, + DocRemixes, License, LicenseCode, UserInfo, @@ -702,6 +703,7 @@ export async function copyActivityToFolder( classificationId: c.classificationId, })), }, + licenseCode: origActivity.licenseCode, }, }); @@ -1211,6 +1213,31 @@ export async function getSharedEditorData( }, }, }, + owner: { + select: { + userId: true, + email: true, + firstNames: true, + lastNames: true, + }, + }, + classifications: { + select: { + classification: { + include: { + subCategories: { + include: { + category: { + include: { + system: true, + }, + }, + }, + }, + }, + }, + }, + }, }, }); @@ -1218,6 +1245,7 @@ export async function getSharedEditorData( license, sharedWith: sharedWithOrig, parentFolder, + classifications, ...preliminaryActivity2 } = preliminaryActivity; @@ -1231,7 +1259,7 @@ export async function getSharedEditorData( isShared, sharedWith, license: license ? processLicense(license) : null, - classifications: [], + classifications: classifications.map((c) => c.classification), classCode: null, codeValidUntil: null, assignmentStatus: "Unassigned", @@ -1308,6 +1336,23 @@ export async function getActivityViewerData( lastNames: true, }, }, + classifications: { + select: { + classification: { + include: { + subCategories: { + include: { + category: { + include: { + system: true, + }, + }, + }, + }, + }, + }, + }, + }, }, }); @@ -1315,6 +1360,7 @@ export async function getActivityViewerData( license, sharedWith: sharedWithOrig, parentFolder, + classifications, ...preliminaryActivity2 } = preliminaryActivity; @@ -1329,7 +1375,7 @@ export async function getActivityViewerData( isShared, sharedWith, license: license ? processLicense(license) : null, - classifications: [], + classifications: classifications.map((v) => v.classification), classCode: null, codeValidUntil: null, assignmentStatus: "Unassigned", @@ -1530,6 +1576,7 @@ export async function getDocumentDirectRemixes({ contributorHistory: { where: { document: { + isDeleted: false, activity: { OR: [ { ownerId: loggedInUserId }, @@ -1543,7 +1590,11 @@ export async function getDocumentDirectRemixes({ }, }, orderBy: { timestampDoc: "desc" }, - include: { + select: { + docId: true, + withLicenseCode: true, + timestampDoc: true, + timestampPrevDoc: true, document: { select: { activity: { @@ -1569,7 +1620,21 @@ export async function getDocumentDirectRemixes({ }, }); - return docRemixes; + const docRemixes2: DocRemixes[] = docRemixes.map((remixes) => ({ + id: remixes.id, + documentVersions: remixes.documentVersions.map((docVersion) => ({ + versionNumber: docVersion.versionNum, + remixes: docVersion.contributorHistory.map((contribHist) => ({ + docId: contribHist.docId, + withLicenseCode: contribHist.withLicenseCode, + timestampDoc: contribHist.timestampDoc, + timestampPrevDoc: contribHist.timestampPrevDoc, + activity: contribHist.document.activity, + })), + })), + })); + + return docRemixes2; } export async function getDocumentRemixes({ @@ -1599,6 +1664,7 @@ export async function getDocumentRemixes({ contributorHistory: { where: { document: { + isDeleted: false, activity: { OR: [ { ownerId: loggedInUserId }, @@ -1609,7 +1675,11 @@ export async function getDocumentRemixes({ }, }, orderBy: { timestampDoc: "desc" }, - include: { + select: { + docId: true, + withLicenseCode: true, + timestampDoc: true, + timestampPrevDoc: true, document: { select: { activity: { @@ -1635,7 +1705,21 @@ export async function getDocumentRemixes({ }, }); - return docRemixes; + const docRemixes2: DocRemixes[] = docRemixes.map((remixes) => ({ + id: remixes.id, + documentVersions: remixes.documentVersions.map((docVersion) => ({ + versionNumber: docVersion.versionNum, + remixes: docVersion.contributorHistory.map((contribHist) => ({ + docId: contribHist.docId, + withLicenseCode: contribHist.withLicenseCode, + timestampDoc: contribHist.timestampDoc, + timestampPrevDoc: contribHist.timestampPrevDoc, + activity: contribHist.document.activity, + })), + })), + })); + + return docRemixes2; } export async function getAssignmentDataFromCode(code: string) { @@ -1786,6 +1870,24 @@ export async function searchSharedContent( }, }, }, + classifications: { + select: { + classification: { + include: { + subCategories: { + include: { + category: { + include: { + system: true, + }, + }, + }, + }, + }, + }, + }, + }, + documents: { select: { id: true, doenetmlVersion: true } }, parentFolder: { select: { id: true, @@ -1813,6 +1915,7 @@ export async function searchSharedContent( license, sharedWith: sharedWithOrig, parentFolder, + classifications, ...content2 } = content; @@ -1825,9 +1928,8 @@ export async function searchSharedContent( ...content2, isShared, sharedWith, - documents: [], license: license ? processLicense(license) : null, - classifications: [], + classifications: classifications.map((c) => c.classification), classCode: null, codeValidUntil: null, assignmentStatus: "Unassigned", diff --git a/server/src/types.ts b/server/src/types.ts index 471bf43fa..7570dd0fa 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -115,6 +115,30 @@ export type DocHistory = { }[]; }; +export type DocRemixes = { + id: Uint8Array; + documentVersions: { + versionNumber: number; + remixes: { + activity: { + id: Uint8Array; + name: string; + owner: { + userId: Uint8Array; + email: string; + firstNames: string | null; + lastNames: string; + }; + }; + + docId: Uint8Array; + withLicenseCode: string | null; + timestampDoc: Date; + timestampPrevDoc: Date; + }[]; + }[]; +}; + export type ClassificationCategoryTree = { id: number; name: string; diff --git a/server/src/utils/uuid.ts b/server/src/utils/uuid.ts index 7aba58130..a9ac696a1 100644 --- a/server/src/utils/uuid.ts +++ b/server/src/utils/uuid.ts @@ -1,4 +1,4 @@ -import { ContentStructure, DocHistory, UserInfo } from "../types"; +import { ContentStructure, DocHistory, DocRemixes, UserInfo } from "../types"; import { fromBinaryUUID, toBinaryUUID } from "./binary-uuid"; import short from "short-uuid"; @@ -90,6 +90,24 @@ export function docHistoryConvertUUID(docHistory: DocHistory) { }; } +export function docRemixesConvertUUID(docRemixes: DocRemixes) { + return { + id: fromUUID(docRemixes.id), + documentVersions: docRemixes.documentVersions.map((docVersion) => ({ + versionNumber: docVersion.versionNumber, + remixes: docVersion.remixes.map((remix) => ({ + ...remix, + docId: fromUUID(remix.docId), + activity: { + name: remix.activity.name, + id: fromUUID(remix.activity.id), + owner: userConvertUUID(remix.activity.owner), + }, + })), + })), + }; +} + export function assignmentConvertUUID(assignment: { id: Uint8Array; documents: { From 1ee2c4bb786ae1f4dab67a8963a2f996ec3b3b3a Mon Sep 17 00:00:00 2001 From: Duane Nykamp Date: Fri, 20 Dec 2024 22:18:03 -0600 Subject: [PATCH 03/12] remove bullet from license display --- client/src/Widgets/Licenses.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/client/src/Widgets/Licenses.tsx b/client/src/Widgets/Licenses.tsx index 5da8a7150..3209fb30e 100644 --- a/client/src/Widgets/Licenses.tsx +++ b/client/src/Widgets/Licenses.tsx @@ -3,7 +3,6 @@ import { License } from "../_utils/types"; import { HStack, Image, - ListIcon, ListItem, Text, Tooltip, @@ -104,10 +103,7 @@ export function DisplayLicenseItem({ return ( - - - {item} - + {item} ); } From 5b6ff75e8a1206818f6d14b4dfdf4dde1372ddce Mon Sep 17 00:00:00 2001 From: Duane Nykamp Date: Fri, 20 Dec 2024 22:20:40 -0600 Subject: [PATCH 04/12] rename public editor to code viewer --- .../Tools/_framework/Paths/ActivityEditor.tsx | 4 +- .../Tools/_framework/Paths/ActivityViewer.tsx | 4 +- .../Paths/AssignmentStudentData.tsx | 4 +- .../_framework/Paths/AssignmentViewer.tsx | 2 +- .../{PublicEditor.tsx => CodeViewer.tsx} | 54 +++++++++++++++---- client/src/Tools/_framework/Paths/Home.tsx | 2 +- .../ToolPanels/AssignmentPreview.tsx | 2 +- client/src/index.tsx | 24 ++++----- 8 files changed, 65 insertions(+), 31 deletions(-) rename client/src/Tools/_framework/Paths/{PublicEditor.tsx => CodeViewer.tsx} (82%) diff --git a/client/src/Tools/_framework/Paths/ActivityEditor.tsx b/client/src/Tools/_framework/Paths/ActivityEditor.tsx index 36d9032f4..c8723dc12 100644 --- a/client/src/Tools/_framework/Paths/ActivityEditor.tsx +++ b/client/src/Tools/_framework/Paths/ActivityEditor.tsx @@ -97,7 +97,7 @@ export async function loader({ params }) { if (notMe) { return redirect( - `/publicEditor/${params.activityId}${params.docId ? "/" + params.docId : ""}`, + `/codeViewer/${params.activityId}${params.docId ? "/" + params.docId : ""}`, ); } @@ -661,7 +661,7 @@ export function ActivityEditor() { navigate={navigate} linkSettings={{ viewURL: "/activityViewer", - editURL: "/publicEditor", + editURL: "/codeViewer", }} includeVariantSelector={true} /> diff --git a/client/src/Tools/_framework/Paths/ActivityViewer.tsx b/client/src/Tools/_framework/Paths/ActivityViewer.tsx index ad854f343..c9ccd6d8c 100644 --- a/client/src/Tools/_framework/Paths/ActivityViewer.tsx +++ b/client/src/Tools/_framework/Paths/ActivityViewer.tsx @@ -179,7 +179,7 @@ export function ActivityViewer() { colorScheme="blue" data-test="See Inside" onClick={() => { - navigate(`/publicEditor/${activityId}/${docId}`); + navigate(`/codeViewer/${activityId}/${docId}`); }} > See Inside @@ -240,7 +240,7 @@ export function ActivityViewer() { navigate={navigate} linkSettings={{ viewURL: "/activityViewer", - editURL: "/publicEditor", + editURL: "/codeViewer", }} includeVariantSelector={false} /> diff --git a/client/src/Tools/_framework/Paths/AssignmentStudentData.tsx b/client/src/Tools/_framework/Paths/AssignmentStudentData.tsx index 1a2835f12..cbf2c77b7 100644 --- a/client/src/Tools/_framework/Paths/AssignmentStudentData.tsx +++ b/client/src/Tools/_framework/Paths/AssignmentStudentData.tsx @@ -267,7 +267,7 @@ export function AssignmentStudentData() { paginate={true} linkSettings={{ viewURL: "/activityViewer", - editURL: "/publicEditor", + editURL: "/codeViewer", }} apiURLs={{ postMessages: true }} /> @@ -298,7 +298,7 @@ export function AssignmentStudentData() { paginate={true} linkSettings={{ viewURL: "/activityViewer", - editURL: "/publicEditor", + editURL: "/codeViewer", }} apiURLs={{ postMessages: true }} /> diff --git a/client/src/Tools/_framework/Paths/AssignmentViewer.tsx b/client/src/Tools/_framework/Paths/AssignmentViewer.tsx index b45a6dd97..49e107015 100644 --- a/client/src/Tools/_framework/Paths/AssignmentViewer.tsx +++ b/client/src/Tools/_framework/Paths/AssignmentViewer.tsx @@ -296,7 +296,7 @@ export function AssignmentViewer() { navigate={navigate} linkSettings={{ viewURL: "/activityViewer", - editURL: "/publicEditor", + editURL: "/codeViewer", }} apiURLs={{ postMessages: true }} /> diff --git a/client/src/Tools/_framework/Paths/PublicEditor.tsx b/client/src/Tools/_framework/Paths/CodeViewer.tsx similarity index 82% rename from client/src/Tools/_framework/Paths/PublicEditor.tsx rename to client/src/Tools/_framework/Paths/CodeViewer.tsx index c23b04387..433699004 100644 --- a/client/src/Tools/_framework/Paths/PublicEditor.tsx +++ b/client/src/Tools/_framework/Paths/CodeViewer.tsx @@ -9,7 +9,9 @@ import { Grid, GridItem, HStack, + IconButton, Text, + Tooltip, useDisclosure, } from "@chakra-ui/react"; import { WarningIcon } from "@chakra-ui/icons"; @@ -18,6 +20,8 @@ import { CopyActivityAndReportFinish } from "../ToolPanels/CopyActivityAndReport import axios from "axios"; import { User } from "./SiteHeader"; import { ContentStructure, DoenetmlVersion } from "../../../_utils/types"; +import { ContentInfoDrawer } from "../ToolPanels/ContentInfoDrawer"; +import { MdOutlineInfo } from "react-icons/md"; export async function loader({ params, request }) { const url = new URL(request.url); @@ -58,7 +62,7 @@ export async function loader({ params, request }) { }; } -export function PublicEditor() { +export function CodeViewer() { const { doenetML, doenetmlVersion, activityData } = useLoaderData() as { doenetML: string; doenetmlVersion?: DoenetmlVersion; @@ -66,9 +70,15 @@ export function PublicEditor() { }; const { - isOpen: copyActivityIsOpen, - onOpen: copyActivityOnOpen, - onClose: copyActivityOnClose, + isOpen: copyDialogIsOpen, + onOpen: copyDialogOnOpen, + onClose: copyDialogOnClose, + } = useDisclosure(); + + const { + isOpen: infoIsOpen, + onOpen: infoOnOpen, + onClose: infoOnClose, } = useDisclosure(); const user = useOutletContext(); @@ -83,11 +93,19 @@ export function PublicEditor() { return ( <> {activityData ? ( - + <> + + + ) : null} { - copyActivityOnOpen(); + copyDialogOnOpen(); }} > Copy to Activities @@ -195,6 +213,20 @@ export function PublicEditor() { Sign In To Copy to Activities )} + + } + aria-label="Activity information" + onClick={() => { + infoOnOpen(); + }} + /> + )} @@ -210,6 +242,8 @@ export function PublicEditor() { readOnly={activityData !== undefined} doenetmlVersion={doenetmlVersion?.fullVersion} border="none" + showFormatter={false} + showErrorsWarnings={false} /> diff --git a/client/src/Tools/_framework/Paths/Home.tsx b/client/src/Tools/_framework/Paths/Home.tsx index c551da561..1693d4b90 100644 --- a/client/src/Tools/_framework/Paths/Home.tsx +++ b/client/src/Tools/_framework/Paths/Home.tsx @@ -511,7 +511,7 @@ export function Home() { addBottomPadding={false} linkSettings={{ viewURL: "/activityViewer", - editURL: "/publicEditor", + editURL: "/codeViewer", }} /> diff --git a/client/src/Tools/_framework/ToolPanels/AssignmentPreview.tsx b/client/src/Tools/_framework/ToolPanels/AssignmentPreview.tsx index 6e690175d..964b0b67e 100644 --- a/client/src/Tools/_framework/ToolPanels/AssignmentPreview.tsx +++ b/client/src/Tools/_framework/ToolPanels/AssignmentPreview.tsx @@ -53,7 +53,7 @@ export default function AssignmentPreview({ navigate={navigate} linkSettings={{ viewURL: "/activityViewer", - editURL: "/publicEditor", + editURL: "/codeViewer", }} showAnswerTitles={true} /> diff --git a/client/src/index.tsx b/client/src/index.tsx index b44cae5f1..78a3d2baa 100644 --- a/client/src/index.tsx +++ b/client/src/index.tsx @@ -98,9 +98,9 @@ import { action as activityEditorAction, } from "./Tools/_framework/Paths/ActivityEditor"; import { - PublicEditor, - loader as publicEditorLoader, -} from "./Tools/_framework/Paths/PublicEditor"; + CodeViewer, + loader as codeViewerLoader, +} from "./Tools/_framework/Paths/CodeViewer"; import { mathjaxConfig } from "@doenet/doenetml-iframe"; import { SignIn, @@ -277,22 +277,22 @@ const router = createBrowserRouter([ errorElement: , }, { - path: "publicEditor", - loader: publicEditorLoader, + path: "codeViewer", + loader: codeViewerLoader, errorElement: , - element: , + element: , }, { - path: "publicEditor/:activityId", - loader: publicEditorLoader, + path: "codeViewer/:activityId", + loader: codeViewerLoader, errorElement: , - element: , + element: , }, { - path: "publicEditor/:activityId/:docId", - loader: publicEditorLoader, + path: "codeViewer/:activityId/:docId", + loader: codeViewerLoader, errorElement: , - element: , + element: , }, { path: "assigned", From 7947e06b198c361c97e9edd973ffcee9531e4dcf Mon Sep 17 00:00:00 2001 From: Duane Nykamp Date: Fri, 20 Dec 2024 22:22:03 -0600 Subject: [PATCH 05/12] only allow licenses compatible with remix license --- .../_framework/ToolPanels/ShareDrawer.tsx | 12 +- .../_framework/ToolPanels/ShareSettings.tsx | 142 +++++++++++++----- 2 files changed, 107 insertions(+), 47 deletions(-) diff --git a/client/src/Tools/_framework/ToolPanels/ShareDrawer.tsx b/client/src/Tools/_framework/ToolPanels/ShareDrawer.tsx index a53dd62dd..d9643cd8d 100644 --- a/client/src/Tools/_framework/ToolPanels/ShareDrawer.tsx +++ b/client/src/Tools/_framework/ToolPanels/ShareDrawer.tsx @@ -24,6 +24,7 @@ import { DocHistoryItem, DocRemixItem, License, + LicenseCode, } from "../../../_utils/types"; import axios from "axios"; import { cidFromText } from "../../../_utils/cid"; @@ -69,6 +70,8 @@ export function ShareDrawer({ const [haveChangedHistoryItem, setHaveChangedHistoryItem] = useState(false); const [remixes, setRemixes] = useState(null); const [thisCid, setThisCid] = useState(null); + const [remixedWithLicense, setRemixedWithLicense] = + useState(null); useEffect(() => { async function getHistoryAndRemixes() { @@ -83,6 +86,8 @@ export function ShareDrawer({ setHaveChangedHistoryItem(haveChanged); + setRemixedWithLicense(hist[0].withLicenseCode || null); + const { data: data2 } = await axios.get( `/api/getRemixes/${contentData.id}`, ); @@ -96,12 +101,6 @@ export function ShareDrawer({ } }, [contentData]); - useEffect(() => { - async function getRemixes() {} - - getRemixes(); - }, [contentData.id]); - useEffect(() => { async function recalculateThisCid() { let cid: string | null = null; @@ -171,6 +170,7 @@ export function ShareDrawer({ fetcher={fetcher} contentData={contentData} allLicenses={allLicenses} + remixedWithLicense={remixedWithLicense} /> {!contentData.isFolder ? ( diff --git a/client/src/Tools/_framework/ToolPanels/ShareSettings.tsx b/client/src/Tools/_framework/ToolPanels/ShareSettings.tsx index 86ed6575e..703a97cbd 100644 --- a/client/src/Tools/_framework/ToolPanels/ShareSettings.tsx +++ b/client/src/Tools/_framework/ToolPanels/ShareSettings.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState } from "react"; +import React, { ReactElement, useEffect, useRef, useState } from "react"; import { FetcherWithComponents, Form, @@ -31,7 +31,7 @@ import { Hide, Button, } from "@chakra-ui/react"; -import { InfoIcon } from "@chakra-ui/icons"; +import { InfoIcon, WarningIcon } from "@chakra-ui/icons"; import axios from "axios"; import { createFullName } from "../../../_utils/names"; import { ContentStructure, License, LicenseCode } from "../../../_utils/types"; @@ -128,10 +128,12 @@ export function ShareSettings({ fetcher, contentData, allLicenses, + remixedWithLicense, }: { fetcher: FetcherWithComponents; contentData: ContentStructure; allLicenses: License[]; + remixedWithLicense: LicenseCode | null; }) { let license = contentData.license; @@ -202,6 +204,101 @@ export function ShareSettings({ setStatusText(nextStatusText.current); }, [contentData]); + let licenseDeterminedFromRemix = + selectedLicenseCode === remixedWithLicense && + remixedWithLicense !== "CCDUAL"; + + let licenseNotMatchRemix = + remixedWithLicense !== null && + remixedWithLicense !== "CCDUAL" && + selectedLicenseCode !== remixedWithLicense; + + let licenseWarning: ReactElement | null = null; + if (licenseNotMatchRemix) { + let remixedWithLicenseName = allLicenses.find( + (l) => l.code === remixedWithLicense, + )?.name; + + let selectedLicenseName = allLicenses.find( + (l) => l.code === selectedLicenseCode, + )?.name; + + licenseWarning = ( + + + Selected license {selectedLicenseName} is not compatible with the + license that this activity was remixed from: {remixedWithLicenseName}.{" "} + + (More information) + + + ); + } + + + Your code is not being saved in this view. Copy to one of your activities to + save changes. + ; + + let chooseLicenseForm: ReactElement | null = null; + if (licenseDeterminedFromRemix) { + let licenseName = allLicenses.find( + (l) => l.code === remixedWithLicense, + )?.name; + chooseLicenseForm = ( + +

License: {licenseName}

+

+ (Cannot change license since remixed from activity with this license.) +

+
+ ); + } else if ( + !(contentData.parentFolder?.isPublic || contentData.parentFolder?.isShared) + ) { + chooseLicenseForm = ( + + Change license + + + + + A license is required to make public. + + + ); + } + return ( <> @@ -535,45 +632,8 @@ export function ShareSettings({ )} - {contentData.parentFolder?.isPublic || - contentData.parentFolder?.isShared ? null : ( - - Change license - - - - - A license is required to make public. - - - )} + {chooseLicenseForm} + {licenseWarning} ); From 2b641e2f60fea77b5eaceb462d1c95f177515fc9 Mon Sep 17 00:00:00 2001 From: Duane Nykamp Date: Fri, 20 Dec 2024 22:26:45 -0600 Subject: [PATCH 06/12] add content info drawer --- .../Tools/_framework/Paths/ActivityViewer.tsx | 154 ++++++++++---- .../src/Tools/_framework/Paths/Community.tsx | 62 +++++- .../ToolPanels/ClassificationInfo.tsx | 132 ++++++++++++ .../ToolPanels/ContentInfoDrawer.tsx | 198 ++++++++++++++++++ .../ToolPanels/ContributorsMenu.tsx | 2 +- .../ToolPanels/GeneralContentInfo.tsx | 65 ++++++ 6 files changed, 563 insertions(+), 50 deletions(-) create mode 100644 client/src/Tools/_framework/ToolPanels/ClassificationInfo.tsx create mode 100644 client/src/Tools/_framework/ToolPanels/ContentInfoDrawer.tsx create mode 100644 client/src/Tools/_framework/ToolPanels/GeneralContentInfo.tsx diff --git a/client/src/Tools/_framework/Paths/ActivityViewer.tsx b/client/src/Tools/_framework/Paths/ActivityViewer.tsx index c9ccd6d8c..4d535e1d9 100644 --- a/client/src/Tools/_framework/Paths/ActivityViewer.tsx +++ b/client/src/Tools/_framework/Paths/ActivityViewer.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from "react"; +import React, { useEffect, useState } from "react"; import { useLoaderData, useNavigate, @@ -13,10 +13,14 @@ import { Flex, Grid, GridItem, + Heading, HStack, + IconButton, List, + ListItem, Spacer, Text, + Tooltip, useDisclosure, VStack, } from "@chakra-ui/react"; @@ -30,13 +34,14 @@ import { ContentStructure, DocHistoryItem, DoenetmlVersion, - License, } from "../../../_utils/types"; import { processContributorHistory } from "../../../_utils/processRemixes"; import { DisplayLicenseItem, SmallLicenseBadges, } from "../../../Widgets/Licenses"; +import { ContentInfoDrawer } from "../ToolPanels/ContentInfoDrawer"; +import { MdOutlineInfo } from "react-icons/md"; export async function loader({ params }) { try { @@ -102,6 +107,16 @@ export function ActivityViewer() { onClose: copyDialogOnClose, } = useDisclosure(); + const { + isOpen: infoIsOpen, + onOpen: infoOnOpen, + onClose: infoOnClose, + } = useDisclosure(); + + const [displayInfoTab, setDisplayInfoTab] = useState< + "general" | "classifications" + >("general"); + const navigate = useNavigate(); const location = useLocation(); @@ -111,6 +126,8 @@ export function ActivityViewer() { document.title = `${activity.name} - Doenet`; }, [activity.name]); + const haveClassifications = activity.classifications.length > 0; + return ( <> + )} + + } + aria-label="Activity information" + onClick={() => { + setDisplayInfoTab("general"); + infoOnOpen(); + }} + /> + @@ -252,55 +291,86 @@ export function ActivityViewer() { padding="0px" margin="0px" /> - - {activity.license ? ( - activity.license.isComposition ? ( - <> -

- {activity.name} by{" "} - {createFullName(activity.owner!)} is shared with these - licenses: -

- - {activity.license.composedOf.map((comp) => ( + + {activity.license ? ( + activity.license.isComposition ? ( + <> +

+ {activity.name} by{" "} + {createFullName(activity.owner!)} is shared with + these licenses: +

+ + {activity.license.composedOf.map((comp) => ( + + ))} + +

+ You are free to use either license when reusing this + work. +

+ + ) : ( + <> +

+ {activity.name} by{" "} + {createFullName(activity.owner!)} is shared using + the license: +

+ - ))} - -

- You are free to use either license when reusing this - work. -

- +
+ + ) ) : ( - <> -

- {activity.name} by{" "} - {createFullName(activity.owner!)} is shared using the - license: -

- - - - - ) - ) : ( -

- {activity.name} by{" "} - {createFullName(activity.owner!)} is shared, but a license - was not specified. Contact the author to determine in what - ways you can reuse this activity. -

- )} -
+

+ {activity.name} by{" "} + {createFullName(activity.owner!)} is shared, but a + license was not specified. Contact the author to + determine in what ways you can reuse this activity. +

+ )} +
+ {haveClassifications ? ( + { + setDisplayInfoTab("classifications"); + infoOnOpen(); + }} + > + Classifications + + {activity.classifications.map((classification) => { + return ( + + + {classification.code} ( + { + classification.subCategories[0].category + .system.shortName + } + ) + + + ); + })} + + + ) : null} + diff --git a/client/src/Tools/_framework/Paths/Community.tsx b/client/src/Tools/_framework/Paths/Community.tsx index d8d3bed46..fd99b68ae 100644 --- a/client/src/Tools/_framework/Paths/Community.tsx +++ b/client/src/Tools/_framework/Paths/Community.tsx @@ -41,6 +41,7 @@ import AuthorCard from "../../../Widgets/AuthorCard"; import { createFullName } from "../../../_utils/names"; import ActivityTable from "../../../Widgets/ActivityTable"; import { ContentStructure } from "../../../_utils/types"; +import { ContentInfoDrawer } from "../ToolPanels/ContentInfoDrawer"; type SearchMatch = | (ContentStructure & { type: "content" }) @@ -425,6 +426,14 @@ export function Community() { document.title = `Community - Doenet`; }, []); + const [infoContentId, setInfoContentId] = useState(null); + + const { + isOpen: infoIsOpen, + onOpen: infoOnOpen, + onClose: infoOnClose, + } = useDisclosure(); + if (searchResults) { let contentMatches: SearchMatch[] = searchResults.content.map((c) => ({ type: "content", @@ -533,19 +542,35 @@ export function Community() { ? `/sharedActivities/${owner.userId}/${id}` : `/activityViewer/${id}`; - return { - id, - title: name, - ownerName: owner != undefined ? createFullName(owner) : "", - cardLink, - menuItems: isAdmin ? ( + let menuItems = ( + { + setInfoContentId(id); + infoOnOpen(); + }} + > + Activity information + + ); + + if (isAdmin) { + menuItems = ( <> + {menuItems} - ) : undefined, + ); + } + + return { + id, + title: name, + ownerName: owner != undefined ? createFullName(owner) : "", + cardLink, + menuItems, }; } else if (itemObj?.type == "author") { const cardLink = `/sharedActivities/${itemObj.userId}`; @@ -564,8 +589,31 @@ export function Community() { ); } + let contentData: ContentStructure | undefined; + if (infoContentId) { + let index = searchResults.content.findIndex( + (obj) => obj.id == infoContentId, + ); + if (index != -1) { + contentData = searchResults.content[index]; + } else { + //Throw error not found + } + } + + let infoDrawer = + contentData && infoContentId ? ( + + ) : null; + return ( <> + {infoDrawer} + {!contentData.isFolder ? ( + + Classifications + + {contentData.classifications.length === 0 ? ( + + None added yet. + + ) : ( + + {contentData.classifications.map((classification, i) => { + const { + code, + systemName, + categoryLabel, + category, + subCategoryLabel, + subCategory, + description, + descriptionLabel, + } = extraClassificationData(classification); + return ( + + +

+ + + + {code} + + {systemName} + + + +

+
+ + + {categoryLabel}: + {category} + + + {subCategoryLabel}: + {subCategory} + + + {descriptionLabel}: + {description} + + +
+ ); + })} +
+ )} +
+ ) : null} + + ); +} + +function extraClassificationData(classification: ContentClassification) { + // For now, we don't have a classification that shares multiple system. + // If we add one that does, we need a better system than concatenating their names, + // but this concatenation will at least show that this combination occurred and a change is needed. + const systemName = classification.subCategories + .map((sc) => sc.category.system.name) + .reduce((acc: string[], c) => (acc.includes(c) ? acc : [...acc, c]), []) + .join(" / "); + + const categories = classification.subCategories + .map((sc) => sc.category.category) + .reduce((acc: string[], c) => (acc.includes(c) ? acc : [...acc, c]), []); + let categoryLabel = + classification.subCategories[0].category.system.categoryLabel; + if (categories.length > 1) { + // for now, all our category labels are pluralized by adding an s... + categoryLabel += "s"; + } + const category = categories.join(" / "); + + const subCategories = classification.subCategories + .map((sc) => sc.subCategory) + .reduce((acc: string[], c) => (acc.includes(c) ? acc : [...acc, c]), []); + let subCategoryLabel = + classification.subCategories[0].category.system.subCategoryLabel; + if (subCategories.length > 1) { + // for now, all our sub-category labels are pluralized by adding an s... + subCategoryLabel += "s"; + } + const subCategory = subCategories.join(" / "); + + const descriptionLabel = + classification.subCategories[0].category.system.descriptionLabel; + + return { + code: classification.code, + systemName, + categoryLabel, + category, + subCategoryLabel, + subCategory, + description: classification.description, + descriptionLabel, + }; +} diff --git a/client/src/Tools/_framework/ToolPanels/ContentInfoDrawer.tsx b/client/src/Tools/_framework/ToolPanels/ContentInfoDrawer.tsx new file mode 100644 index 000000000..d8b4ba573 --- /dev/null +++ b/client/src/Tools/_framework/ToolPanels/ContentInfoDrawer.tsx @@ -0,0 +1,198 @@ +import React, { RefObject, useEffect, useState } from "react"; +import { + Box, + Drawer, + DrawerBody, + DrawerCloseButton, + DrawerContent, + DrawerHeader, + DrawerOverlay, + Tab, + TabList, + TabPanel, + TabPanels, + Tabs, + Text, + Tooltip, +} from "@chakra-ui/react"; +import { + ContentStructure, + DocHistoryItem, + DocRemixItem, +} from "../../../_utils/types"; +import { GeneralContentInfo } from "./GeneralContentInfo"; +import { ClassificationInfo } from "./ClassificationInfo"; +import axios from "axios"; +import { + processContributorHistory, + processRemixes, +} from "../../../_utils/processRemixes"; +import { cidFromText } from "../../../_utils/cid"; +import { RemixedFrom } from "./RemixedFrom"; +import { Remixes } from "./Remixes"; + +export function ContentInfoDrawer({ + isOpen, + onClose, + finalFocusRef, + id, + contentData, + displayTab = "general", +}: { + isOpen: boolean; + onClose: () => void; + finalFocusRef?: RefObject; + id: string; + contentData: ContentStructure; + displayTab?: "general" | "classifications"; +}) { + let initialTabIndex: number; + switch (displayTab) { + case "general": { + initialTabIndex = 0; + break; + } + case "classifications": { + initialTabIndex = 1; + break; + } + } + + const [tabIndex, setTabIndex] = useState(initialTabIndex); + + useEffect(() => { + setTabIndex(initialTabIndex); + }, [displayTab, isOpen]); + + // TODO: this next section (through recalculateThisCid()) is copied almost verbatim from ShareDrawer.tsx + // Refactor to avoid code duplication + + const [contributorHistory, setContributorHistory] = useState< + DocHistoryItem[] | null + >(null); + const [haveChangedHistoryItem, setHaveChangedHistoryItem] = useState(false); + const [remixes, setRemixes] = useState(null); + const [thisCid, setThisCid] = useState(null); + + useEffect(() => { + async function getHistoryAndRemixes() { + const { data } = await axios.get( + `/api/getContributorHistory/${contentData.id}`, + ); + + const hist = await processContributorHistory(data.docHistories[0]); + setContributorHistory(hist); + + let haveChanged = hist.some((dhi) => dhi.prevChanged); + + setHaveChangedHistoryItem(haveChanged); + + const { data: data2 } = await axios.get( + `/api/getRemixes/${contentData.id}`, + ); + + const doc0Remixes = processRemixes(data2.docRemixes[0]); + setRemixes(doc0Remixes); + } + + if (!contentData.isFolder) { + getHistoryAndRemixes(); + } + }, [contentData]); + + useEffect(() => { + async function recalculateThisCid() { + let cid: string | null = null; + if (haveChangedHistoryItem) { + let thisSource = contentData.documents[0].source; + + if (thisSource === undefined) { + const { data: sourceData } = await axios.get( + `/api/getDocumentSource/${contentData.documents[0].id}`, + ); + + thisSource = sourceData.source as string; + } + cid = await cidFromText(thisSource); + } + + setThisCid(cid); + } + + recalculateThisCid(); + }, [haveChangedHistoryItem]); + + return ( + + + + + + {contentData.isFolder ? "Folder" : "Activity"} Information + + + {contentData.name} + + + + + + setTabIndex(index)}> + + General + + {!contentData.isFolder ? ( + <> + + Classifications ({contentData.classifications.length}) + + + Remixed From{" "} + {contributorHistory !== null + ? `(${contributorHistory.length})` + : null} + {haveChangedHistoryItem ? "*" : null} + + + Remixes {remixes !== null ? `(${remixes.length})` : null} + + + ) : null} + + + + + + + {!contentData.isFolder ? ( + + + + ) : null} + {!contentData.isFolder ? ( + + + + ) : null} + {!contentData.isFolder ? ( + + + + ) : null} + + + + + + + ); +} diff --git a/client/src/Tools/_framework/ToolPanels/ContributorsMenu.tsx b/client/src/Tools/_framework/ToolPanels/ContributorsMenu.tsx index 8225f76c4..ad2631b36 100644 --- a/client/src/Tools/_framework/ToolPanels/ContributorsMenu.tsx +++ b/client/src/Tools/_framework/ToolPanels/ContributorsMenu.tsx @@ -59,7 +59,7 @@ export default function ContributorsMenu({ to={`/activityViewer/${contributorHistory[0].prevActivityId}`} aria-label={`Go to ${contributorHistory[0].prevActivityName}`} > - {contributorHistory[0].prevActivityName} by + {contributorHistory[0].prevActivityName} by{" "} {createFullName(contributorHistory[0].prevOwner)} diff --git a/client/src/Tools/_framework/ToolPanels/GeneralContentInfo.tsx b/client/src/Tools/_framework/ToolPanels/GeneralContentInfo.tsx new file mode 100644 index 000000000..70f88ff48 --- /dev/null +++ b/client/src/Tools/_framework/ToolPanels/GeneralContentInfo.tsx @@ -0,0 +1,65 @@ +import React from "react"; +import { Box, List } from "@chakra-ui/react"; +import { ContentStructure } from "../../../_utils/types"; +import { InfoIcon } from "@chakra-ui/icons"; +import { DisplayLicenseItem } from "../../../Widgets/Licenses"; +import { createFullName } from "../../../_utils/names"; + +export function GeneralContentInfo({ + contentData, +}: { + contentData: ContentStructure; +}) { + let license = contentData.license; + let contentType = contentData.isFolder ? "Folder" : "Activity"; + + return ( + + + {license === null ? ( + + {contentType} is shared + without specifying a license. Please select a license below to + inform other how they can use your content. + + ) : license.isComposition ? ( + <> +

+ {contentData.name} by{" "} + {createFullName(contentData.owner!)} is shared with these + licenses: +

+ + {license.composedOf.map((comp) => ( + + ))} + +

+ (You are free to use either of these licenses when reusing this + work.) +

+ + ) : ( + <> +

+ {contentData.name} by{" "} + {createFullName(contentData.owner!)} is shared with the license: +

+ + + + + )} +
+ + {!contentData.isFolder + ? `DoenetML version: ${contentData.documents[0].doenetmlVersion.fullVersion}` + : null} +
+ ); +} From b83692d399d183718c1472fb60dd1dc6d5dc744e Mon Sep 17 00:00:00 2001 From: Duane Nykamp Date: Fri, 20 Dec 2024 22:27:21 -0600 Subject: [PATCH 07/12] rework remix panels --- .../_framework/ToolPanels/RemixedFrom.tsx | 69 +++++++++++++------ .../Tools/_framework/ToolPanels/Remixes.tsx | 44 +++++++++--- client/src/_utils/processRemixes.ts | 22 +++--- client/src/_utils/types.ts | 5 +- 4 files changed, 100 insertions(+), 40 deletions(-) diff --git a/client/src/Tools/_framework/ToolPanels/RemixedFrom.tsx b/client/src/Tools/_framework/ToolPanels/RemixedFrom.tsx index a9d552aba..c2831f23b 100644 --- a/client/src/Tools/_framework/ToolPanels/RemixedFrom.tsx +++ b/client/src/Tools/_framework/ToolPanels/RemixedFrom.tsx @@ -2,6 +2,7 @@ import React from "react"; import { Flex, Hide, + Link as ChakraLink, Show, Spinner, Table, @@ -13,7 +14,9 @@ import { Thead, Tr, VStack, + Box, } from "@chakra-ui/react"; +import { Link as ReactRouterLink } from "react-router-dom"; import { createFullName } from "../../../_utils/names"; import { DateTime } from "luxon"; import { DocHistoryItem } from "../../../_utils/types"; @@ -53,20 +56,20 @@ export function RemixedFrom({ - Activity Name + Remixed Activity Owner - + - Activity Name + Remixed Activity Owner License - Date copied + Date copied @@ -75,29 +78,56 @@ export function RemixedFrom({ let changeText = ""; if (ch.prevChanged) { // The previous doc changed since it was remixed. - // Check if this activity's doc change since then - let thisActivityDocChanged = thisCid !== ch.prevCid; - if (thisActivityDocChanged) { - changeText = - "The original doc changed and so did this one, so would have to merge changes"; - } else { - changeText = - "The original doc changed but this one did not, so could just copy over changes."; - } + changeText = "*Changed since copied"; + // // Check if this activity's doc change since then + // let thisActivityDocChanged = thisCid !== ch.prevCid; + // if (thisActivityDocChanged) { + // changeText = + // "The original doc changed and so did this one, so would have to merge changes"; + // } else { + // changeText = + // "The original doc changed but this one did not, so could just copy over changes."; + // } } else { - changeText = "The original doc is unchanged"; + changeText = ""; } return ( - {ch.prevActivityName} - {createFullName(ch.prevOwner)} + + + + {ch.prevActivityName} + + + + + + + {createFullName(ch.prevOwner)} + + + - {ch.prevActivityName} - {createFullName(ch.prevOwner)} + + {ch.prevActivityName} + + + {createFullName(ch.prevOwner)} + @@ -108,10 +138,9 @@ export function RemixedFrom({ {ch.timestampPrevDoc.toLocaleString(DateTime.DATE_MED)} - + diff --git a/client/src/Tools/_framework/ToolPanels/Remixes.tsx b/client/src/Tools/_framework/ToolPanels/Remixes.tsx index 88949985a..963ac8de1 100644 --- a/client/src/Tools/_framework/ToolPanels/Remixes.tsx +++ b/client/src/Tools/_framework/ToolPanels/Remixes.tsx @@ -2,6 +2,7 @@ import React from "react"; import { Flex, Hide, + Link as ChakraLink, Show, Spinner, Table, @@ -14,6 +15,7 @@ import { Tr, VStack, } from "@chakra-ui/react"; +import { Link as ReactRouterLink } from "react-router-dom"; import { createFullName } from "../../../_utils/names"; import { DateTime } from "luxon"; import { DocRemixItem } from "../../../_utils/types"; @@ -32,7 +34,7 @@ export function Remixes({ remixes }: { remixes: DocRemixItem[] | null }) { } if (remixes.length === 0) { - return No remixes of this activity (yet!); + return No visible remixes of this activity (yet!); } let remixTable = ( @@ -51,7 +53,7 @@ export function Remixes({ remixes }: { remixes: DocRemixItem[] | null }) { Owner - + Activity Name Owner @@ -60,7 +62,7 @@ export function Remixes({ remixes }: { remixes: DocRemixItem[] | null }) { License - Date copied + Date copied @@ -69,14 +71,40 @@ export function Remixes({ remixes }: { remixes: DocRemixItem[] | null }) { return ( - {ch.activityName} - {createFullName(ch.owner)} + + + + {ch.activityName} + + + + + + + {createFullName(ch.owner)} + + + - {ch.activityName} - {createFullName(ch.owner)} + + {ch.activityName} + + + {createFullName(ch.owner)} + @@ -86,7 +114,7 @@ export function Remixes({ remixes }: { remixes: DocRemixItem[] | null }) { {ch.timestampPrevDoc.toLocaleString(DateTime.DATE_MED)} - {ch.isDirect.toString()} + {ch.isDirect ? "direct copy" : ""} ); diff --git a/client/src/_utils/processRemixes.ts b/client/src/_utils/processRemixes.ts index fcc2353f1..9ac740f82 100644 --- a/client/src/_utils/processRemixes.ts +++ b/client/src/_utils/processRemixes.ts @@ -28,19 +28,21 @@ export async function processContributorHistory(hist: { return historyItems; } -export function processRemixes(remixes: { - documentVersions: { contributorHistory: any[] }[]; +export function processRemixes(docRemixes: { + documentVersions: { versionNumber: number; remixes: any[] }[]; + id: string; }): DocRemixItem[] { - let items = remixes.documentVersions + let items = docRemixes.documentVersions .flatMap((dv) => - dv.contributorHistory.map((ch) => { - const { document, ...item } = ch; - const activity = document.activity; + dv.remixes.map((remix) => { + const activity = remix.activity; const remixItem: DocRemixItem = { - ...item, - isDirect: ch.timestampDoc === ch.timestampPrevDoc, - timestampDoc: DateTime.fromISO(ch.timestampDoc), - timestampPrevDoc: DateTime.fromISO(ch.timestampPrevDoc), + ...remix, + prevDocId: docRemixes.id, + prevDocVersionNum: dv.versionNumber, + isDirect: remix.timestampDoc === remix.timestampPrevDoc, + timestampDoc: DateTime.fromISO(remix.timestampDoc), + timestampPrevDoc: DateTime.fromISO(remix.timestampPrevDoc), activityId: activity.id, activityName: activity.name, owner: activity.owner, diff --git a/client/src/_utils/types.ts b/client/src/_utils/types.ts index 0a0e7abfb..4ba8c3d81 100644 --- a/client/src/_utils/types.ts +++ b/client/src/_utils/types.ts @@ -52,6 +52,7 @@ export type ContentClassification = { system: { id: number; name: string; + shortName: string; categoryLabel: string; subCategoryLabel: string; descriptionLabel: string; @@ -96,7 +97,7 @@ export type DocHistoryItem = { docId: string; prevDocId: string; prevDocVersionNum: number; - withLicenseCode: string | null; + withLicenseCode: LicenseCode | null; timestampDoc: DateTime; timestampPrevDoc: DateTime; prevActivityId: string; @@ -110,7 +111,7 @@ export type DocRemixItem = { docId: string; prevDocId: string; prevDocVersionNum: number; - withLicenseCode: string | null; + withLicenseCode: LicenseCode | null; isDirect: boolean; timestampDoc: DateTime; timestampPrevDoc: DateTime; From 58c53a260a45b6bd07b85289fc11f9194158dee7 Mon Sep 17 00:00:00 2001 From: Duane Nykamp Date: Fri, 20 Dec 2024 22:44:55 -0600 Subject: [PATCH 08/12] fix tests --- server/src/model.test.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/server/src/model.test.ts b/server/src/model.test.ts index 80d00501a..abc5a3bee 100644 --- a/server/src/model.test.ts +++ b/server/src/model.test.ts @@ -2627,7 +2627,7 @@ test("contributor history shows only documents user can view", async () => { await makeActivityPublic({ id: activityId4, ownerId: ownerId3, - licenseCode: "CCBYNCSA", + licenseCode: "CCBYSA", }); // owner 3 copies activity 1 to activity 5 and shares it with owner 1 @@ -2636,7 +2636,7 @@ test("contributor history shows only documents user can view", async () => { await shareActivity({ id: activityId5, ownerId: ownerId3, - licenseCode: "CCBYSA", + licenseCode: "CCBYNCSA", users: [ownerId1], }); @@ -2731,7 +2731,7 @@ test("contributor history shows only documents user can view", async () => { expect(docHistory.length).eq(3); expect(docHistory[0].prevDocId).eqls(docId3); expect(docHistory[0].prevDoc.document.activity.id).eqls(activityId3); - expect(docHistory[0].withLicenseCode).eq("CCDUAL"); + expect(docHistory[0].withLicenseCode).eq("CCBYSA"); expect(docHistory[1].prevDocId).eqls(docId2); expect(docHistory[1].prevDoc.document.activity.id).eqls(activityId2); expect(docHistory[1].withLicenseCode).eq("CCBYSA"); @@ -7551,7 +7551,7 @@ test("searchMyFolderContent, handle tags in search", async () => { test("get licenses", async () => { const cc_by_sa = await getLicense("CCBYSA"); - expect(cc_by_sa.name).eq("Creative Commons Attribution-ShareAlike"); + expect(cc_by_sa.name).eq("Creative Commons Attribution-ShareAlike 4.0"); expect(cc_by_sa.imageURL).eq("/creative_commons_by_sa.png"); expect(cc_by_sa.smallImageURL).eq("/creative_commons_by_sa_small.png"); expect(cc_by_sa.licenseURL).eq( @@ -7560,7 +7560,7 @@ test("get licenses", async () => { const cc_by_nc_sa = await getLicense("CCBYNCSA"); expect(cc_by_nc_sa.name).eq( - "Creative Commons Attribution-NonCommercial-ShareAlike", + "Creative Commons Attribution-NonCommercial-ShareAlike 4.0", ); expect(cc_by_nc_sa.imageURL).eq("/creative_commons_by_nc_sa.png"); expect(cc_by_nc_sa.smallImageURL).eq("/creative_commons_by_nc_sa_small.png"); @@ -7570,11 +7570,11 @@ test("get licenses", async () => { const cc_dual = await getLicense("CCDUAL"); expect(cc_dual.name).eq( - "Dual license Creative Commons Attribution-ShareAlike OR Attribution-NonCommercial-ShareAlike", + "Dual license Creative Commons Attribution-ShareAlike 4.0 OR Attribution-NonCommercial-ShareAlike 4.0", ); expect(cc_dual.composedOf[0].name).eq( - "Creative Commons Attribution-ShareAlike", + "Creative Commons Attribution-ShareAlike 4.0", ); expect(cc_dual.composedOf[0].imageURL).eq("/creative_commons_by_sa.png"); expect(cc_dual.composedOf[0].smallImageURL).eq( @@ -7584,7 +7584,7 @@ test("get licenses", async () => { "https://creativecommons.org/licenses/by-sa/4.0/", ); expect(cc_dual.composedOf[1].name).eq( - "Creative Commons Attribution-NonCommercial-ShareAlike", + "Creative Commons Attribution-NonCommercial-ShareAlike 4.0", ); expect(cc_dual.composedOf[1].imageURL).eq("/creative_commons_by_nc_sa.png"); expect(cc_dual.composedOf[1].smallImageURL).eq( @@ -7617,7 +7617,7 @@ test("set license to make public", async () => { expect(activityData.license?.code).eq("CCBYSA"); expect(activityData.license?.name).eq( - "Creative Commons Attribution-ShareAlike", + "Creative Commons Attribution-ShareAlike 4.0", ); expect(activityData.license?.licenseURL).eq( "https://creativecommons.org/licenses/by-sa/4.0/", @@ -7646,7 +7646,7 @@ test("set license to make public", async () => { expect(activityData.license?.code).eq("CCBYNCSA"); expect(activityData.license?.name).eq( - "Creative Commons Attribution-NonCommercial-ShareAlike", + "Creative Commons Attribution-NonCommercial-ShareAlike 4.0", ); expect(activityData.license?.licenseURL).eq( "https://creativecommons.org/licenses/by-nc-sa/4.0/", @@ -7668,12 +7668,12 @@ test("set license to make public", async () => { expect(activityData.license?.code).eq("CCDUAL"); expect(activityData.license?.name).eq( - "Dual license Creative Commons Attribution-ShareAlike OR Attribution-NonCommercial-ShareAlike", + "Dual license Creative Commons Attribution-ShareAlike 4.0 OR Attribution-NonCommercial-ShareAlike 4.0", ); expect(activityData.license?.composedOf[0].code).eq("CCBYSA"); expect(activityData.license?.composedOf[0].name).eq( - "Creative Commons Attribution-ShareAlike", + "Creative Commons Attribution-ShareAlike 4.0", ); expect(activityData.license?.composedOf[0].licenseURL).eq( "https://creativecommons.org/licenses/by-sa/4.0/", @@ -7684,7 +7684,7 @@ test("set license to make public", async () => { expect(activityData.license?.composedOf[1].code).eq("CCBYNCSA"); expect(activityData.license?.composedOf[1].name).eq( - "Creative Commons Attribution-NonCommercial-ShareAlike", + "Creative Commons Attribution-NonCommercial-ShareAlike 4.0", ); expect(activityData.license?.composedOf[1].licenseURL).eq( "https://creativecommons.org/licenses/by-nc-sa/4.0/", From 5e699d3cdfadbf68d4f55b6b9165b360d182557c Mon Sep 17 00:00:00 2001 From: Duane Nykamp Date: Sat, 21 Dec 2024 13:54:11 -0600 Subject: [PATCH 09/12] classification and sharing tests --- .../activityEditor.cy.ts | 7 +- .../e2e/Activities/activityViewer.cy.ts | 60 +++++++ .../cypress/e2e/Activities/codeViewer.cy.ts | 38 ++++ .../e2e/Activities/sharingActivities.cy.ts | 4 +- .../classifications.cy.ts | 7 +- client/cypress/e2e/SettingsPanels/share.cy.ts | 170 ++++++++++++++++++ client/cypress/support/commands.ts | 41 ++++- .../Tools/_framework/Paths/ActivityViewer.tsx | 3 +- .../src/Tools/_framework/Paths/CodeViewer.tsx | 1 + .../_framework/ToolPanels/ShareDrawer.tsx | 2 +- .../_framework/ToolPanels/ShareSettings.tsx | 7 +- server/src/index.ts | 8 + server/src/test_apis.ts | 58 ++++++ 13 files changed, 393 insertions(+), 13 deletions(-) rename client/cypress/e2e/{ActivityEditor => Activities}/activityEditor.cy.ts (85%) create mode 100644 client/cypress/e2e/Activities/activityViewer.cy.ts create mode 100644 client/cypress/e2e/Activities/codeViewer.cy.ts rename client/cypress/e2e/{SettingsPanel => SettingsPanels}/classifications.cy.ts (93%) create mode 100644 client/cypress/e2e/SettingsPanels/share.cy.ts create mode 100644 server/src/test_apis.ts diff --git a/client/cypress/e2e/ActivityEditor/activityEditor.cy.ts b/client/cypress/e2e/Activities/activityEditor.cy.ts similarity index 85% rename from client/cypress/e2e/ActivityEditor/activityEditor.cy.ts rename to client/cypress/e2e/Activities/activityEditor.cy.ts index 7836f414f..02175d8d1 100644 --- a/client/cypress/e2e/ActivityEditor/activityEditor.cy.ts +++ b/client/cypress/e2e/Activities/activityEditor.cy.ts @@ -1,11 +1,14 @@ -describe("Create Folders Tests", function () { +describe("Activity Editor Tests", function () { it("correctly restore editor state after clicking view", () => { // test bug where activity editor was not restoring itself with the correct state // after one switched to view mode and back cy.loginAsTestUser(); - cy.createActivity("Hello!", "Initial content").then((activityId) => { + cy.createActivity({ + activityName: "Hello!", + doenetML: "Initial content", + }).then((activityId) => { cy.visit(`/activityEditor/${activityId}`); cy.iframe() diff --git a/client/cypress/e2e/Activities/activityViewer.cy.ts b/client/cypress/e2e/Activities/activityViewer.cy.ts new file mode 100644 index 000000000..17956bc83 --- /dev/null +++ b/client/cypress/e2e/Activities/activityViewer.cy.ts @@ -0,0 +1,60 @@ +describe("Activity Viewer Tests", function () { + it("classifications shown in activity viewer", () => { + cy.loginAsTestUser(); + cy.createActivity({ + activityName: "Classifications!", + doenetML: "Hi!", + classifications: [ + { + systemShortName: "WeBWorK", + category: "Algebra", + subCategory: "Factoring", + code: "Alg.F.2", + }, + { + systemShortName: "Common Core", + category: "HS", + subCategory: + "Seeing Structure in Expressions. Write expressions in equivalent forms to solve problems.", + code: "A.SSE.3 a.", + }, + ], + }).then((activityId) => { + cy.visit(`/activityViewer/${activityId}`); + + cy.get('[data-test="Classifications Footer"]').should( + "contain.text", + "Alg.F.2", + ); + cy.get('[data-test="Classifications Footer"]').should( + "contain.text", + "A.SSE.3 a.", + ); + cy.get('[data-test="Classifications Footer"]').click(); + + cy.get('[data-test="Classification 1"]').should( + "contain.text", + "A.SSE.3 a.", + ); + cy.get('[data-test="Classification 2"]').should( + "contain.text", + "Alg.F.2", + ); + + cy.get('[data-test="Close Settings Button"]').click(); + cy.get('[data-test="Classification 1"]').should("not.exist"); + + cy.get('[data-test="Activity Information"]').click(); + + cy.get('[data-test="Classifications"]').click(); + cy.get('[data-test="Classification 1"]').should( + "contain.text", + "A.SSE.3 a.", + ); + cy.get('[data-test="Classification 2"]').should( + "contain.text", + "Alg.F.2", + ); + }); + }); +}); diff --git a/client/cypress/e2e/Activities/codeViewer.cy.ts b/client/cypress/e2e/Activities/codeViewer.cy.ts new file mode 100644 index 000000000..1163cbe90 --- /dev/null +++ b/client/cypress/e2e/Activities/codeViewer.cy.ts @@ -0,0 +1,38 @@ +describe("Code Viewer Tests", function () { + it("classifications shown in code viewer", () => { + cy.loginAsTestUser(); + cy.createActivity({ + activityName: "Classifications!", + doenetML: "Hi!", + classifications: [ + { + systemShortName: "WeBWorK", + category: "Algebra", + subCategory: "Factoring", + code: "Alg.F.2", + }, + { + systemShortName: "Common Core", + category: "HS", + subCategory: + "Seeing Structure in Expressions. Write expressions in equivalent forms to solve problems.", + code: "A.SSE.3 a.", + }, + ], + }).then((activityId) => { + cy.visit(`/codeViewer/${activityId}`); + + cy.get('[data-test="Activity Information"]').click(); + + cy.get('[data-test="Classifications"]').click(); + cy.get('[data-test="Classification 1"]').should( + "contain.text", + "A.SSE.3 a.", + ); + cy.get('[data-test="Classification 2"]').should( + "contain.text", + "Alg.F.2", + ); + }); + }); +}); diff --git a/client/cypress/e2e/Activities/sharingActivities.cy.ts b/client/cypress/e2e/Activities/sharingActivities.cy.ts index 8ede5621c..6afce47af 100644 --- a/client/cypress/e2e/Activities/sharingActivities.cy.ts +++ b/client/cypress/e2e/Activities/sharingActivities.cy.ts @@ -31,8 +31,8 @@ describe("Share Activities Tests", function () { cy.get('[data-test="Sharing Button"]').click(); cy.get('[data-test="Public Checkbox"]').click(); cy.get('[data-test="Status message"]').should( - "have.text", - "Successfully shared publicly", + "contain.text", + "shared publicly", ); cy.get('[data-test="Close Share Drawer Button"]').click(); diff --git a/client/cypress/e2e/SettingsPanel/classifications.cy.ts b/client/cypress/e2e/SettingsPanels/classifications.cy.ts similarity index 93% rename from client/cypress/e2e/SettingsPanel/classifications.cy.ts rename to client/cypress/e2e/SettingsPanels/classifications.cy.ts index c8ba96be7..9ba1d6974 100644 --- a/client/cypress/e2e/SettingsPanel/classifications.cy.ts +++ b/client/cypress/e2e/SettingsPanels/classifications.cy.ts @@ -2,7 +2,10 @@ describe("Classifications test", function () { it("add classifications to activity", () => { cy.loginAsTestUser(); - cy.createActivity("Hello!", "Initial content").then((activityId) => { + cy.createActivity({ + activityName: "Hello!", + doenetML: "Initial content", + }).then((activityId) => { cy.visit(`/activityEditor/${activityId}`); cy.get('[data-test="Settings Button"]').click(); @@ -87,7 +90,7 @@ describe("Classifications test", function () { cy.get('[data-test="Add 9.2.3.3"]').should("not.exist"); cy.get('[data-test="Stop Filter By System').click(); - cy.get('[data-test="Add 9.2.3.3"]').should("be.visible"); + cy.get('[data-test="Add 9.2.3.3"]').scrollIntoView().should("be.visible"); }); }); }); diff --git a/client/cypress/e2e/SettingsPanels/share.cy.ts b/client/cypress/e2e/SettingsPanels/share.cy.ts new file mode 100644 index 000000000..5e386d1c0 --- /dev/null +++ b/client/cypress/e2e/SettingsPanels/share.cy.ts @@ -0,0 +1,170 @@ +describe("Classifications test", function () { + it("cannot select incompatible license after remix, ShareAlike", () => { + let code = Date.now().toString(); + const scrappyEmail = `scrappy${code}@doo`; + const scoobyEmail = `scooby${code}@doo`; + + cy.loginAsTestUser({ + email: scoobyEmail, + firstNames: "Scooby", + lastNames: "Doo", + }); + + cy.createActivity({ + activityName: "Share alike", + doenetML: "Shared with ShareAlike", + }).then((activityId) => { + cy.visit(`/activityEditor/${activityId}`); + + cy.get('[data-test="Sharing Button"]').click(); + + cy.get('[data-test="Public Checkbox"]').click(); + cy.get('[data-test="Status message"]').should( + "contain.text", + "shared publicly", + ); + + cy.get('[data-test="Select License"]').select( + "Creative Commons Attribution-ShareAlike 4.0", + ); + cy.get('[data-test="Status message"]').should( + "contain.text", + "changed license", + ); + + cy.loginAsTestUser({ + email: scrappyEmail, + firstNames: "Scrappy", + lastNames: "Doo", + }); + + cy.visit(`/activityViewer/${activityId}`); + cy.get('[data-test="Copy to Activities Button"]').click(); + cy.get('[data-test="Go to Activities"]').click(); + + cy.get('[data-test="Card Menu Button"]').eq(0).click(); + cy.get('[data-test="Share Menu Item"]').click(); + + cy.get('[data-test="Cannot Change License"]').should( + "contain.text", + "Creative Commons Attribution-ShareAlike 4.0", + ); + cy.get('[data-test="Cannot Change License"]').should( + "contain.text", + "Cannot change license", + ); + cy.get('[data-test="Select License"]').should("not.exist"); + }); + }); + + it("cannot select incompatible license after remix, NonCommercial-ShareAlike", () => { + let code = Date.now().toString(); + const scrappyEmail = `scrappy${code}@doo`; + const scoobyEmail = `scooby${code}@doo`; + + cy.loginAsTestUser({ + email: scoobyEmail, + firstNames: "Scooby", + lastNames: "Doo", + }); + + cy.createActivity({ + activityName: "Non-commercial share alike", + doenetML: "Shared with NonCommercial-ShareAlike", + }).then((activityId) => { + cy.visit(`/activityEditor/${activityId}`); + + cy.get('[data-test="Sharing Button"]').click(); + + cy.get('[data-test="Public Checkbox"]').click(); + cy.get('[data-test="Status message"]').should( + "contain.text", + "shared publicly", + ); + + cy.get('[data-test="Select License"]').select( + "Creative Commons Attribution-NonCommercial-ShareAlike 4.0", + ); + cy.get('[data-test="Status message"]').should( + "contain.text", + "changed license", + ); + + cy.loginAsTestUser({ + email: scrappyEmail, + firstNames: "Scrappy", + lastNames: "Doo", + }); + + cy.visit(`/activityViewer/${activityId}`); + cy.get('[data-test="Copy to Activities Button"]').click(); + cy.get('[data-test="Go to Activities"]').click(); + + cy.get('[data-test="Card Menu Button"]').eq(0).click(); + cy.get('[data-test="Share Menu Item"]').click(); + + cy.get('[data-test="Cannot Change License"]').should( + "contain.text", + "Creative Commons Attribution-NonCommercial-ShareAlike 4.0", + ); + cy.get('[data-test="Cannot Change License"]').should( + "contain.text", + "Cannot change license", + ); + cy.get('[data-test="Select License"]').should("not.exist"); + }); + }); + + it("can select license after remix, Dual License", () => { + let code = Date.now().toString(); + const scrappyEmail = `scrappy${code}@doo`; + const scoobyEmail = `scooby${code}@doo`; + + cy.loginAsTestUser({ + email: scoobyEmail, + firstNames: "Scooby", + lastNames: "Doo", + }); + + cy.createActivity({ + activityName: "Dual license", + doenetML: "Shared with Dual License", + }).then((activityId) => { + cy.visit(`/activityEditor/${activityId}`); + + cy.get('[data-test="Sharing Button"]').click(); + + cy.get('[data-test="Public Checkbox"]').click(); + cy.get('[data-test="Status message"]').should( + "contain.text", + "shared publicly", + ); + + cy.get('[data-test="Select License"]').should( + "contain.text", + "Dual license Creative Commons Attribution-ShareAlike 4.0 OR Attribution-NonCommercial-ShareAlike 4.0", + ); + + cy.loginAsTestUser({ + email: scrappyEmail, + firstNames: "Scrappy", + lastNames: "Doo", + }); + + cy.visit(`/activityViewer/${activityId}`); + cy.get('[data-test="Copy to Activities Button"]').click(); + cy.get('[data-test="Go to Activities"]').click(); + + cy.get('[data-test="Card Menu Button"]').eq(0).click(); + cy.get('[data-test="Share Menu Item"]').click(); + + cy.get('[data-test="Select License"]').select( + "Creative Commons Attribution-ShareAlike 4.0", + ); + cy.get('[data-test="Status message"]').should( + "contain.text", + "changed license", + ); + }); + }); +}); diff --git a/client/cypress/support/commands.ts b/client/cypress/support/commands.ts index a8be6ff7e..efba33a41 100644 --- a/client/cypress/support/commands.ts +++ b/client/cypress/support/commands.ts @@ -47,7 +47,20 @@ declare global { /** * Custom command to create an activity for the logged in user */ - createActivity(activityName: string, doenetML: string): Chainable; + createActivity({ + activityName, + doenetML, + classifications, + }: { + activityName: string; + doenetML: string; + classifications?: { + systemShortName: string; + category: string; + subCategory: string; + code: string; + }[]; + }): Chainable; } } } @@ -78,7 +91,20 @@ Cypress.Commands.add( Cypress.Commands.add( "createActivity", - (activityName: string, doenetML: string) => { + ({ + activityName, + doenetML, + classifications, + }: { + activityName: string; + doenetML: string; + classifications?: { + systemShortName: string; + category: string; + subCategory: string; + code: string; + }[]; + }) => { cy.request({ method: "POST", url: "/api/createActivity", @@ -86,6 +112,17 @@ Cypress.Commands.add( let activityId: string = resp.body.activityId; let docId: string = resp.body.docId; + if (classifications) { + cy.request({ + method: "POST", + url: "/api/test/addClassificationsByNames", + body: { + id: activityId, + classifications, + }, + }); + } + cy.request({ method: "POST", url: "/api/updateContentName", diff --git a/client/src/Tools/_framework/Paths/ActivityViewer.tsx b/client/src/Tools/_framework/Paths/ActivityViewer.tsx index 4d535e1d9..d3ae44bab 100644 --- a/client/src/Tools/_framework/Paths/ActivityViewer.tsx +++ b/client/src/Tools/_framework/Paths/ActivityViewer.tsx @@ -240,6 +240,7 @@ export function ActivityViewer() { colorScheme="blue" icon={} aria-label="Activity information" + data-test="Activity Information" onClick={() => { setDisplayInfoTab("general"); infoOnOpen(); @@ -352,7 +353,7 @@ export function ActivityViewer() { }} > Classifications - + {activity.classifications.map((classification) => { return ( diff --git a/client/src/Tools/_framework/Paths/CodeViewer.tsx b/client/src/Tools/_framework/Paths/CodeViewer.tsx index 433699004..a7e7fb025 100644 --- a/client/src/Tools/_framework/Paths/CodeViewer.tsx +++ b/client/src/Tools/_framework/Paths/CodeViewer.tsx @@ -222,6 +222,7 @@ export function CodeViewer() { colorScheme="blue" icon={} aria-label="Activity information" + data-test="Activity Information" onClick={() => { infoOnOpen(); }} diff --git a/client/src/Tools/_framework/ToolPanels/ShareDrawer.tsx b/client/src/Tools/_framework/ToolPanels/ShareDrawer.tsx index d9643cd8d..aee3b0ca3 100644 --- a/client/src/Tools/_framework/ToolPanels/ShareDrawer.tsx +++ b/client/src/Tools/_framework/ToolPanels/ShareDrawer.tsx @@ -86,7 +86,7 @@ export function ShareDrawer({ setHaveChangedHistoryItem(haveChanged); - setRemixedWithLicense(hist[0].withLicenseCode || null); + setRemixedWithLicense(hist[0]?.withLicenseCode || null); const { data: data2 } = await axios.get( `/api/getRemixes/${contentData.id}`, diff --git a/client/src/Tools/_framework/ToolPanels/ShareSettings.tsx b/client/src/Tools/_framework/ToolPanels/ShareSettings.tsx index 703a97cbd..0c870e3e5 100644 --- a/client/src/Tools/_framework/ToolPanels/ShareSettings.tsx +++ b/client/src/Tools/_framework/ToolPanels/ShareSettings.tsx @@ -249,7 +249,7 @@ export function ShareSettings({ (l) => l.code === remixedWithLicense, )?.name; chooseLicenseForm = ( - +

License: {licenseName}

(Cannot change license since remixed from activity with this license.) @@ -265,6 +265,7 @@ export function ShareSettings({