mirror of
https://gitlab.science.ru.nl/technicie/MarietjeDjango.git
synced 2025-12-10 13:32:22 +01:00
Marietje 4.1: Addition of Django REST framework, Swagger, Dark mode and updates to Django and Bootstrap
This commit is contained in:
@ -1,28 +1,30 @@
|
||||
{% extends 'base.html' %}
|
||||
{% extends 'marietje/base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Manage{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="/songs/edit/{{ song.id }}" method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
<div class="row centered-form">
|
||||
<div class="col-xs-12 col-sm-8 col-md-4 col-sm-offset-2 col-md-offset-4">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">Edit Song</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="form-group">
|
||||
<input type="text" id="artist" name="artist" class="form-control input-sm" placeholder="Artist" value="{{ song.artist }}">
|
||||
<div class="container">
|
||||
<div class="row mt-5">
|
||||
<div class="col-lg-4 mx-auto">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Edit Song</h3>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="text" id="title" name="title" class="form-control input-sm" placeholder="Title" value="{{ song.title }}">
|
||||
<div class="card-body">
|
||||
<form method="POST" action="/songs/edit/{{ song.id }}/" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
<div class="form-group mb-3">
|
||||
<input type="text" id="artist" name="artist" class="form-control input-sm" placeholder="Artist" value="{{ song.artist }}">
|
||||
</div>
|
||||
<div class="form-group mb-3">
|
||||
<input type="text" id="title" name="title" class="form-control input-sm" placeholder="Title" value="{{ song.title }}">
|
||||
</div>
|
||||
<input type="submit" value="Save" class="btn btn-primary btn-block w-100">
|
||||
</form>
|
||||
</div>
|
||||
<input type="submit" value="Save" class="btn btn-primary btn-block">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
@ -1,47 +1,173 @@
|
||||
{% extends 'base.html' %}
|
||||
{% extends 'marietje/base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Manage{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="table-responsive">
|
||||
<table id="request-table" class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Artist</th>
|
||||
<th>Title</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><input id="search-artist" class="search-input" type="text"></th>
|
||||
<th><input id="search-title" class="search-input" type="text"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<th colspan="3" class="ts-pager form-horizontal">
|
||||
<button type="button" class="btn first"><i class="icon-step-backward glyphicon glyphicon-step-backward"></i></button>
|
||||
<button type="button" class="btn prev"><i class="icon-arrow-left glyphicon glyphicon-backward"></i></button>
|
||||
<button type="button" class="btn next"><i class="icon-arrow-right glyphicon glyphicon-forward"></i></button>
|
||||
<button type="button" class="btn last"><i class="icon-step-forward glyphicon glyphicon-step-forward"></i></button>
|
||||
<select class="pagesize input-mini" title="Select page size">
|
||||
<option selected value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
<option value="500">500</option>
|
||||
<option value="1000">1000</option>
|
||||
</select>
|
||||
<select class="pagenum input-mini" title="Select page number"></select>
|
||||
</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<script type="text/javascript" src="{% static 'js/js.cookie-2.1.3.min.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'js/manage.js' %}"></script>
|
||||
<script type="text/javascript">
|
||||
var csrf_token = "{{ csrf_token }}";
|
||||
</script>
|
||||
<div class="container">
|
||||
<div class="table-responsive mt-5">
|
||||
<table id="request-table" class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Artist</th>
|
||||
<th>Title</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th colspan="2"><input id="search-all" class="search-input" type="text" v-model="search_input"/></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<th colspan="2" class="ts-pager form-horizontal">
|
||||
<button v-if="page_number === 1" type="button" class="btn first disabled"><i class="fa-solid fa-backward-fast"></i></button>
|
||||
<button v-else v-on:click="update_page(1);" type="button" class="btn first"><i class="fa-solid fa-backward-fast"></i></button>
|
||||
|
||||
<button v-if="page_number === 1" type="button" class="btn prev disabled"><i class="fa-solid fa-backward"></i></button>
|
||||
<button v-else v-on:click="update_page(page_number - 1);" type="button" class="btn prev"><i class="fa-solid fa-backward"></i></button>
|
||||
|
||||
<button v-if="page_number === number_of_pages" type="button" class="btn next disabled"><i class="fa-solid fa-forward"></i></button>
|
||||
<button v-else v-on:click="update_page(page_number + 1);" type="button" class="btn next"><i class="fa-solid fa-forward"></i></button>
|
||||
|
||||
<button v-if="page_number === number_of_pages" type="button" class="btn last disabled"><i class="fa-solid fa-forward-fast"></i></button>
|
||||
<button v-else v-on:click="update_page(number_of_pages);" type="button" class="btn last"><i class="fa-solid fa-forward-fast"></i></button>
|
||||
|
||||
<select class="pagesize input-mini" title="Select page size" v-model="page_size">
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
<option value="500">500</option>
|
||||
<option value="1000">1000</option>
|
||||
</select>
|
||||
<select class="pagenum input-mini" title="Select page number" v-model="page_number">
|
||||
<template v-for="(i, index) in number_of_pages">
|
||||
<option :value="i"><% i %></option>
|
||||
</template>
|
||||
</select>
|
||||
</th>
|
||||
<th colspan="2"></th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
<tbody>
|
||||
<template v-for="(song, index) in songs">
|
||||
<tr>
|
||||
<td>
|
||||
<% song.artist %>
|
||||
</td>
|
||||
<td>
|
||||
<a :href="'/songs/edit/' + song.id + '/'" v-on:click="request_song(song.id);"><% song.title %></a>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
let manage_vue = new Vue({
|
||||
el: '#request-table',
|
||||
delimiters: ['<%', '%>'],
|
||||
data: {
|
||||
songs: [],
|
||||
total_songs: 0,
|
||||
search_input: "",
|
||||
typing_timer: null,
|
||||
page_size: 10,
|
||||
page_number: 1,
|
||||
},
|
||||
watch: {
|
||||
search_input: {
|
||||
handler(val, oldVal) {
|
||||
clearTimeout(this.typing_timer);
|
||||
if (this.search !== "") {
|
||||
this.typing_timer = setTimeout(this.search, 200);
|
||||
}
|
||||
}
|
||||
},
|
||||
page_number: {
|
||||
handler(val, oldVal) {
|
||||
if (this.page_number <= 0) {
|
||||
this.page_number = 1;
|
||||
}
|
||||
if (this.page_number > this.number_of_pages) {
|
||||
this.page_number = this.number_of_pages;
|
||||
}
|
||||
this.refresh();
|
||||
}
|
||||
},
|
||||
page_size: {
|
||||
handler(val, oldVal) {
|
||||
if (this.page_size <= 0) {
|
||||
this.page_size = 10;
|
||||
}
|
||||
this.page_number = 1;
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
number_of_pages: function() {
|
||||
return Math.ceil(this.total_songs / this.page_size);
|
||||
}
|
||||
},
|
||||
created() {
|
||||
fetch(
|
||||
`/api/v1/songs/?ordering=title&limit=${this.page_size}&offset=${this.page_size * (this.page_number - 1)}`
|
||||
).then(response => {
|
||||
if (response.status === 200) {
|
||||
return response.json();
|
||||
} else {
|
||||
throw response;
|
||||
}
|
||||
}).then(data => {
|
||||
this.songs = data.results;
|
||||
this.total_songs = data.count;
|
||||
}).catch((e) => {
|
||||
if (e instanceof Response) {
|
||||
e.json().then(data => {
|
||||
tata.error("", data.errorMessage);
|
||||
});
|
||||
} else {
|
||||
tata.error("", "An unknown error occurred, please try again.")
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
search() {
|
||||
this.page_number = 1;
|
||||
this.refresh();
|
||||
},
|
||||
refresh() {
|
||||
fetch(
|
||||
`/api/v1/songs/?ordering=title&limit=${this.page_size}&offset=${this.page_size * (this.page_number - 1)}&search=${this.search_input}`,
|
||||
{
|
||||
headers: {
|
||||
"X-CSRFToken": CSRF_TOKEN,
|
||||
}
|
||||
}
|
||||
).then(response => {
|
||||
if (response.status === 200) {
|
||||
return response.json();
|
||||
} else {
|
||||
throw response;
|
||||
}
|
||||
}).then(data => {
|
||||
this.songs = data.results;
|
||||
this.total_songs = data.count;
|
||||
}).catch((e) => {
|
||||
if (e instanceof Response) {
|
||||
e.json().then(data => {
|
||||
tata.error("", data.errorMessage);
|
||||
});
|
||||
} else {
|
||||
tata.error("", "An unknown error occurred, please try again.")
|
||||
}
|
||||
});
|
||||
},
|
||||
update_page(page_number) {
|
||||
this.page_number = page_number;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@ -1,58 +1,218 @@
|
||||
{% extends 'base.html' %}
|
||||
{% extends 'marietje/base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Upload{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-sm-10 col-md-6 col-sm-offset-1 col-md-offset-3">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">Upload</h3>
|
||||
</div>
|
||||
<div class="panel-body ">
|
||||
<div class="forms-container">
|
||||
<div class="panel panel-default uploadform">
|
||||
<div class="panel-body">
|
||||
<form action="/api/upload" method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
<div class="fileupload fileupload-new" data-provides="fileupload">
|
||||
<span class="btn btn-primary btn-file"><span class="fileupload-new">Select files</span>
|
||||
<span class="fileupload-exists">Change</span>
|
||||
<input class="filefield" type="file" name="file[]" accept="audio/*" multiple />
|
||||
</span>
|
||||
<br>
|
||||
</div>
|
||||
<div class="song-container panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">Song</h3>
|
||||
</div>
|
||||
<div class="panel-body ">
|
||||
<div class="form-group">
|
||||
<input type="text" name="artist[]" class="form-control input-sm artist" placeholder="Artist">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="text" name="title[]" class="form-control input-sm title" placeholder="Title">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="container">
|
||||
<div class="row mt-5">
|
||||
<div class="col-lg-6 mx-auto">
|
||||
<div class="card uploadform" id="uploadform">
|
||||
<form action="{% url "songs:upload" %}" method="POST" enctype="multipart/form-data">
|
||||
<div class="card-header">
|
||||
<h3>Upload</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% csrf_token %}
|
||||
<div class="fileupload fileupload-new" data-provides="fileupload">
|
||||
<span class="btn btn-primary btn-file">
|
||||
<span v-if="fileObjects.length === 0">
|
||||
Select files
|
||||
</span>
|
||||
<span v-else>
|
||||
Change
|
||||
</span>
|
||||
<input class="filefield" id="filefield" type="file" name="file[]" accept="audio/*"
|
||||
multiple @change="set_new_files"/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="songs">
|
||||
<div v-for="fileObject in fileObjects" class="song-container card mb-3">
|
||||
<div class="card-header">
|
||||
<h3><% fileObject.name %></h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group mb-3">
|
||||
<div v-if="fileObject.artist === '' || fileObject.artist === null" class="alert alert-danger">Please enter an artist for this song.</div>
|
||||
<input v-if="upload_in_progress || uploaded" type="text" name="artist[]" class="form-control input-sm artist" disabled
|
||||
placeholder="Artist" v-model="fileObject.artist"/>
|
||||
<input v-else type="text" name="artist[]" class="form-control input-sm artist"
|
||||
placeholder="Artist" v-model="fileObject.artist"/>
|
||||
</div>
|
||||
<div class="form-group mb-3">
|
||||
<div v-if="fileObject.title === '' || fileObject.title === null" class="alert alert-danger">Please enter a title for this song.</div>
|
||||
<input v-if="upload_in_progress || uploaded" type="text" name="title[]" class="form-control input-sm title" disabled
|
||||
placeholder="Title" v-model="fileObject.title"/>
|
||||
<input v-else type="text" name="title[]" class="form-control input-sm title"
|
||||
placeholder="Title" v-model="fileObject.title"/>
|
||||
</div>
|
||||
<template v-if="fileObject.upload_finished === true">
|
||||
<div v-if="fileObject.success === true" class="alert alert-success">Upload finished successfully.</div>
|
||||
<div v-else class="alert alert-danger"><% fileObject.error_message %></div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div class="progress mt-2 mb-3">
|
||||
<div :class="{ 'progress-bar-animated': (upload_in_progress), 'bg-success': (uploaded && everything_successfully_uploaded), 'bg-danger': (uploaded && !everything_successfully_uploaded) }" class="progress-bar progress-bar-striped" role="progressbar" :style="{ width: (progress_bar_width + '%') }" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
</div>
|
||||
<template v-if="upload_in_progress || uploaded">
|
||||
<button v-if="uploaded" class="btn btn-primary btn-block w-100" v-on:click="clear">Clear</button>
|
||||
<button v-else class="btn btn-primary btn-block w-100 disabled">Clear</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<input v-if="ready_for_upload" id="upload" class="btn btn-primary btn-block w-100" type="submit" value="Upload" v-on:click="upload"/>
|
||||
<button v-else class="btn btn-primary btn-block w-100 disabled">Upload</button>
|
||||
</template>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<span class="result-message"></span>
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-info progress-bar-striped" role="progressbar" style="width: 0%;">
|
||||
</div>
|
||||
</div>
|
||||
<button id="upload" class="btn btn-primary btn-block">Upload</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<link rel="stylesheet" href="{% static 'songs/css/upload.css' %}"/>
|
||||
<script type="module">
|
||||
import * as id3 from '//unpkg.com/id3js@^2/lib/id3.js';
|
||||
|
||||
<link rel="stylesheet" href="{% static 'css/upload.css' %}" />
|
||||
<script type="text/javascript" src="{% static 'js/upload.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'js/id3.min.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'js/jquery.form.min.js' %}"></script>
|
||||
let upload_vue = new Vue({
|
||||
el: '#uploadform',
|
||||
delimiters: ['<%', '%>'],
|
||||
data: {
|
||||
files: [],
|
||||
fileObjects: [],
|
||||
uploaded: false,
|
||||
upload_in_progress: false,
|
||||
},
|
||||
computed: {
|
||||
ready_for_upload: function() {
|
||||
if (this.uploaded !== false || this.upload_in_progress !== false || this.fileObjects.length === 0) {
|
||||
return false;
|
||||
} else {
|
||||
for (let i = 0; i < this.fileObjects.length; i++) {
|
||||
if (this.fileObjects[i].artist === null || this.fileObjects[i].artist === '' || this.fileObjects[i].title === null || this.fileObjects[i].title === '') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
},
|
||||
everything_successfully_uploaded: function() {
|
||||
return this.fileObjects.map((fileObject) => {
|
||||
return fileObject.upload_finished === true && fileObject.success === true;
|
||||
}).reduce((previousValue, currentValue) => {
|
||||
return previousValue && currentValue;
|
||||
}, true);
|
||||
},
|
||||
progress_bar_width: function() {
|
||||
if (this.fileObjects.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const files_uploaded_successfully = this.fileObjects.map((fileObject) => {
|
||||
if (fileObject.upload_finished === true && fileObject.success === true) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}).reduce((previousValue, currentValue) => {
|
||||
return previousValue + currentValue;
|
||||
}, 0);
|
||||
|
||||
return Math.round((files_uploaded_successfully / this.fileObjects.length) * 100);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
clear(event) {
|
||||
event.preventDefault();
|
||||
this.files = [];
|
||||
this.fileObjects = [];
|
||||
this.uploaded = false;
|
||||
this.upload_in_progress = false;
|
||||
},
|
||||
upload(event) {
|
||||
this.upload_in_progress = true;
|
||||
event.preventDefault();
|
||||
let allPromises = [];
|
||||
for (let i = 0; i < this.fileObjects.length; i++) {
|
||||
const current_file = this.fileObjects[i].file;
|
||||
const current_artist = this.fileObjects[i].artist;
|
||||
const current_title = this.fileObjects[i].title;
|
||||
let data = new FormData();
|
||||
data.append('file', current_file);
|
||||
data.append('artist', current_artist);
|
||||
data.append('title', current_title);
|
||||
allPromises.push(fetch('/api/v1/songs/upload/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"X-CSRFToken": CSRF_TOKEN,
|
||||
},
|
||||
body: data,
|
||||
}).then(result => {
|
||||
if (result.status === 200) {
|
||||
return result.json();
|
||||
} else {
|
||||
throw result;
|
||||
}
|
||||
}).then(() => {
|
||||
this.fileObjects[i].success = true;
|
||||
}).catch(e => {
|
||||
if (e instanceof Response) {
|
||||
e.json().then(data => {
|
||||
this.fileObjects.error_message = data.errorMessage;
|
||||
this.fileObjects.success = false;
|
||||
});
|
||||
} else {
|
||||
this.fileObjects.error_message = "An exception occurred while uploading this file, please try again.";
|
||||
this.fileObjects.success = false;
|
||||
}
|
||||
}).finally(() => {
|
||||
this.fileObjects[i].upload_finished = true;
|
||||
}));
|
||||
}
|
||||
Promise.all(allPromises).finally(() => {
|
||||
this.upload_in_progress = false;
|
||||
this.uploaded = true;
|
||||
});
|
||||
},
|
||||
async set_new_files(event) {
|
||||
this.files = event.target.files;
|
||||
let newFileObjects = [];
|
||||
for (let i = 0; i < this.files.length; i++) {
|
||||
try {
|
||||
const tags = await this.parseSong(this.files[i]);
|
||||
newFileObjects.push(
|
||||
{
|
||||
"file": this.files[i],
|
||||
"name": this.files[i].name,
|
||||
"artist": tags.artist,
|
||||
"title": tags.title,
|
||||
"success": null,
|
||||
"error_message": null,
|
||||
"upload_finished": false,
|
||||
}
|
||||
);
|
||||
} catch {
|
||||
newFileObjects.push(
|
||||
{
|
||||
"file": this.files[i],
|
||||
"name": this.files[i].name,
|
||||
"artist": "",
|
||||
"title": "",
|
||||
"success": null,
|
||||
"error_message": null,
|
||||
"upload_finished": false,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
this.fileObjects = newFileObjects;
|
||||
},
|
||||
parseSong(file) {
|
||||
return id3.fromFile(file);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user