mirror of
https://github.com/aljazceru/CTFd.git
synced 2025-12-18 14:34:21 +01:00
Start to finish up interface for fields
This commit is contained in:
@@ -5,6 +5,7 @@ from CTFd.api.v1.awards import awards_namespace
|
||||
from CTFd.api.v1.challenges import challenges_namespace
|
||||
from CTFd.api.v1.comments import comments_namespace
|
||||
from CTFd.api.v1.config import configs_namespace
|
||||
from CTFd.api.v1.fields import fields_namespace
|
||||
from CTFd.api.v1.files import files_namespace
|
||||
from CTFd.api.v1.flags import flags_namespace
|
||||
from CTFd.api.v1.hints import hints_namespace
|
||||
@@ -50,3 +51,4 @@ CTFd_API_v1.add_namespace(pages_namespace, "/pages")
|
||||
CTFd_API_v1.add_namespace(unlocks_namespace, "/unlocks")
|
||||
CTFd_API_v1.add_namespace(tokens_namespace, "/tokens")
|
||||
CTFd_API_v1.add_namespace(comments_namespace, "/comments")
|
||||
CTFd_API_v1.add_namespace(fields_namespace, "/fields")
|
||||
|
||||
101
CTFd/api/v1/fields.py
Normal file
101
CTFd/api/v1/fields.py
Normal file
@@ -0,0 +1,101 @@
|
||||
from typing import List
|
||||
|
||||
from flask import request, session
|
||||
from flask_restx import Namespace, Resource
|
||||
|
||||
from CTFd.api.v1.helpers.models import build_model_filters
|
||||
from CTFd.api.v1.helpers.request import validate_args
|
||||
from CTFd.api.v1.helpers.schemas import sqlalchemy_to_pydantic
|
||||
from CTFd.api.v1.schemas import APIDetailedSuccessResponse, APIListSuccessResponse
|
||||
from CTFd.constants import RawEnum
|
||||
from CTFd.models import Fields, db
|
||||
from CTFd.schemas.fields import FieldSchema
|
||||
from CTFd.utils.decorators import admins_only
|
||||
|
||||
fields_namespace = Namespace("fields", description="Endpoint to retrieve Fields")
|
||||
|
||||
|
||||
@fields_namespace.route("")
|
||||
class FieldList(Resource):
|
||||
@admins_only
|
||||
@validate_args(
|
||||
{
|
||||
"type": (str, None),
|
||||
"q": (str, None),
|
||||
"field": (RawEnum("FieldFields", {"description": "description"}), None),
|
||||
},
|
||||
location="query",
|
||||
)
|
||||
def get(self, query_args):
|
||||
q = query_args.pop("q", None)
|
||||
field = str(query_args.pop("field", None))
|
||||
filters = build_model_filters(model=Fields, query=q, field=field)
|
||||
|
||||
fields = Fields.query.filter_by(**query_args).filter(*filters).all()
|
||||
schema = FieldSchema(many=True)
|
||||
|
||||
response = schema.dump(fields)
|
||||
|
||||
if response.errors:
|
||||
return {"success": False, "errors": response.errors}, 400
|
||||
|
||||
return {"success": True, "data": response.data}
|
||||
|
||||
@admins_only
|
||||
def post(self):
|
||||
req = request.get_json()
|
||||
schema = FieldSchema()
|
||||
response = schema.load(req, session=db.session)
|
||||
|
||||
if response.errors:
|
||||
return {"success": False, "errors": response.errors}, 400
|
||||
|
||||
db.session.add(response.data)
|
||||
db.session.commit()
|
||||
|
||||
response = schema.dump(response.data)
|
||||
db.session.close()
|
||||
|
||||
return {"success": True, "data": response.data}
|
||||
|
||||
|
||||
@fields_namespace.route("/<field_id>")
|
||||
class Field(Resource):
|
||||
@admins_only
|
||||
def get(self, field_id):
|
||||
field = Fields.query.filter_by(id=field_id).first_or_404()
|
||||
schema = FieldSchema()
|
||||
|
||||
response = schema.dump(field)
|
||||
|
||||
if response.errors:
|
||||
return {"success": False, "errors": response.errors}, 400
|
||||
|
||||
return {"success": True, "data": response.data}
|
||||
|
||||
@admins_only
|
||||
def patch(self, field_id):
|
||||
field = Fields.query.filter_by(id=field_id).first_or_404()
|
||||
schema = FieldSchema()
|
||||
|
||||
req = request.get_json()
|
||||
|
||||
response = schema.load(req, session=db.session, instance=field)
|
||||
if response.errors:
|
||||
return {"success": False, "errors": response.errors}, 400
|
||||
|
||||
db.session.commit()
|
||||
|
||||
response = schema.dump(response.data)
|
||||
db.session.close()
|
||||
|
||||
return {"success": True, "data": response.data}
|
||||
|
||||
@admins_only
|
||||
def delete(self, field_id):
|
||||
field = Fields.query.filter_by(id=field_id).first_or_404()
|
||||
db.session.delete(field)
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
|
||||
return {"success": True}
|
||||
@@ -1,6 +1,5 @@
|
||||
<template>
|
||||
<div class="border-bottom">
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
@@ -16,17 +15,22 @@
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<label>Field Type</label>
|
||||
<select class="form-control custom-select">
|
||||
<option>Text Field</option>
|
||||
<option>Checkbox</option>
|
||||
<select
|
||||
class="form-control custom-select"
|
||||
v-model.lazy="field.field_type"
|
||||
>
|
||||
<option value="text">Text Field</option>
|
||||
<option value="checkbox">Checkbox</option>
|
||||
</select>
|
||||
<small class="form-text text-muted">Type of field shown to the user</small>
|
||||
<small class="form-text text-muted"
|
||||
>Type of field shown to the user</small
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
<div class="form-group">
|
||||
<label>Field Name</label>
|
||||
<input type="text" class="form-control" v-model.lazy="field.name">
|
||||
<input type="text" class="form-control" v-model.lazy="field.name" />
|
||||
<small class="form-text text-muted">Field name</small>
|
||||
</div>
|
||||
</div>
|
||||
@@ -34,25 +38,46 @@
|
||||
<div class="col-md-12">
|
||||
<div class="form-group">
|
||||
<label>Field Description</label>
|
||||
<input type="text" class="form-control" v-model.lazy="field.description">
|
||||
<small id="emailHelp" class="form-text text-muted">Field Description</small>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
v-model.lazy="field.description"
|
||||
/>
|
||||
<small id="emailHelp" class="form-text text-muted"
|
||||
>Field Description</small
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12">
|
||||
<div class="form-check">
|
||||
<label class="form-check-label">
|
||||
<input class="form-check-input" type="checkbox" v-model.lazy="field.editable"> Editable by user in profile
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
v-model.lazy="field.editable"
|
||||
/>
|
||||
Editable by user in profile
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<label class="form-check-label">
|
||||
<input class="form-check-input" type="checkbox" v-model.lazy="field.required"> Required on registration
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
v-model.lazy="field.required"
|
||||
/>
|
||||
Required on registration
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<label class="form-check-label">
|
||||
<input class="form-check-input" type="checkbox" v-model.lazy="field.public"> Shown on public profile
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
v-model.lazy="field.public"
|
||||
/>
|
||||
Shown on public profile
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -75,6 +100,9 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CTFd from "core/CTFd";
|
||||
import { ezToast } from "core/ezq";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
index: Number,
|
||||
@@ -83,20 +111,90 @@ export default {
|
||||
data: function() {
|
||||
return {
|
||||
field: this.initialField
|
||||
}
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
saveField: function(){
|
||||
console.log(this.field)
|
||||
// Update field in API
|
||||
persistedField: function() {
|
||||
// We're using Math.random() for unique IDs so new items have IDs < 1
|
||||
// Real items will have an ID > 1
|
||||
return this.field.id >= 1;
|
||||
},
|
||||
saveField: function() {
|
||||
let body = this.field;
|
||||
if (this.persistedField()) {
|
||||
CTFd.fetch(`/api/v1/fields/${this.field.id}`, {
|
||||
method: "PATCH",
|
||||
credentials: "same-origin",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
.then(response => {
|
||||
return response.json();
|
||||
})
|
||||
.then(response => {
|
||||
if (response.success === true) {
|
||||
this.field = response.data;
|
||||
ezToast({
|
||||
title: "Success",
|
||||
body: "Field has been updated!",
|
||||
delay: 1000
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
CTFd.fetch(`/api/v1/fields`, {
|
||||
method: "POST",
|
||||
credentials: "same-origin",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
.then(response => {
|
||||
return response.json();
|
||||
})
|
||||
.then(response => {
|
||||
if (response.success === true) {
|
||||
this.field = response.data;
|
||||
ezToast({
|
||||
title: "Success",
|
||||
body: "Field has been created!",
|
||||
delay: 1000
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
deleteField: function() {
|
||||
// Delete field in API
|
||||
this.$emit('delete-field', this.index);
|
||||
},
|
||||
if (confirm("Are you sure you'd like to delete this field?")) {
|
||||
if (this.persistedField()) {
|
||||
CTFd.fetch(`/api/v1/fields/${this.field.id}`, {
|
||||
method: "DELETE",
|
||||
credentials: "same-origin",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
return response.json();
|
||||
})
|
||||
.then(response => {
|
||||
if (response.success === true) {
|
||||
this.$emit("remove-field", this.index);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.$emit("remove-field", this.index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
||||
@@ -6,21 +6,26 @@
|
||||
<Field
|
||||
:index="index"
|
||||
:initialField.sync="fields[index]"
|
||||
@delete-field="deleteField"
|
||||
@remove-field="removeField"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col text-center">
|
||||
<button
|
||||
class="btn btn-sm btn-success btn-outlined float-right"
|
||||
class="btn btn-sm btn-success btn-outlined m-auto"
|
||||
type="button"
|
||||
@click="addField()"
|
||||
>
|
||||
Add New Field
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CTFd from "core/CTFd";
|
||||
import Field from "./Field.vue";
|
||||
|
||||
export default {
|
||||
@@ -35,35 +40,41 @@ export default {
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
loadFields: function() {
|
||||
CTFd.fetch("/api/v1/fields?type=user", {
|
||||
method: "GET",
|
||||
credentials: "same-origin",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
return response.json();
|
||||
})
|
||||
.then(response => {
|
||||
this.fields = response.data;
|
||||
});
|
||||
},
|
||||
addField: function() {
|
||||
this.fields.push({
|
||||
id: `#${Math.random().toString(16).slice(2)}`,
|
||||
id: Math.random(),
|
||||
type: "user",
|
||||
field_type: "text",
|
||||
name: "",
|
||||
description: "",
|
||||
editable: false,
|
||||
required: false,
|
||||
public: false
|
||||
})
|
||||
console.log(this.$data.fields)
|
||||
});
|
||||
},
|
||||
deleteField: function(index) {
|
||||
// if (fieldId) {
|
||||
// Wait for API implementation
|
||||
// }
|
||||
// Remove field at index
|
||||
removeField: function(index) {
|
||||
this.fields.splice(index, 1);
|
||||
console.log(this.fields)
|
||||
console.log(this.fields);
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.fields.push({
|
||||
id: 1,
|
||||
name: "Name",
|
||||
description: "Desc",
|
||||
editable: true,
|
||||
required: false,
|
||||
public: true
|
||||
});
|
||||
this.loadFields();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user