diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index 5bb447b..4009c67 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -2,63 +2,24 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
-
-
-
-
-
+
+
@@ -68,6 +29,7 @@
+
@@ -114,9 +76,9 @@
-
+
-
+
@@ -125,27 +87,27 @@
-
-
+
+
-
+
-
-
-
+
+
+
-
-
+
+
-
+
-
-
-
+
+
+
@@ -155,82 +117,80 @@
-
+
-
+
-
+
-
+
-
+
-
-
+
+
-
+
-
+
-
+
-
+
-
-
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
+
-
-
+
+
+
+
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
+
+
@@ -238,21 +198,64 @@
-
-
-
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -270,10 +273,6 @@
- lds
- get_selected
- url
- delete_fo
folder
modal
folderEditDialog
@@ -298,6 +297,12 @@
dialog_
.find
.show
+ videos_wrapper
+ videos_loading
+ submit
+ modal_edit_folder
+ modal_update_folder
+ modal_delete_folder
loading
@@ -308,6 +313,8 @@
=
dj_settings
modal_
+ videos-wrapper
+ videos-loading
@@ -316,11 +323,6 @@
@@ -382,10 +389,10 @@
-
-
-
-
+
+
+
+
@@ -530,13 +537,6 @@
-
-
-
-
-
-
-
@@ -556,6 +556,11 @@
+
+
+
+
+
@@ -563,11 +568,6 @@
-
-
-
-
-
@@ -581,7 +581,7 @@
-
+
@@ -604,6 +604,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -654,9 +676,11 @@
+
+
@@ -676,15 +700,15 @@
-
+
-
+
-
+
@@ -692,7 +716,7 @@
-
+
@@ -701,7 +725,7 @@
-
+
@@ -747,36 +771,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -847,13 +841,6 @@
-
-
-
-
-
-
-
@@ -905,13 +892,7 @@
-
-
-
-
-
-
-
+
@@ -919,13 +900,6 @@
-
-
-
-
-
-
-
@@ -974,17 +948,6 @@
-
-
-
-
-
-
-
-
-
-
-
@@ -992,26 +955,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -1032,20 +975,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -1056,60 +985,11 @@
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
@@ -1120,29 +1000,166 @@
-
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/YtManagerApp/management/folders.py b/YtManagerApp/management/folders.py
index d1daace..a5af081 100644
--- a/YtManagerApp/management/folders.py
+++ b/YtManagerApp/management/folders.py
@@ -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
diff --git a/YtManagerApp/migrations/0004_auto_20181014_1702.py b/YtManagerApp/migrations/0004_auto_20181014_1702.py
new file mode 100644
index 0000000..3e8e66a
--- /dev/null
+++ b/YtManagerApp/migrations/0004_auto_20181014_1702.py
@@ -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),
+ ),
+ ]
diff --git a/YtManagerApp/models.py b/YtManagerApp/models.py
index abe7950..81cc1dc 100644
--- a/YtManagerApp/models.py
+++ b/YtManagerApp/models.py
@@ -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):
diff --git a/YtManagerApp/static/YtManagerApp/css/style.css b/YtManagerApp/static/YtManagerApp/css/style.css
index 82be77a..f95552a 100644
--- a/YtManagerApp/static/YtManagerApp/css/style.css
+++ b/YtManagerApp/static/YtManagerApp/css/style.css
@@ -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 */
diff --git a/YtManagerApp/static/YtManagerApp/css/style.css.map b/YtManagerApp/static/YtManagerApp/css/style.css.map
index 145e6a0..8a24164 100644
--- a/YtManagerApp/static/YtManagerApp/css/style.css.map
+++ b/YtManagerApp/static/YtManagerApp/css/style.css.map
@@ -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"
diff --git a/YtManagerApp/static/YtManagerApp/css/style.scss b/YtManagerApp/static/YtManagerApp/css/style.scss
index 0f95473..ef655dd 100644
--- a/YtManagerApp/static/YtManagerApp/css/style.scss
+++ b/YtManagerApp/static/YtManagerApp/css/style.scss
@@ -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;
+ }
+
}
\ No newline at end of file
diff --git a/YtManagerApp/templates/YtManagerApp/controls/folder_create_dialog.html b/YtManagerApp/templates/YtManagerApp/controls/folder_create_dialog.html
deleted file mode 100644
index b3112e4..0000000
--- a/YtManagerApp/templates/YtManagerApp/controls/folder_create_dialog.html
+++ /dev/null
@@ -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 %}
\ No newline at end of file
diff --git a/YtManagerApp/templates/YtManagerApp/controls/folder_create_modal.html b/YtManagerApp/templates/YtManagerApp/controls/folder_create_modal.html
new file mode 100644
index 0000000..74b1c2b
--- /dev/null
+++ b/YtManagerApp/templates/YtManagerApp/controls/folder_create_modal.html
@@ -0,0 +1,21 @@
+{% extends 'YtManagerApp/controls/modal.html' %}
+{% load crispy_forms_tags %}
+
+{% block modal_title %}
+ New folder
+{% endblock modal_title %}
+
+{% block modal_content %}
+
+{% endblock %}
+
+{% block modal_body %}
+ {% crispy form %}
+{% endblock modal_body %}
+
+{% block modal_footer %}
+
+
+{% endblock modal_footer %}
\ No newline at end of file
diff --git a/YtManagerApp/templates/YtManagerApp/controls/folder_delete_modal.html b/YtManagerApp/templates/YtManagerApp/controls/folder_delete_modal.html
new file mode 100644
index 0000000..faeca72
--- /dev/null
+++ b/YtManagerApp/templates/YtManagerApp/controls/folder_delete_modal.html
@@ -0,0 +1,21 @@
+{% extends 'YtManagerApp/controls/modal.html' %}
+{% load crispy_forms_tags %}
+
+{% block modal_title %}
+ Delete folder
+{% endblock modal_title %}
+
+{% block modal_content %}
+
+{% endblock %}
+
+{% block modal_body %}
+ {% crispy form %}
+{% endblock modal_body %}
+
+{% block modal_footer %}
+
+
+{% endblock modal_footer %}
\ No newline at end of file
diff --git a/YtManagerApp/templates/YtManagerApp/controls/folder_edit_dialog.html b/YtManagerApp/templates/YtManagerApp/controls/folder_edit_dialog.html
deleted file mode 100644
index 8cefaa1..0000000
--- a/YtManagerApp/templates/YtManagerApp/controls/folder_edit_dialog.html
+++ /dev/null
@@ -1,40 +0,0 @@
-
\ No newline at end of file
diff --git a/YtManagerApp/templates/YtManagerApp/controls/folder_update_modal.html b/YtManagerApp/templates/YtManagerApp/controls/folder_update_modal.html
new file mode 100644
index 0000000..a481a74
--- /dev/null
+++ b/YtManagerApp/templates/YtManagerApp/controls/folder_update_modal.html
@@ -0,0 +1,21 @@
+{% extends 'YtManagerApp/controls/modal.html' %}
+{% load crispy_forms_tags %}
+
+{% block modal_title %}
+ Edit folder
+{% endblock modal_title %}
+
+{% block modal_content %}
+
+{% endblock %}
+
+{% block modal_body %}
+ {% crispy form %}
+{% endblock modal_body %}
+
+{% block modal_footer %}
+
+
+{% endblock modal_footer %}
\ No newline at end of file
diff --git a/YtManagerApp/templates/YtManagerApp/index.html b/YtManagerApp/templates/YtManagerApp/index.html
index a73a571..56c83f9 100644
--- a/YtManagerApp/templates/YtManagerApp/index.html
+++ b/YtManagerApp/templates/YtManagerApp/index.html
@@ -11,14 +11,19 @@
-
- {% include 'YtManagerApp/controls/folder_edit_dialog.html' %}
- {% include 'YtManagerApp/controls/subscription_edit_dialog.html' %}
-
{% endblock %}
{% block body %}
+
+
@@ -55,10 +60,10 @@
{% crispy filter_form %}
-
+
-
+
diff --git a/YtManagerApp/templates/YtManagerApp/js/common.js b/YtManagerApp/templates/YtManagerApp/js/common.js
index 03c1f1a..fdf451b 100644
--- a/YtManagerApp/templates/YtManagerApp/js/common.js
+++ b/YtManagerApp/templates/YtManagerApp/js/common.js
@@ -42,4 +42,135 @@ class Dialog {
hideModal() {
this.modal.modal('hide');
}
-}
\ No newline at end of file
+}
+
+
+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('
An error occurred while displaying the dialog!
');
+ }
+
+ _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 = "
";
+
+ for(let error of errorsArray) {
+ errorsConcat += `- ${error.message}
`;
+ }
+ errorsConcat += '
';
+
+ 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(`
An error occurred while processing request!
`);
+ }
+
+ 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();
+ });
+ }
+}
diff --git a/YtManagerApp/templates/YtManagerApp/js/subscription_tree.js b/YtManagerApp/templates/YtManagerApp/js/subscription_tree.js
index 0bacff2..5127f6b 100644
--- a/YtManagerApp/templates/YtManagerApp/js/subscription_tree.js
+++ b/YtManagerApp/templates/YtManagerApp/js/subscription_tree.js
@@ -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('
An error occurred while retrieving the video list!
');
+ $("#videos-wrapper").html('
An error occurred while retrieving the video list!
');
})
.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);
});
diff --git a/YtManagerApp/urls.py b/YtManagerApp/urls.py
index 25c65a8..347bbbb 100644
--- a/YtManagerApp/urls.py
+++ b/YtManagerApp/urls.py
@@ -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/
/', CreateFolderModal.as_view(), name='modal_create_folder'),
+ path('modal/update_folder//', UpdateFolderModal.as_view(), name='modal_update_folder'),
+ path('modal/delete_folder//', 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'),
diff --git a/YtManagerApp/views/controls/modal.py b/YtManagerApp/views/controls/modal.py
index ec3472b..52f80dc 100644
--- a/YtManagerApp/views/controls/modal.py
+++ b/YtManagerApp/views/controls/modal.py
@@ -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)
diff --git a/YtManagerApp/views/index.py b/YtManagerApp/views/index.py
index 64ec46a..e1a5b90 100644
--- a/YtManagerApp/views/index.py
+++ b/YtManagerApp/views/index.py
@@ -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