Skip to content
Merged
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
137 changes: 137 additions & 0 deletions front/src/components/Dialogs/TaskExportDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<template>
<q-dialog ref="dialogRef" @hide="onDialogHide">
<q-card class="ctfnote-dialog">
<q-card-section class="row">
<div class="text-h6">Export tasks</div>
<q-space />
<q-btn v-close-popup icon="close" flat round dense />
</q-card-section>
<q-card-section>
<div>
<q-select
v-model="currentOption"
class="full-width"
label="Export Type"
:options="exportOptions"
/>
<q-input v-model="teamName" label="Team name" />
</div>
</q-card-section>
<q-card-actions class="q-pr-md q-pb-md" align="right">
<q-btn flat color="warning" label="Cancel" @click="backClick" />
<q-btn color="positive" label="Export" @click="btnClick" />
</q-card-actions>
</q-card>
</q-dialog>
</template>

<script lang="ts">
import { useDialogPluginComponent } from 'quasar';
import { Ctf, Task } from 'src/ctfnote/models';
import { defineComponent, ref } from 'vue';

export default defineComponent({
props: {
ctf: { type: Object as () => Ctf, required: true },
},
emits: useDialogPluginComponent.emits,
setup() {
const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
useDialogPluginComponent();

const exportOptions = ['Solved tasks only', 'All tasks'];

return {
model: ref(''),
currentOption: ref(exportOptions[0]),
teamName: ref(''),
exportOptions,
dialogRef,
onDialogHide,
onDialogOK,
onDialogCancel,
};
},
computed: {},
methods: {
backClick() {
this.onDialogCancel();
},

async downloadTaskMarkdown(task: Task) {
const result = await fetch(task.padUrl + '/download/markdown');
let markdown = await result.text();
if (markdown.trimStart().substring(0, 1) != '#') {
//does not start with a title, manually adding...
markdown = `# ${task.title} - ${task.category}\n` + markdown;
}
return markdown;
},

async exportTasks(tasks: Task[]) {
let template = '';

// Add CTF title
template += `${this.ctf.title}\n===\n\n`;
// Add CTF date
template += `${this.ctf.startTime.toUTCString()} - ${this.ctf.endTime.toUTCString()} \n`;
// Add team name
if (this.teamName != '') {
template += `Team: ${this.teamName}\n`;
}

// Add flags table
const flagTasks = tasks.filter((f) => f.flag != '');
if (flagTasks.length > 0) {
template += '\n| Task | Flag |\n|---|---|\n';
template += flagTasks
.map((flagTask) => `| ${flagTask.title} | ${flagTask.flag} |`)
.join('\n');
}

template += '\n\n\n';

// Add tasks
template += (
await Promise.all(
tasks.flatMap((task) => this.downloadTaskMarkdown(task), this)
)
).join('\n\n');

// download
const blob = new Blob([template], {
type: 'text/markdown',
});
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = this.ctf.title + '.md';
link.click();
URL.revokeObjectURL(link.href);
},

btnClick() {
let tasks = this.ctf.tasks;
if (this.currentOption == 'Solved tasks only') {
tasks = tasks.filter((t) => t.solved);
}

return this.exportTasks(
tasks
.sort((a, b) =>
a.title.toLowerCase().localeCompare(b.title.toLowerCase())
)
.sort((a, b) =>
a.category.toLowerCase().localeCompare(b.category.toLowerCase())
)
);
},
},
});
</script>

<style scoped>
.q-dialog-plugin {
width: 800px;
max-width: 90vw !important;
}
</style>
16 changes: 16 additions & 0 deletions front/src/components/Task/TaskList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,13 @@
label="Import "
@click="openImportTaskDialog"
/>
<q-fab-action
color="accent"
push
icon="file_download"
label="Export "
@click="openExportTasksDialog"
/>
</q-fab>
</q-page-sticky>
</div>
Expand All @@ -102,6 +109,7 @@ import { useStoredSettings } from 'src/extensions/storedSettings';
import { defineComponent, ref, provide } from 'vue';
import TaskEditDialogVue from '../Dialogs/TaskEditDialog.vue';
import TaskImportDialogVue from '../Dialogs/TaskImportDialog.vue';
import TaskExportDialogVue from '../Dialogs/TaskExportDialog.vue';
import TaskCards from './TaskCards.vue';
import TaskTable from './TaskTable.vue';
import { useQuasar } from 'quasar';
Expand Down Expand Up @@ -257,6 +265,14 @@ export default defineComponent({
},
});
},
openExportTasksDialog() {
this.$q.dialog({
component: TaskExportDialogVue,
componentProps: {
ctf: this.ctf,
},
});
},
},
});
</script>