Worked on folder modals.

This commit is contained in:
2018-10-15 00:45:08 +03:00
parent 1d5c7ea24b
commit c3e3bfa33c
18 changed files with 756 additions and 406 deletions

View File

@ -2,6 +2,7 @@ from YtManagerApp.models import SubscriptionFolder, Subscription
from typing import Callable, Union, Any, Optional
from django.contrib.auth.models import User
import logging
from django.db.models.functions import Lower
def traverse_tree(root_folder_id: Optional[int], user: User, visit_func: Callable[[Union[SubscriptionFolder, Subscription]], Any]):
@ -27,11 +28,11 @@ def traverse_tree(root_folder_id: Optional[int], user: User, visit_func: Callabl
continue
visited.append(folder_id)
for folder in SubscriptionFolder.objects.filter(parent_id=folder_id, user=user).order_by('name'):
for folder in SubscriptionFolder.objects.filter(parent_id=folder_id, user=user).order_by(Lower('name')):
collect(visit_func(folder))
queue.append(folder.id)
for subscription in Subscription.objects.filter(parent_folder_id=folder_id, user=user).order_by('name'):
for subscription in Subscription.objects.filter(parent_folder_id=folder_id, user=user).order_by(Lower('name')):
collect(visit_func(subscription))
return data_collected

View File

@ -0,0 +1,18 @@
# Generated by Django 2.1.2 on 2018-10-14 14:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('YtManagerApp', '0003_auto_20181013_2018'),
]
operations = [
migrations.AlterField(
model_name='subscriptionfolder',
name='name',
field=models.CharField(max_length=250),
),
]

View File

@ -1,5 +1,6 @@
from django.db import models
from django.contrib.auth.models import User
from django.db.models.functions import Lower
# help_text = user shown text
# verbose_name = user shown name
@ -69,12 +70,20 @@ class UserSettings(models.Model):
class SubscriptionFolder(models.Model):
name = models.TextField(null=False)
name = models.CharField(null=False, max_length=250)
parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True)
user = models.ForeignKey(User, on_delete=models.CASCADE, null=False, blank=False)
def __str__(self):
return self.name
s = ""
current = self
while current is not None:
s = current.name + " > " + s
current = current.parent
return s[:-3]
class Meta:
ordering = [Lower('parent__name'), Lower('name')]
class Channel(models.Model):

View File

