Files
CTFd/CTFd/themes/core/assets/js/graphs.js
Kevin Chung 9264e96428 Mark 3.1.0 (#1634)
# 3.1.0 / 2020-09-08

**General**

- Loosen team password confirmation in team settings to also accept the team captain's password to make it easier to change the team password
- Adds the ability to add custom user and team fields for registration/profile settings.
- Improve Notifications pubsub events system to use a subscriber per server instead of a subscriber per browser. This should improve the reliability of CTFd at higher load and make it easier to deploy the Notifications system

**Admin Panel**

- Add a comments functionality for admins to discuss challenges, users, teams, pages
- Adds a legal section in Configs where users can add a terms of service and privacy policy
- Add a Custom Fields section in Configs where admins can add/edit custom user/team fields
- Move user graphs into a modal for Admin Panel

**API**

- Add `/api/v1/comments` to manipulate and create comments

**Themes**

- Make scoreboard caching only cache the score table instead of the entire page. This is done by caching the specific template section. Refer to #1586, specifically the changes in `scoreboard.html`.
- Add rel=noopener to external links to prevent tab napping attacks
- Change the registration page to reference links to Terms of Service and Privacy Policy if specified in configuration

**Miscellaneous**

- Make team settings modal larger in the core theme
- Update tests in Github Actions to properly test under MySQL and Postgres
- Make gevent default in serve.py and add a `--disable-gevent` switch in serve.py
- Add `tenacity` library for retrying logic
- Add `pytest-sugar` for slightly prettier pytest output
- Add a `listen()` method to `CTFd.utils.events.EventManager` and `CTFd.utils.events.RedisEventManager`.
  - This method should implement subscription for a CTFd worker to whatever underlying notification system there is. This should be implemented with gevent or a background thread.
  - The `subscribe()` method (which used to implement the functionality of the new `listen()` function) now only handles passing notifications from CTFd to the browser. This should also be implemented with gevent or a background thread.
2020-09-08 00:08:35 -04:00

325 lines
7.4 KiB
JavaScript

import $ from "jquery";
import echarts from "echarts/dist/echarts-en.common";
import Moment from "moment";
import { cumulativeSum, colorHash } from "./utils";
const graph_configs = {
score_graph: {
format: (type, id, name, _account_id, responses) => {
let option = {
title: {
left: "center",
text: "Score over Time"
},
tooltip: {
trigger: "axis",
axisPointer: {
type: "cross"
}
},
legend: {
type: "scroll",
orient: "horizontal",
align: "left",
bottom: 0,
data: [name]
},
toolbox: {
feature: {
saveAsImage: {}
}
},
grid: {
containLabel: true
},
xAxis: [
{
type: "category",
boundaryGap: false,
data: []
}
],
yAxis: [
{
type: "value"
}
],
series: []
};
const times = [];
const scores = [];
const solves = responses[0].data;
const awards = responses[2].data;
const total = solves.concat(awards);
total.sort((a, b) => {
return new Date(a.date) - new Date(b.date);
});
for (let i = 0; i < total.length; i++) {
const date = Moment(total[i].date);
times.push(date.toDate());
try {
scores.push(total[i].challenge.value);
} catch (e) {
scores.push(total[i].value);
}
}
times.forEach(time => {
option.xAxis[0].data.push(time);
});
option.series.push({
name: window.stats_data.name,
type: "line",
label: {
normal: {
show: true,
position: "top"
}
},
areaStyle: {
normal: {
color: colorHash(name + id)
}
},
itemStyle: {
normal: {
color: colorHash(name + id)
}
},
data: cumulativeSum(scores)
});
return option;
}
},
category_breakdown: {
format: (type, id, name, account_id, responses) => {
let option = {
title: {
left: "center",
text: "Category Breakdown"
},
tooltip: {
trigger: "item"
},
toolbox: {
show: true,
feature: {
saveAsImage: {}
}
},
legend: {
orient: "horizontal",
bottom: 0,
data: []
},
series: [
{
name: "Category Breakdown",
type: "pie",
radius: ["30%", "50%"],
avoidLabelOverlap: false,
label: {
show: false,
position: "center"
},
itemStyle: {
normal: {
label: {
show: true,
formatter: function(data) {
return `${data.name} - ${data.value} (${data.percent}%)`;
}
},
labelLine: {
show: true
}
},
emphasis: {
label: {
show: true,
position: "center",
textStyle: {
fontSize: "14",
fontWeight: "normal"
}
}
}
},
emphasis: {
label: {
show: true,
fontSize: "30",
fontWeight: "bold"
}
},
labelLine: {
show: false
},
data: []
}
]
};
const solves = responses[0].data;
const categories = [];
for (let i = 0; i < solves.length; i++) {
categories.push(solves[i].challenge.category);
}
const keys = categories.filter((elem, pos) => {
return categories.indexOf(elem) == pos;
});
const counts = [];
for (let i = 0; i < keys.length; i++) {
let count = 0;
for (let x = 0; x < categories.length; x++) {
if (categories[x] == keys[i]) {
count++;
}
}
counts.push(count);
}
keys.forEach((category, index) => {
option.legend.data.push(category);
option.series[0].data.push({
value: counts[index],
name: category,
itemStyle: { color: colorHash(category) }
});
});
return option;
}
},
solve_percentages: {
format: (type, id, name, account_id, responses) => {
const solves_count = responses[0].data.length;
const fails_count = responses[1].meta.count;
let option = {
title: {
left: "center",
text: "Solve Percentages"
},
tooltip: {
trigger: "item"
},
toolbox: {
show: true,
feature: {
saveAsImage: {}
}
},
legend: {
orient: "horizontal",
bottom: 0,
data: ["Fails", "Solves"]
},
series: [
{
name: "Solve Percentages",
type: "pie",
radius: ["30%", "50%"],
avoidLabelOverlap: false,
label: {
show: false,
position: "center"
},
itemStyle: {
normal: {
label: {
show: true,
formatter: function(data) {
return `${data.name} - ${data.value} (${data.percent}%)`;
}
},
labelLine: {
show: true
}
},
emphasis: {
label: {
show: true,
position: "center",
textStyle: {
fontSize: "14",
fontWeight: "normal"
}
}
}
},
emphasis: {
label: {
show: true,
fontSize: "30",
fontWeight: "bold"
}
},
labelLine: {
show: false
},
data: [
{
value: fails_count,
name: "Fails",
itemStyle: { color: "rgb(207, 38, 0)" }
},
{
value: solves_count,
name: "Solves",
itemStyle: { color: "rgb(0, 209, 64)" }
}
]
}
]
};
return option;
}
}
};
export function createGraph(
graph_type,
target,
data,
type,
id,
name,
account_id
) {
const cfg = graph_configs[graph_type];
let chart = echarts.init(document.querySelector(target));
chart.setOption(cfg.format(type, id, name, account_id, data));
$(window).on("resize", function() {
if (chart != null && chart != undefined) {
chart.resize();
}
});
}
export function updateGraph(
graph_type,
target,
data,
type,
id,
name,
account_id
) {
const cfg = graph_configs[graph_type];
let chart = echarts.init(document.querySelector(target));
chart.setOption(cfg.format(type, id, name, account_id, data));
}
export function disposeGraph(target) {
echarts.dispose(document.querySelector(target));
}