mirror of
				https://github.com/chibicitiberiu/ytsm.git
				synced 2024-02-24 05:43:31 +00:00 
			
		
		
		
	Worked on folder modals.
This commit is contained in:
		@@ -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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										18
									
								
								YtManagerApp/migrations/0004_auto_20181014_1702.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								YtManagerApp/migrations/0004_auto_20181014_1702.py
									
									
									
									
									
										Normal 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),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@@ -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):
 | 
			
		||||
 
 | 
			
		||||
@@ -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 */
 | 
			
		||||
 
 | 
			
		||||
@@ -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"
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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 %}
 | 
			
		||||
@@ -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 %}
 | 
			
		||||
@@ -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 %}
 | 
			
		||||
@@ -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">×</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>
 | 
			
		||||
@@ -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 %}
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
@@ -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();
 | 
			
		||||
            });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -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'),
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user