Start to finish up interface for fields

This commit is contained in:
Kevin Chung
2020-08-18 01:43:18 -04:00
parent 7b56fa4d5d
commit 3dbc4d51c1
5 changed files with 262 additions and 50 deletions

View File

@@ -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
View 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}

View File

@@ -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>

View File

@@ -6,21 +6,26 @@
<Field
:index="index"
:initialField.sync="fields[index]"
@delete-field="deleteField"
@remove-field="removeField"
/>
</div>
<button
class="btn btn-sm btn-success btn-outlined float-right"
type="button"
@click="addField()"
>
Add New Field
</button>
<div class="row">
<div class="col text-center">
<button
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