@ -11,6 +11,13 @@
width: 64px;
height: 64px; }
.loading-dual-ring-center-screen {
position: fixed;
top: 50%;
left: 50%;
margin-top: -32px;
margin-left: -32px; }
.loading-dual-ring:after {
content: " ";
display: block;
@ -22,6 +29,26 @@
border-color: #007bff transparent #007bff transparent;
animation: loading-dual-ring 1.2s linear infinite; }
.black-overlay {
position: fixed;
/* Sit on top of the page content */
display: none;
/* Hidden by default */
width: 100%;
/* Full width (cover the whole page) */
height: 100%;
/* Full height (cover the whole page) */
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
/* Black background with opacity */
z-index: 2;
/* Specify a stack order in case you're using a different order for other elements */
cursor: pointer;
/* Add a pointer on hover */ }
@keyframes loading-dual-ring {
0% {
transform: rotate(0deg); }
@ -58,4 +85,10 @@
.no-asterisk .asteriskField {
display: none; }
.modal-field-error {
margin: 0.5rem 0;
padding: 0.5rem 0; }
.modal-field-error ul {
margin: 0; }
/*# sourceMappingURL=style.css.map */

View File

@ -1,6 +1,6 @@
{
"version": 3,
"mappings": "AAEA,gCAAgC;AAChC,wBAAyB;EACrB,OAAO,EAAE,OAAO;;AAGpB,wBAAyB;EACrB,OAAO,EAAE,OAAO;;AAGpB,uBAAuB;AACvB,kBAAmB;EACf,OAAO,EAAE,YAAY;EACrB,KAAK,EAAE,IAAI;EACX,MAAM,EAAE,IAAI;;AAGhB,wBAAyB;EACrB,OAAO,EAAE,GAAG;EACZ,OAAO,EAAE,KAAK;EACd,KAAK,EAAE,IAAI;EACX,MAAM,EAAE,IAAI;EACZ,MAAM,EAAE,GAAG;EACX,aAAa,EAAE,GAAG;EAClB,MAAM,EAAE,iBAAuB;EAC/B,YAAY,EAAE,uCAAmD;EACjE,SAAS,EAAE,sCAAsC;;AAGrD,4BAOC;EANG,EAAG;IACC,SAAS,EAAE,YAAY;EAE3B,IAAK;IACD,SAAS,EAAE,cAAc;AAK7B,4BAAc;EACV,OAAO,EAAE,MAAM;EACf,aAAa,EAAE,KAAK;AAGpB,+BAAW;EACP,OAAO,EAAE,MAAM;AAEnB,+BAAW;EACP,SAAS,EAAE,IAAI;EACf,aAAa,EAAE,KAAK;AAExB,gCAAY;EACR,SAAS,EAAE,IAAI;EACf,aAAa,EAAE,KAAK;EAEpB,uCAAO;IACH,SAAS,EAAE,GAAG;AAGtB,iCAAa;EACT,OAAO,EAAE,YAAY;AAGzB,+BAAW;EACP,YAAY,EAAE,QAAQ;EACtB,qCAAQ;IACJ,eAAe,EAAE,IAAI;AAKjC,8BAAgB;EACZ,KAAK,EAAE,OAAO;AAElB,6BAAe;EACX,KAAK,EAAE,OAAO;;AAItB,WAAY;EACR,SAAS,EAAE,KAAK;EAChB,MAAM,EAAE,MAAM;;AAId,2BAAe;EACX,OAAO,EAAE,IAAI",
"mappings": "AAEA,gCAAgC;AAChC,wBAAyB;EACrB,OAAO,EAAE,OAAO;;AAGpB,wBAAyB;EACrB,OAAO,EAAE,OAAO;;AAGpB,uBAAuB;AACvB,kBAAmB;EACf,OAAO,EAAE,YAAY;EACrB,KAAK,EAAE,IAAI;EACX,MAAM,EAAE,IAAI;;AAGhB,gCAAiC;EAC7B,QAAQ,EAAE,KAAK;EACf,GAAG,EAAE,GAAG;EACR,IAAI,EAAE,GAAG;EACT,UAAU,EAAE,KAAK;EACjB,WAAW,EAAE,KAAK;;AAGtB,wBAAyB;EACrB,OAAO,EAAE,GAAG;EACZ,OAAO,EAAE,KAAK;EACd,KAAK,EAAE,IAAI;EACX,MAAM,EAAE,IAAI;EACZ,MAAM,EAAE,GAAG;EACX,aAAa,EAAE,GAAG;EAClB,MAAM,EAAE,iBAAuB;EAC/B,YAAY,EAAE,uCAAmD;EACjE,SAAS,EAAE,sCAAsC;;AAGrD,cAAe;EACX,QAAQ,EAAE,KAAK;EAAE,oCAAoC;EACrD,OAAO,EAAE,IAAI;EAAE,uBAAuB;EACtC,KAAK,EAAE,IAAI;EAAE,uCAAuC;EACpD,MAAM,EAAE,IAAI;EAAE,wCAAwC;EACtD,GAAG,EAAE,CAAC;EACN,IAAI,EAAE,CAAC;EACP,KAAK,EAAE,CAAC;EACR,MAAM,EAAE,CAAC;EACT,gBAAgB,EAAE,kBAAe;EAAE,mCAAmC;EACtE,OAAO,EAAE,CAAC;EAAE,qFAAqF;EACjG,MAAM,EAAE,OAAO;EAAE,4BAA4B;;AAGjD,4BAOC;EANG,EAAG;IACC,SAAS,EAAE,YAAY;EAE3B,IAAK;IACD,SAAS,EAAE,cAAc;AAK7B,4BAAc;EACV,OAAO,EAAE,MAAM;EACf,aAAa,EAAE,KAAK;AAGpB,+BAAW;EACP,OAAO,EAAE,MAAM;AAEnB,+BAAW;EACP,SAAS,EAAE,IAAI;EACf,aAAa,EAAE,KAAK;AAExB,gCAAY;EACR,SAAS,EAAE,IAAI;EACf,aAAa,EAAE,KAAK;EAEpB,uCAAO;IACH,SAAS,EAAE,GAAG;AAGtB,iCAAa;EACT,OAAO,EAAE,YAAY;AAGzB,+BAAW;EACP,YAAY,EAAE,QAAQ;EACtB,qCAAQ;IACJ,eAAe,EAAE,IAAI;AAKjC,8BAAgB;EACZ,KAAK,EAAE,OAAO;AAElB,6BAAe;EACX,KAAK,EAAE,OAAO;;AAItB,WAAY;EACR,SAAS,EAAE,KAAK;EAChB,MAAM,EAAE,MAAM;;AAId,2BAAe;EACX,OAAO,EAAE,IAAI;;AAIrB,kBAAmB;EACf,MAAM,EAAE,QAAQ;EAChB,OAAO,EAAE,QAAQ;EAEjB,qBAAG;IACC,MAAM,EAAE,CAAC",
"sources": ["style.scss"],
"names": [],
"file": "style.css"

View File

@ -16,6 +16,14 @@ $accent-color: #007bff;
height: 64px;
}
.loading-dual-ring-center-screen {
position: fixed;
top: 50%;
left: 50%;
margin-top: -32px;
margin-left: -32px;
}
.loading-dual-ring:after {
content: " ";
display: block;
@ -28,6 +36,20 @@ $accent-color: #007bff;
animation: loading-dual-ring 1.2s linear infinite;
}
.black-overlay {
position: fixed; /* Sit on top of the page content */
display: none; /* Hidden by default */
width: 100%; /* Full width (cover the whole page) */
height: 100%; /* Full height (cover the whole page) */
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0,0,0,0.5); /* Black background with opacity */
z-index: 2; /* Specify a stack order in case you're using a different order for other elements */
cursor: pointer; /* Add a pointer on hover */
}
@keyframes loading-dual-ring {
0% {
transform: rotate(0deg);
@ -87,4 +109,14 @@ $accent-color: #007bff;
.asteriskField {
display: none;
}
}
.modal-field-error {
margin: 0.5rem 0;
padding: 0.5rem 0;
ul {
margin: 0;
}
}

View File

@ -1,13 +0,0 @@
{% extends 'YtManagerApp/controls/modal.html' %}
{% load crispy_forms_tags %}
{% block modal_title %}
New folder
{% endblock modal_title %}
{% block modal_body %}
{% crispy form %}
{% endblock modal_body %}
{% block modal_footer_wrapper %}
{% endblock modal_footer_wrapper %}

View File

@ -0,0 +1,21 @@
{% extends 'YtManagerApp/controls/modal.html' %}
{% load crispy_forms_tags %}
{% block modal_title %}
New folder
{% endblock modal_title %}
{% block modal_content %}
<form action="{% url 'modal_create_folder' %}" method="post">
{{ block.super }}
</form>
{% endblock %}
{% block modal_body %}
{% crispy form %}
{% endblock modal_body %}
{% block modal_footer %}
<input class="btn btn-primary" type="submit" value="Create">
<input class="btn btn-secondary" type="button" value="Cancel" data-dismiss="modal" aria-label="Cancel">
{% endblock modal_footer %}

View File

@ -0,0 +1,21 @@
{% extends 'YtManagerApp/controls/modal.html' %}
{% load crispy_forms_tags %}
{% block modal_title %}
Delete folder
{% endblock modal_title %}
{% block modal_content %}
<form action="{% url 'modal_delete_folder' form.instance.pk %}" method="post">
{{ block.super }}
</form>
{% endblock %}
{% block modal_body %}
{% crispy form %}
{% endblock modal_body %}
{% block modal_footer %}
<input class="btn btn-danger" type="submit" value="Save" aria-label="Delete">
<input class="btn btn-secondary" type="button" value="Cancel" data-dismiss="modal" aria-label="Cancel">
{% endblock modal_footer %}

View File

@ -1,40 +0,0 @@
<div id="folderEditDialog" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 id="folderEditDialog_Title" class="modal-title">Edit folder</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div id="folderEditDialog_Loading" class="modal-body">
<div class="loading-dual-ring"></div>
</div>
<div id="folderEditDialog_Error">
</div>
<form id="folderEditDialog_Form" action="{% url 'ajax_edit_folder' %}" method="post">
<div class="modal-body">
{% csrf_token %}
<input type="hidden" id="folderEditDialog_Id" name="id" value="#">
<div class="form-group row">
<label class="col-sm-3" for="folderEditDialog_Name">Name</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="folderEditDialog_Name" name="name" placeholder="Folder name">
</div>
</div>
<div class="form-group row">
<label class="col-sm-3" for="folderEditDialog_Parent">Parent folder</label>
<div class="col-sm-9">
<select class="form-control" id="folderEditDialog_Parent" name="parent">
</select>
</div>
</div>
</div>
<div class="modal-footer">
<button id="folderEditDialog_Submit" type="submit" class="btn btn-primary">Submit</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div>
</form>
</div>
</div>
</div>

View File

@ -0,0 +1,21 @@
{% extends 'YtManagerApp/controls/modal.html' %}
{% load crispy_forms_tags %}
{% block modal_title %}
Edit folder
{% endblock modal_title %}
{% block modal_content %}
<form action="{% url 'modal_update_folder' form.instance.pk %}" method="post">
{{ block.super }}
</form>
{% endblock %}
{% block modal_body %}
{% crispy form %}
{% endblock modal_body %}
{% block modal_footer %}
<input class="btn btn-primary" type="submit" value="Save" aria-label="Save">
<input class="btn btn-secondary" type="button" value="Cancel" data-dismiss="modal" aria-label="Cancel">
{% endblock modal_footer %}

View File

@ -11,14 +11,19 @@
<script>
{% include 'YtManagerApp/js/subscription_tree.js' %}
</script>
{% include 'YtManagerApp/controls/folder_edit_dialog.html' %}
{% include 'YtManagerApp/controls/subscription_edit_dialog.html' %}
{% endblock %}
{% block body %}
<div id="modal-wrapper">
<div id="modal-loading" class="black-overlay">
<div class="loading-dual-ring loading-dual-ring-center-screen"></div>
</div>
<div id="modal-wrapper">
</div>
</div>
<div class="row">
<div class="col-3">
@ -55,10 +60,10 @@
{% crispy filter_form %}
</div>
<div id="videos_wrapper">
<div id="videos-wrapper">
</div>
<div id="videos_loading" style="display: none">
<div id="videos-loading" style="display: none">
<div class="d-flex">
<div class="loading-dual-ring mx-auto my-5"></div>
</div>

View File

@ -42,4 +42,135 @@ class Dialog {
hideModal() {
this.modal.modal('hide');
}
}
}
class AjaxModal
{
constructor(url)
{
this.wrapper = $("#modal-wrapper");
this.loading = $("#modal-loading");
this.url = url;
this.modal = null;
this.form = null;
this.submitCallback = null;
}
setSubmitCallback(callback) {
this.submitCallback = callback;
}
_showLoading() {
this.loading.fadeIn(500);
}
_hideLoading() {
this.loading.fadeOut(100);
}
_showModal() {
if (this.modal != null)
this.modal.modal();
}
_hideModal() {
if (this.modal != null)
this.modal.modal('hide');
}
_load(result) {
this.wrapper.html(result);
this.modal = this.wrapper.find('.modal');
this.form = this.wrapper.find('form');
let pThis = this;
this.form.submit(function(e) {
pThis._submit(e);
})
}
_loadFailed() {
this.wrapper.html('<div class="alert alert-danger">An error occurred while displaying the dialog!</div>');
}
_submit(e) {
let pThis = this;
let url = this.form.attr('action');
$.post(this.url, this.form.serialize())
.done(function(result) {
pThis._submitDone(result);
})
.fail(function() {
pThis._submitFailed();
});
e.preventDefault();
}
_submitDone(result) {
// Clear old errors first
this.form.find('.modal-field-error').remove();
if (result.success) {
this._hideModal();
if (this.submitCallback != null)
this.submitCallback();
}
else {
for (let field in result.errors)
if (result.errors.hasOwnProperty(field))
{
let errorsArray = result.errors[field];
let errorsConcat = "<div class=\"alert alert-danger modal-field-error\"><ul>";
for(let error of errorsArray) {
errorsConcat += `<li>${error.message}</li>`;
}
errorsConcat += '</ul></div>';
if (field === '__all__')
this.form.find('.modal-body').append(errorsConcat);
else
this.form.find(`[name='${field}']`).after(errorsConcat);
}
let errorsHtml = '';
let err = this.modal.find('#__modal_error');
if (err.length) {
err.html('An error occurred');
}
else {
this.modal.find('.modal-body').append(errorsHtml)
}
}
}
_submitFailed() {
// Clear old errors first
this.form.find('.modal-field-error').remove();
this.form.find('.modal-body')
.append(`<div class="alert alert-danger modal-field-error">An error occurred while processing request!</div>`);
}
loadAndShow()
{
let pThis = this;
this._showLoading();
$.get(this.url)
.done(function (result) {
pThis._load(result);
pThis._showModal();
})
.fail(function () {
pThis._loadFailed();
})
.always(function() {
pThis._hideLoading();
});
}
}

