chg: [website] history add input attr

pull/655/head
David Cruciani 2024-02-12 15:33:47 +01:00
parent 443f1f5e9c
commit ad6397b294
No known key found for this signature in database
GPG Key ID: 8690CDE1E3994B9B
11 changed files with 118 additions and 663 deletions

View File

@ -7,6 +7,7 @@ class Module(db.Model):
description = db.Column(db.String)
is_active = db.Column(db.Boolean, default=True)
request_on_query = db.Column(db.Boolean, default=False)
input_attr = db.Column(db.String)
def to_json(self):
json_dict = {
@ -14,7 +15,8 @@ class Module(db.Model):
"name": self.name,
"description": self.description,
"is_active": self.is_active,
"request_on_query": self.request_on_query
"request_on_query": self.request_on_query,
"input_attr": self.input_attr
}
return json_dict

View File

@ -79,6 +79,8 @@ def get_modules_config():
modules_list = []
for module in modules:
loc_module = module.to_json()
if loc_module["input_attr"]:
loc_module["input_attr"] = json.loads(loc_module["input_attr"])
loc_module["config"] = []
mcs = Module_Config.query.filter_by(module_id=module.id).all()
for mc in mcs:

View File

@ -1,634 +0,0 @@
import {display_toast} from '../toaster.js'
const { ref, nextTick, computed } = Vue
export default {
delimiters: ['[[', ']]'],
props: {
cases_info: Object,
status_info: Object,
users_in_case: Object,
edit_mode: Boolean,
task: Object,
key_loop: Number
},
emits: ['edit_mode', 'task'],
setup(props, {emit}) {
Vue.onMounted(async () => {
select2_change(props.task.id)
const targetElement = document.getElementById('editor_' + props.task.id)
editor = new Editor.EditorView({
doc: "\n\n",
extensions: [Editor.basicSetup, Editor.markdown(),Editor.EditorView.updateListener.of((v) => {
if (v.docChanged) {
note_editor_render.value = editor.state.doc.toString()
}
})],
parent: targetElement
})
const allCollapses = document.getElementById('collapse' + props.task.id)
allCollapses.addEventListener('shown.bs.collapse', event => {
md.mermaid.init()
})
is_mounted.value = true
})
Vue.onUpdated(async () => {
select2_change(props.task.id)
// do not initialize mermaid before the page is mounted
if(is_mounted)
md.mermaid.init()
})
const is_mounted = ref(false)
const is_exporting = ref(false)
const notes = ref(props.task.notes)
const note_editor_render = ref("")
let editor
const md = window.markdownit()
md.use(mermaidMarkdown.default)
if(props.task.notes)
note_editor_render.value = props.task.notes
async function change_status(status, task){
const res = await fetch(
'/case/' + task.case_id + '/change_task_status/'+task.id,{
headers: { "X-CSRFToken": $("#csrf_token").val(), "Content-Type": "application/json" },
method: "POST",
body: JSON.stringify({"status": status})
}
)
if(await res.status==200){
task.last_modif = Date.now()
task.status_id=status
if(props.status_info.status[status-1].name == 'Finished'){
task.last_modif = Date.now()
task.completed = true
fetch('/case/complete_task/'+task.id)
}else{
task.completed = false
}
}
await display_toast(res)
}
async function take_task(task, current_user){
const res = await fetch('/case/' + task.case_id + '/take_task/' + task.id)
if( await res.status == 200){
task.last_modif = Date.now()
task.is_current_user_assigned = true
task.users.push(current_user)
}
await display_toast(res)
}
async function remove_assign_task(task, current_user){
const res = await fetch('/case/' + task.case_id + '/remove_assignment/' + task.id)
if( await res.status == 200){
task.last_modif = Date.now()
task.is_current_user_assigned = false
let index = -1
for(let i=0;i<task.users.length;i++){
if (task.users[i].id==current_user.id)
index = i
}
if(index > -1)
task.users.splice(index, 1)
}
await display_toast(res)
}
async function assign_user_task(){
let users_select = $('#selectUser'+props.task.id).val()
if(users_select.length){
const res_msg = await fetch(
'/case/' + props.task.case_id + '/assign_users/' + props.task.id,{
headers: { "X-CSRFToken": $("#csrf_token").val(), "Content-Type": "application/json" },
method: "POST",
body: JSON.stringify({"users_id": users_select})
}
)
if( await res_msg.status == 200){
if(users_select.includes(props.cases_info.current_user.id.toString())){
props.task.is_current_user_assigned = true
}
const res = await fetch('/case/' + props.task.case_id + '/get_assigned_users/' +props.task.id)
if(await res.status == 404){
display_toast(res)
}else{
let loc = await res.json()
props.task.users = loc
props.task.last_modif = Date.now()
emit('task', props.task)
}
}
await display_toast(res_msg)
}
}
async function remove_assigned_user(user_id){
const res = await fetch(
'/case/' + props.task.case_id + '/remove_assigned_user/' + props.task.id,{
headers: { "X-CSRFToken": $("#csrf_token").val(), "Content-Type": "application/json" },
method: "POST",
body: JSON.stringify({"user_id": user_id})
}
)
if( await res.status == 200){
props.task.last_modif = Date.now()
let index = -1
for(let i=0;i<props.task.users.length;i++){
if (props.task.users[i].id==user_id){
if(user_id == props.cases_info.current_user.id.toString()){
props.task.is_current_user_assigned = true
}
props.task.is_current_user_assigned = false
index = i
}
}
if(index > -1)
props.task.users.splice(index, 1)
}
await display_toast(res)
}
async function delete_task(task, task_array){
const res = await fetch('/case/' + task.case_id + '/delete_task/' + task.id)
if( await res.status == 200){
let index = task_array.indexOf(task)
if(index > -1)
task_array.splice(index, 1)
}
await display_toast(res)
}
async function edit_note(task){
task.last_modif = Date.now()
emit('edit_mode', true)
const res = await fetch('/case/' + task.case_id + '/get_note/' + task.id)
let loc = await res.json()
task.notes = loc["note"]
const targetElement = document.getElementById('editor1_' + props.task.id)
editor = new Editor.EditorView({
doc: task.notes,
extensions: [Editor.basicSetup, Editor.markdown(),Editor.EditorView.updateListener.of((v) => {
if (v.docChanged) {
note_editor_render.value = editor.state.doc.toString()
}
})],
parent: targetElement
})
}
async function modif_note(task){
let notes_loc = editor.state.doc.toString()
if(notes_loc.trim().length == 0){
notes_loc = notes_loc.trim()
}
const res_msg = await fetch(
'/case/' + task.case_id + '/modif_note/' + task.id,{
headers: { "X-CSRFToken": $("#csrf_token").val(), "Content-Type": "application/json" },
method: "POST",
body: JSON.stringify({"task_id": task.id.toString(), "notes": notes_loc})
}
)
if(await res_msg.status == 200){
emit('edit_mode', false)
task.last_modif = Date.now()
task.notes = notes_loc
notes.value = notes_loc
await nextTick()
if(!notes_loc){
const targetElement = document.getElementById('editor_' + props.task.id)
if(targetElement.innerHTML === ""){
editor = new Editor.EditorView({
doc: "\n\n",
extensions: [Editor.basicSetup, Editor.markdown(),Editor.EditorView.updateListener.of((v) => {
if (v.docChanged) {
note_editor_render.value = editor.state.doc.toString()
}
})],
parent: targetElement
})
}
}
}
await display_toast(res_msg)
}
async function add_file(task){
let files = document.getElementById('formFileMultiple'+task.id).files
let formData = new FormData();
for(let i=0;i<files.length;i++){
formData.append("files"+i, files[i]);
}
const res = await fetch(
'/case/' + task.case_id + '/add_files/' + task.id,{
headers: { "X-CSRFToken": $("#csrf_token").val() },
method: "POST",
files: files,
body: formData
}
)
if(await res.status == 200){
const res_files = await fetch('/case/' + task.case_id + '/get_files/'+task.id)
if(await res_files.status == 200){
task.last_modif = Date.now()
let loc = await res_files.json()
task.files = []
for(let file in loc['files']){
task.files.push(loc['files'][file])
}
}else{
await display_toast(res_files)
}
}
await display_toast(res)
}
async function delete_file(file, task){
const res = await fetch('/case/task/' + task.id + '/delete_file/' + file.id)
if(await res.status == 200){
task.last_modif = Date.now()
let index = task.files.indexOf(file)
if(index > -1)
task.files.splice(index, 1)
}
await display_toast(res)
}
async function complete_task(task){
const res = await fetch('/case/complete_task/'+task.id)
if (await res.status == 200){
task.last_modif = Date.now()
task.completed = !task.completed
let status = task.status_id
if(props.status_info.status[task.status_id -1].name == 'Finished'){
for(let i in props.status_info.status){
if(props.status_info.status[i].name == 'Created')
task.status_id = props.status_info.status[i].id
}
if(task.status_id == status)
task.status_id = 1
}else{
for(let i in props.status_info.status){
if(props.status_info.status[i].name == 'Finished'){
task.status_id = props.status_info.status[i].id
break
}
}
}
let index = props.cases_info.tasks.indexOf(task)
if(index > -1)
props.cases_info.tasks.splice(index, 1)
}
await display_toast(res)
}
async function notify_user(user_id){
const res = await fetch(
'/case/' + props.task.case_id + '/task/' + props.task.id + '/notify_user',{
headers: { "X-CSRFToken": $("#csrf_token").val(), "Content-Type": "application/json" },
method: "POST",
body: JSON.stringify({"task_id": props.task.id, "user_id": user_id})
}
)
await display_toast(res)
}
function formatNow(dt) {
return moment.utc(dt).from(moment.utc())
}
function endOf(dt){
return moment.utc(dt).endOf().from(moment.utc())
}
function present_user_in_task(task_user_list, user){
let index = -1
for(let i=0;i<task_user_list.length;i++){
if (task_user_list[i].id==user.id)
index = i
}
return index
}
async function export_notes(task, type){
is_exporting.value = true
let filename = ""
await fetch('/case/'+task.case_id+'/task/'+task.id+'/export_notes?type=' + type)
.then(res =>{
filename = res.headers.get("content-disposition").split("=")
filename = filename[filename.length - 1]
return res.blob()
})
.then(data =>{
var a = document.createElement("a")
a.href = window.URL.createObjectURL(data);
a.download = filename;
a.click();
})
is_exporting.value = false
}
function select2_change(tid){
$('.select2-selectUser'+tid).select2({width: 'element'})
$('.select2-container').css("min-width", "200px")
}
return {
notes,
note_editor_render,
md,
is_exporting,
getTextColor,
mapIcon,
change_status,
take_task,
remove_assign_task,
assign_user_task,
remove_assigned_user,
delete_task,
edit_note,
modif_note,
add_file,
delete_file,
complete_task,
notify_user,
formatNow,
endOf,
export_notes,
present_user_in_task
}
},
template: `
<div style="display: flex;">
<a :href="'#collapse'+task.id" class="list-group-item list-group-item-action" data-bs-toggle="collapse" role="button" aria-expanded="false" :aria-controls="'collapse'+task.id">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">[[ key_loop ]]- [[task.title]]</h5>
<small><i>Changed [[ formatNow(task.last_modif) ]] </i></small>
</div>
<div class="d-flex w-100 justify-content-between">
<p v-if="task.description" class="card-text">[[ task.description ]]</p>
<p v-else class="card-text"><i style="font-size: 12px;">No description</i></p>
<small v-if="status_info">
<span :class="'badge rounded-pill text-bg-'+status_info.status[task.status_id -1].bootstrap_style">
[[ status_info.status[task.status_id -1].name ]]
</span>
</small>
</div>
<div class="d-flex w-100 justify-content-between">
<div style="display: flex;" v-if="task.tags">
<template v-for="tag in task.tags">
<div class="tag" :title="tag.description" :style="{'background-color': tag.color, 'color': getTextColor(tag.color)}">
<i class="fa-solid fa-tag" style="margin-right: 3px; margin-left: 3px;"></i>
[[tag.name]]
</div>
</template>
</div>
<div v-else></div>
</div>
<div class="d-flex w-100 justify-content-between">
<div v-if="task.users.length">
Users:
<template v-for="user in task.users">
[[user.first_name]] [[user.last_name]],
</template>
</div>
<div v-else>
<i>No user assigned</i>
</div>
<small v-if="task.deadline" :title="task.deadline"><i>Deadline [[endOf(task.deadline)]]</i></small>
<small v-else><i>No deadline</i></small>
</div>
<div class="d-flex w-100 justify-content-between">
<div style="display: flex;" v-if="task.clusters">
<template v-for="cluster in task.clusters">
<div :title="'Description:\\n' + cluster.description + '\\n\\nMetadata:\\n' + JSON.stringify(JSON.parse(cluster.meta), null, 4)">
<span v-html="mapIcon(cluster.icon)"></span>
[[cluster.tag]]
</div>
</template>
</div>
<div v-else></div>
</div>
</a>
<div v-if="!cases_info.permission.read_only && cases_info.present_in_case || cases_info.permission.admin" style="display: grid;">
<button v-if="task.completed" class="btn btn-secondary btn-sm" @click="complete_task(task)" title="Revive the task">
<i class="fa-solid fa-backward"></i>
</button>
<button v-else class="btn btn-success btn-sm" @click="complete_task(task)" title="Complete the task">
<i class="fa-solid fa-check"></i>
</button>
<button v-if="!task.is_current_user_assigned" class="btn btn-secondary btn-sm" @click="take_task(task, cases_info.current_user)" title="Be assigned to the task">
<i class="fa-solid fa-hand"></i>
</button>
<button v-else class="btn btn-secondary btn-sm" @click="remove_assign_task(task, cases_info.current_user)" title="Remove the assignment">
<i class="fa-solid fa-handshake-slash"></i>
</button>
<a class="btn btn-primary btn-sm" :href="'/case/'+cases_info.case.id+'/edit_task/'+task.id" type="button" title="Edit the task">
<i class="fa-solid fa-pen-to-square"></i>
</a>
<button class="btn btn-danger btn-sm" @click="delete_task(task, cases_info.tasks)" title="Delete the task">
<i class="fa-solid fa-trash"></i>
</button>
</div>
</div>
<!-- Collapse Part -->
<div class="collapse collapsetest" :id="'collapse'+task.id">
<div class="card card-body" style="background-color: whitesmoke;">
<div class="d-flex w-100 justify-content-between">
<div v-if="!cases_info.permission.read_only && cases_info.present_in_case || cases_info.permission.admin">
<div v-if="users_in_case">
<h5>Assign</h5>
<select data-placeholder="Users" multiple :class="'select2-selectUser'+task.id" :name="'selectUser'+task.id" :id="'selectUser'+task.id" style="min-width:200px">
<template v-for="user in users_in_case.users_list">
<option :value="user.id" v-if="present_user_in_task(task.users, user) == -1">[[user.first_name]] [[user.last_name]]</option>
</template>
</select>
<button class="btn btn-primary" @click="assign_user_task()">Assign</button>
</div>
<div v-if="task.users.length">
<h5>Remove assign</h5>
<div v-for="user in task.users">
<span style="margin-right: 5px">[[user.first_name]] [[user.last_name]]</span>
<button v-if="cases_info.current_user.id != user.id" class="btn btn-primary btn-sm" @click="notify_user(user.id)"><i class="fa-solid fa-bell"></i></button>
<button class="btn btn-danger btn-sm" @click="remove_assigned_user(user.id)"><i class="fa-solid fa-trash"></i></button>
</div>
</div>
</div>
<div v-if="!cases_info.permission.read_only && cases_info.present_in_case || cases_info.permission.admin">
<div>
<h5>Change Status</h5>
</div>
<div>
<div class="dropdown" :id="'dropdown_status_'+task.id">
<template v-if="status_info">
<button class="btn btn-secondary dropdown-toggle" :id="'button_'+task.id" type="button" data-bs-toggle="dropdown" aria-expanded="false">
[[ status_info.status[task.status_id -1].name ]]
</button>
<ul class="dropdown-menu" :id="'dropdown_ul_status_'+task.id">
<template v-for="status_list in status_info.status">
<li v-if="status_list.id != task.status_id">
<button class="dropdown-item" @click="change_status(status_list.id, task)">[[ status_list.name ]]</button>
</li>
</template>
</ul>
</template>
</div>
</div>
</div>
</div>
<hr>
<div class="d-flex w-100 justify-content-between">
<div v-if="task.url">
<div>
<h5>Tool/Url</h5>
</div>
<div>
[[task.url]]
</div>
</div>
<div v-if="task.instances.length">
<div>
<h5>Connectors</h5>
</div>
<div v-for="instance in task.instances" :title="instance.description">
<img :src="'/static/icons/'+instance.icon" style="max-width: 30px;">
<a style="margin-left: 5px" :href="instance.url">[[instance.url]]</a>
</div>
</div>
</div>
<hr>
<div class="d-flex w-100 justify-content-between">
<div>
<div>
<h5>Files</h5>
</div>
<div>
<input class="form-control" type="file" :id="'formFileMultiple'+task.id" multiple/>
<button class="btn btn-primary" @click="add_file(task)">Add</button>
</div>
<br/>
<template v-if="task.files.length">
<template v-for="file in task.files">
<div>
<a class="btn btn-link" :href="'/case/task/'+task.id+'/download_file/'+file.id">
[[ file.name ]]
</a>
<button class="btn btn-danger" @click="delete_file(file, task)"><i class="fa-solid fa-trash"></i></button>
</div>
</template>
</template>
</div>
</div>
<hr>
<div class="d-flex w-100 justify-content-between">
<div class="w-100">
<div>
<h5>Notes</h5>
</div>
<div v-if="task.notes">
<template v-if="edit_mode">
<div>
<button class="btn btn-primary" @click="modif_note(task)" type="button" :id="'note_'+task.id">
<div hidden>[[task.title]]</div>
Save
</button>
</div>
<div style="display: flex;">
<div style="background-color: white; border-width: 1px; border-style: solid; width: 50%" :id="'editor1_'+task.id"></div>
<div style="background-color: white; border: 1px #515151 solid; padding: 5px; width: 50%" v-html="md.render(note_editor_render)"></div>
</div>
</template>
<template v-else>
<template v-if="!cases_info.permission.read_only && cases_info.present_in_case || cases_info.permission.admin">
<button class="btn btn-primary" @click="edit_note(task)" type="button" :id="'note_'+task.id">
<div hidden>[[task.title]]</div>
Edit
</button>
<div class="btn-group">
<button class="btn btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
Export
</button>
<ul class="dropdown-menu">
<li>
<button v-if="!is_exporting" class="btn btn-link" @click="export_notes(task, 'pdf')" title="Export markdown as pdf">PDF</button>
<button v-else class="btn btn-link" disabled>
<span class="spinner-border spinner-border-sm" aria-hidden="true"></span>
<span role="status">Loading...</span>
</button>
</li>
<li>
<button v-if="!is_exporting" class="btn btn-link" @click="export_notes(task, 'docx')" title="Export markdown as docx">DOCX</button>
<button v-else class="btn btn-link" disabled>
<span class="spinner-border spinner-border-sm" aria-hidden="true"></span>
<span role="status">Loading...</span>
</button>
</li>
</ul>
</div>
</template>
<p style="background-color: white; border: 1px #515151 solid; padding: 5px;" v-html="md.render(notes)"></p>
</template>
</div>
<div v-else>
<template v-if="!cases_info.permission.read_only && cases_info.present_in_case || cases_info.permission.admin">
<div>
<button class="btn btn-primary" @click="modif_note(task)" type="button" :id="'note_'+task.id">
<div hidden>[[task.title]]</div>
Create
</button>
</div>
<div style="display: flex;">
<div style="background-color: white; border-width: 1px; border-style: solid; width: 50%" :id="'editor_'+task.id"></div>
<div style="background-color: white; border: 1px #515151 solid; padding: 5px; width: 50%" v-html="md.render(note_editor_render)"></div>
</div>
</template>
</div>
</div>
</div>
</div>
</div>
`
}