View File

@ -228,11 +228,19 @@ function treeNode_Edit()
if (selectedNodes.length === 1)
{
let node = selectedNodes[0];
if (node.type === 'folder') {
folderEditDialog.showEdit(node);
let id = node.id.replace('folder', '');
let modal = new AjaxModal("{% url 'modal_update_folder' 98765 %}".replace('98765', id));
modal.setSubmitCallback(tree_Refresh);
modal.loadAndShow();
}
else {
subscriptionEditDialog.showEdit(node);
//TODO:
//let id = node.id.replace('sub', '');
//let modal = new AjaxModal("{ url 'modal_update_subscription' 98765 }".replace('98765', id));
//modal.setSubmitCallback(tree_Refresh);
//modal.loadAndShow();
}
}
}
@ -245,22 +253,17 @@ function treeNode_Delete()
let node = selectedNodes[0];
if (node.type === 'folder') {
let folderId = node.id.toString().replace('folder', '');
if (confirm('Are you sure you want to delete folder "' + node.text + '" and all its descendants?\nNote: the subscriptions won\'t be deleted, they will only be moved outside.'))
{
$.post("{% url 'ajax_delete_folder' 99999 %}".replace('99999', folderId), {
csrfmiddlewaretoken: '{{ csrf_token }}'
}).done(tree_Refresh);
}
let id = node.id.replace('folder', '');
let modal = new AjaxModal("{% url 'modal_delete_folder' 98765 %}".replace('98765', id));
modal.setSubmitCallback(tree_Refresh);
modal.loadAndShow();
}
else {
let subId = node.id.toString().replace('sub', '');
if (confirm('Are you sure you want to delete subscription "' + node.text + '"?'))
{
$.post("{% url 'ajax_delete_subscription' 99999 %}".replace('99999', subId), {
csrfmiddlewaretoken: '{{ csrf_token }}'
}).done(tree_Refresh);
}
//TODO:
//let id = node.id.replace('sub', '');
//let modal = new AjaxModal("{ url 'modal_delete_subscription' 98765 }".replace('98765', id));
//modal.setSubmitCallback(tree_Refresh);
//modal.loadAndShow();
}
}
}
@ -318,11 +321,14 @@ function tree_OnSelectionChanged(e, data)
let filterForm_folderId = filterForm.find('#form_video_filter_folder_id');
let filterForm_subId = filterForm.find('#form_video_filter_subscription_id');
let node = data.instance.get_selected(true)[0];
// Fill folder/sub fields
if (node.type === 'folder') {
if (node == null) {
filterForm_folderId.val('');
filterForm_subId.val('');
}
else if (node.type === 'folder') {
let id = node.id.replace('folder', '');
filterForm_folderId.val(id);
filterForm_subId.val('');
@ -340,16 +346,16 @@ function tree_OnSelectionChanged(e, data)
function videos_Reload()
{
let filterForm = $('#form_video_filter');
let loadingDiv = $('#videos_loading');
let loadingDiv = $('#videos-loading');
loadingDiv.fadeIn(300);
// Perform query
$.post("{% url 'ajax_index_get_videos' %}", filterForm.serialize())
.done(function (result) {
$("#videos_wrapper").html(result);
$("#videos-wrapper").html(result);
})
.fail(function () {
$("#videos_wrapper").html('<div class="alert alert-danger">An error occurred while retrieving the video list!</div>');
$("#videos-wrapper").html('<div class="alert alert-danger">An error occurred while retrieving the video list!</div>');
})
.always(function() {
loadingDiv.fadeOut(100);
@ -387,8 +393,13 @@ $(document).ready(function ()
// folderEditDialog = new FolderEditDialog('#folderEditDialog');
// subscriptionEditDialog = new SubscriptionEditDialog('#subscriptionEditDialog');
//
// $("#btn_create_sub").on("click", function () { subscriptionEditDialog.showNew(); });
$("#btn_create_folder").on("click", function () {
let modal = new AjaxModal("{% url 'modal_create_folder' %}");
modal.setSubmitCallback(tree_Refresh);
modal.loadAndShow();
});
// $("#btn_create_folder").on("click", function () { folderEditDialog.showNew(); });
// $("#btn_edit_node").on("click", treeNode_Edit);
// $("#btn_delete_node").on("click", treeNode_Delete);
$("#btn_edit_node").on("click", treeNode_Edit);
$("#btn_delete_node").on("click", treeNode_Delete);
});

View File

@ -19,7 +19,7 @@ from django.conf.urls.static import static
from django.urls import path
from .views.auth import ExtendedLoginView, RegisterView, RegisterDoneView
from .views.index import index, ajax_get_tree, ajax_get_videos
from .views.index import index, ajax_get_tree, ajax_get_videos, CreateFolderModal, UpdateFolderModal, DeleteFolderModal
from .views import old_views
urlpatterns = [
@ -29,10 +29,20 @@ urlpatterns = [
path('register_done/', RegisterDoneView.as_view(), name='register_done'),
path('', include('django.contrib.auth.urls')),
path('ajax/index_get_tree', ajax_get_tree, name='ajax_index_get_tree'),
path('ajax/index_get_videos', ajax_get_videos, name='ajax_index_get_videos'),
# Ajax
path('ajax/index_get_tree/', ajax_get_tree, name='ajax_index_get_tree'),
path('ajax/index_get_videos/', ajax_get_videos, name='ajax_index_get_videos'),
# Modals
path('modal/create_folder/', CreateFolderModal.as_view(), name='modal_create_folder'),
path('modal/create_folder/<int:parent_id>/', CreateFolderModal.as_view(), name='modal_create_folder'),
path('modal/update_folder/<int:pk>/', UpdateFolderModal.as_view(), name='modal_update_folder'),
path('modal/delete_folder/<int:pk>/', DeleteFolderModal.as_view(), name='modal_delete_folder'),
# Index
path('', index, name='home'),
# Old stuff
path('ajax/get_children', old_views.ajax_get_children, name='ajax_get_children'),
path('ajax/get_folders', old_views.ajax_get_folders, name='ajax_get_folders'),

View File

@ -1,8 +1,10 @@
from django.views.generic import TemplateView
from django.views.generic.base import ContextMixin
from django.http import JsonResponse
class ModalView(TemplateView):
class ModalMixin(ContextMixin):
template_name = 'YtManagerApp/controls/modal.html'
success_url = '/'
def __init__(self, modal_id='dialog', title='', fade=True, centered=True, small=False, large=False, *args, **kwargs):
super().__init__(*args, **kwargs)
@ -32,3 +34,18 @@ class ModalView(TemplateView):
data['modal_title'] = self.title
return data
def modal_response(self, form, success=True):
result = {'success': success}
if not success:
result['errors'] = form.errors.get_json_data(escape_html=True)
return JsonResponse(result)
def form_valid(self, form):
super().form_valid(form)
return self.modal_response(form, success=True)
def form_invalid(self, form):
super().form_invalid(form)
return self.modal_response(form, success=False)

View File

@ -1,13 +1,15 @@
from django.http import HttpRequest, HttpResponseBadRequest, JsonResponse
from django.shortcuts import render
from django import forms
from django.views.generic import CreateView
from django.views.generic import CreateView, UpdateView, DeleteView
from YtManagerApp.management.folders import traverse_tree
from YtManagerApp.management.videos import get_videos
from YtManagerApp.models import Subscription, SubscriptionFolder
from YtManagerApp.views.controls.modal import ModalView
from YtManagerApp.views.controls.modal import ModalMixin
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Field
from crispy_forms.layout import Submit
from django.db.models import Q
class VideoFilterForm(forms.Form):
@ -162,8 +164,62 @@ def ajax_get_videos(request: HttpRequest):
return HttpResponseBadRequest()
class CreateFolderForm(CreateView, ModalView):
model = SubscriptionFolder
template_name = 'YtManagerApp/controls/folder_create_dialog.html'
fields = ['name', 'parent']
class SubscriptionFolderForm(forms.ModelForm):
class Meta:
model = SubscriptionFolder
fields = ['name', 'parent']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_tag = False
def clean_name(self):
name = self.cleaned_data['name']
return name.strip()
def clean(self):
cleaned_data = super().clean()
name = cleaned_data.get('name')
parent = cleaned_data.get('parent')
# Check name is unique in parent folder
args_id = []
if self.instance is not None:
args_id.append(~Q(id=self.instance.id))
if SubscriptionFolder.objects.filter(parent=parent, name__iexact=name, *args_id).count() > 0:
raise forms.ValidationError('A folder with the same name already exists in the given parent directory!', code='already_exists')
# Check for cycles
if self.instance is not None:
self.__test_cycles(parent)
def __test_cycles(self, new_parent):
visited = [self.instance.id]
current = new_parent
while current is not None:
if current.id in visited:
raise forms.ValidationError('Selected parent would create a parenting cycle!', code='parenting_cycle')
visited.append(current.id)
current = current.parent
class CreateFolderModal(ModalMixin, CreateView):
template_name = 'YtManagerApp/controls/folder_create_modal.html'
form_class = SubscriptionFolderForm
def form_valid(self, form):
form.instance.user = self.request.user
return super().form_valid(form)
class UpdateFolderModal(ModalMixin, UpdateView):
template_name = 'YtManagerApp/controls/folder_update_modal.html'
model = SubscriptionFolder
form_class = SubscriptionFolderForm
class DeleteFolderModal(ModalMixin, DeleteView):
template_name = 'YtManagerApp/controls/folder_delete_modal.html'
model = SubscriptionFolder