View File

@ -0,0 +1,42 @@
const { ref, nextTick } = Vue
export default {
name: "History_view",
delimiters: ['[[', ']]'],
props: {
history: Object,
key_loop: Number
},
setup(props) {
return {
}
},
template: `
<div class="list-group" style="margin-bottom: 20px;">
<a :href="'/query/'+history.uuid" class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">[[key_loop+1]]- [[history.query]]</h5>
<small><i>[[history.uuid]]</i></small>
</div>
<p class="mb-1" style="color: green;"><u>Input Attribute</u>:</p>
<div>[[history.input]]</div>
<br>
<p class="mb-1" style="color: #2000ff;"><u>Modules</u>:</p>
<div>
<template v-for="module in history.modules">[[module]],</template>
</div>
<div class="d-flex w-100 justify-content-between">
<div></div>
<small><i>[[history.query_date]]</i></small>
</div>
</a>
</div>
`
}

View File

@ -12,27 +12,7 @@
<div v-if="history">
<template v-for="h, key in history">
<div class="list-group" style="margin-bottom: 20px;">
<a :href="'/query/'+h.uuid" class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">[[key+1]]- [[h.query]]</h5>
<small><i>[[h.uuid]]</i></small>
</div>
<p class="mb-1" style="color: green;"><u>Input Attribute</u>:</p>
<div>[[h.input]]</div>
<br>
<p class="mb-1" style="color: #2000ff;"><u>Modules</u>:</p>
<div>
<template v-for="module in h.modules">[[module]],</template>
</div>
<div class="d-flex w-100 justify-content-between">
<div></div>
<small><i>[[h.query_date]]</i></small>
</div>
</a>
</div>
<history_view :history="h" :key_loop="key" />
</template>
</div>
@ -43,8 +23,12 @@
<script type="module">
const { createApp, ref, onMounted, nextTick, defineComponent} = Vue
import {display_toast, message_list} from '/static/js/toaster.js'
import history_view from '/static/js/history_view.js'
createApp({
delimiters: ['[[', ']]'],
components: {
history_view
},
setup() {
const history = ref({})

View File

@ -39,19 +39,27 @@
</div>
<br>
<h4>[[ current_config['module_name'] ]]</h4>
<i>
<small>
Attributes:
<template v-for="attr in current_config['input_attr']">
[[ attr ]],
</template>
</small>
</i>
<template v-for="conf, key in current_config">
<div class="mb-3" v-if="key != 'module_name' && !current_config['request_on_query'] && key != 'request_on_query'">
<div class="mb-3" v-if="key != 'module_name' && !current_config['request_on_query'] && key != 'request_on_query' && key != 'input_attr'">
<label :for="'form-'+key" class="form-label">[[key]]</label>
<input type="text" class="form-control" :id="'form-'+key+'-'+current_config['module_name']" :value="conf">
</div>
</template>
<div class="mb-3">
<div class="mb-3" v-if="Object.keys(current_config).length > 3">
<label :for="'form-'+current_config['module_name']+'-checkbox'" class="form-label">Request for config on query</label>
<div>
<input type="checkbox" :checked="current_config['request_on_query']" @click="check_request_on_query()" :id="'form-'+current_config['module_name']+'-checkbox'">
</div>
</div>
<button class="btn btn-primary" @click="change_config()">Save</button>
<button v-if="Object.keys(current_config).length > 3" class="btn btn-primary" @click="change_config()">Save</button>
</div>
</div>
@ -89,9 +97,10 @@
}
}
}
current_config.value["module_name"] = module.name
current_config.value["request_on_query"] = module.request_on_query
}
current_config.value["module_name"] = module.name
current_config.value["input_attr"] = module.input_attr
}
function close_panel(){
@ -101,7 +110,7 @@
async function change_config(){
let result_dict = {}
for(let key in current_config.value){
if(key != "module_name" && key != "request_on_query"){
if(key != "module_name" && key != "request_on_query" && key != "input_attr"){
let loc = $("#form-"+key+"-"+current_config.value["module_name"]).val()
result_dict[key] = loc
current_config.value[key] = loc

View File

@ -10,11 +10,16 @@ def create_modules_db():
for module in modules:
m = Module.query.filter_by(name=module["name"]).first()
input_attr = ""
if "input" in module["mispattributes"]:
input_attr = json.dumps(module["mispattributes"]["input"])
if not m:
m = Module(
name=module["name"],
description=module["meta"]["description"],
is_active=True
is_active=True,
request_on_query=False,
input_attr=input_attr
)
db.session.add(m)
db.session.commit()

12
website/install.sh Executable file
View File

@ -0,0 +1,12 @@
#!/bin/bash
sudo apt-get update -y
sudo apt-get install virtualenv -y
virtualenv env
source env/bin/activate
pip install -r requirements.txt
python app.py -i
deactivate

View File

@ -1,5 +1,4 @@
#!/bin/bash
source env/bin/activate
function launch {

View File

@ -0,0 +1,32 @@
"""empty message
Revision ID: 183bf8fa2b87
Revises: 91f830996ff4
Create Date: 2024-02-09 15:21:17.274707
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '183bf8fa2b87'
down_revision = '91f830996ff4'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('module', schema=None) as batch_op:
batch_op.add_column(sa.Column('input_attr', sa.String(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('module', schema=None) as batch_op:
batch_op.drop_column('input_attr')
# ### end Alembic commands ###

View File

@ -1,5 +1,6 @@
flask
sqlalchemy
flask-session
flask-sqlalchemy
Flask-WTF
Flask-Migrate
@ -12,3 +13,4 @@ schedule
pytest
gunicorn
jsonschema
requests