diff options
-rw-r--r-- | apps/utils/util.py | 12 | ||||
-rw-r--r-- | apps/utils/views.py | 48 | ||||
-rw-r--r-- | config/base_urls.py | 6 | ||||
-rw-r--r-- | config/settings.py | 4 | ||||
-rw-r--r-- | scripts/gulpfile.js | 17 | ||||
-rw-r--r-- | scripts/package.json | 40 | ||||
-rw-r--r-- | scripts/src/js.cookie.js | 165 | ||||
-rw-r--r-- | scripts/src/main-nav.js | 56 | ||||
-rw-r--r-- | scripts/src/overlay.js | 253 | ||||
-rw-r--r-- | scripts/src/util.js | 79 |
10 files changed, 108 insertions, 572 deletions
diff --git a/apps/utils/util.py b/apps/utils/util.py index 415704c..287205a 100644 --- a/apps/utils/util.py +++ b/apps/utils/util.py @@ -96,9 +96,9 @@ def unique_slug_generator(instance, new_slug=None): slug = new_slug else: try: - slug = slugify(instance.title) + slug = slugify(instance.title)[:45] except AttributeError: - slug = slugify(instance.name) + slug = slugify(instance.name)[:45] Klass = instance.__class__ qs_exists = Klass.objects.filter(slug=slug).exists() if qs_exists: @@ -108,3 +108,11 @@ def unique_slug_generator(instance, new_slug=None): ) return unique_slug_generator(instance, new_slug=new_slug) return slug + + +def comma_splitter(tag_string): + return [t.strip().lower() for t in tag_string.split(',') if t.strip()] + + +def comma_joiner(tags): + return ', '.join(t.name for t in tags) diff --git a/apps/utils/views.py b/apps/utils/views.py index dcb16a8..595e102 100644 --- a/apps/utils/views.py +++ b/apps/utils/views.py @@ -1,11 +1,18 @@ from itertools import chain import json from django.http import Http404, HttpResponse +from django.apps import apps from django.views.generic import ListView -from photos.models import LuxImage, LuxVideo +from django.views.generic.base import View, RedirectView +from django.utils.decorators import method_decorator +from django.contrib.auth.decorators import login_required +#from photos.models import LuxImage, LuxVideo from django.shortcuts import render_to_response from django.shortcuts import render from django.template import RequestContext +from taggit.models import Tag +#from dal import autocomplete + class PaginatedListView(ListView): """ @@ -26,24 +33,16 @@ class PaginatedListView(ListView): return super(PaginatedListView, self).dispatch(request, *args, **kwargs) -def insert_image(request): - """ - The view that handles the admin insert image/video feature - """ - images = LuxImage.objects.all()[:80] - videos = LuxVideo.objects.all()[:10] - object_list = sorted( - chain(images, videos), - key=lambda instance: instance.pub_date, - reverse=True - ) - return render(request, 'admin/insert_images.html', {'object_list': object_list, 'textarea_id': request.GET['textarea']}) - +@method_decorator(login_required, name='dispatch') +class LoggedInViewWithUser(View): -from taggit.models import Tag + def get_form_kwargs(self, **kwargs): + kwargs = super().get_form_kwargs(**kwargs) + kwargs.update({'user': self.request.user}) + return kwargs -from dal import autocomplete +''' class TagAutocomplete(autocomplete.Select2QuerySetView): def get_queryset(self): # Don't forget to filter out results depending on the visitor ! @@ -57,7 +56,6 @@ class TagAutocomplete(autocomplete.Select2QuerySetView): return qs -from django.apps import apps def nav_json(request, app, model, pk): model = apps.get_model(app_label=app, model_name=model) @@ -67,3 +65,19 @@ def nav_json(request, app, model, pk): data['next'] = p.get_next_admin_url data = json.dumps(data) return HttpResponse(data) + + +def insert_image(request): + """ + The view that handles the admin insert image/video feature + """ + images = LuxImage.objects.all()[:80] + videos = LuxVideo.objects.all()[:10] + object_list = sorted( + chain(images, videos), + key=lambda instance: instance.pub_date, + reverse=True + ) + return render(request, 'admin/insert_images.html', {'object_list': object_list, 'textarea_id': request.GET['textarea']}) +''' + diff --git a/config/base_urls.py b/config/base_urls.py index c881679..775f07d 100644 --- a/config/base_urls.py +++ b/config/base_urls.py @@ -14,6 +14,7 @@ from notes.views import ( NoteViewSet, NotebookViewSet, NoteListView, + TagViewSet, ) # redirect admin as per Adrian holovaty's site @@ -22,6 +23,7 @@ ADMIN_URL = 'https://docs.djangoproject.com/en/dev/ref/contrib/admin/' router = routers.DefaultRouter() router.register(r'notes/notebook', NotebookViewSet, basename="notebook-api") router.register(r'notes', NoteViewSet, basename="notes-api") +router.register(r'tags', TagViewSet, basename="tags-api") urlpatterns = static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) @@ -35,7 +37,9 @@ urlpatterns += [ path(r'', include('django_registration.backends.activation.urls')), path(r'', include('django.contrib.auth.urls')), path(r'', NoteListView.as_view(), name='homepage',), - path(r'notes/', include('notes.urls')), + path(r'forum/', include('forum.urls')), + path(r'n/', include('notes.notes_urls')), + path(r'nb/', include('notes.notebook_urls')), path(r'api/v1/', include(router.urls)), path(r'<slug>', PageDetailView.as_view(), name="pages"), #path(r'<path>/<slug>/', PageDetailView.as_view(), name="pages"), diff --git a/config/settings.py b/config/settings.py index da75f57..8e2ba3c 100644 --- a/config/settings.py +++ b/config/settings.py @@ -47,7 +47,8 @@ REST_FRAMEWORK = { 'rest_framework.permissions.IsAuthenticated', ) } - +TAGGIT_TAGS_FROM_STRING = 'utils.util.comma_splitter' +TAGGIT_STRING_FROM_TAGS = 'utils.util.comma_joiner' # APPS # https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps DJANGO_APPS = [ @@ -71,6 +72,7 @@ LOCAL_APPS = [ 'pages', 'accounts', 'notes', + 'forum', ] INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS diff --git a/scripts/gulpfile.js b/scripts/gulpfile.js deleted file mode 100644 index 67c5bf7..0000000 --- a/scripts/gulpfile.js +++ /dev/null @@ -1,17 +0,0 @@ -const { src, dest } = require('gulp'); -const uglify = require('gulp-uglify'); -const rename = require('gulp-rename'); -const concat = require('gulp-concat'); -const babel = require('gulp-babel'); - -exports.default = function() { - return src('src/*.js') - .pipe(babel({ - presets: ['@babel/env'] - })) - .pipe(concat('package.js')) - .pipe(uglify()) - .pipe(dest('../media/js/')) - .pipe(rename({ extname: '.min.js' })) - .pipe(dest('../media/js/')); -} diff --git a/scripts/package.json b/scripts/package.json index 778b5e6..890274e 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -1,20 +1,24 @@ { - "name": "scripts", - "version": "1.0.0", - "description": "javascript for aite", - "main": "common.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "author": "luxagraf", - "license": "ISC", - "devDependencies": { - "@babel/core": "^7.1.6", - "@babel/preset-env": "^7.1.6", - "gulp": "^4.0.0", - "gulp-babel": "^8.0.0", - "gulp-concat": "^2.6.1", - "gulp-rename": "^1.4.0", - "gulp-uglify": "^3.0.1" - } + "name": "notes-app", + "version": "1.0.0", + "author": "luxagraf", + "description": "Compile JS for Notes app", + "main": "src/js/index.js", + "scripts": { + "eslint": "eslint src/*.js --fix", + "babel": "mkdir -p tmp && babel src/*.js -d tmp", + "include": "cp src/lib/*.js tmp/", + "uglify:local": "mkdir -p ../media/js && uglifyjs tmp/*.js -m -c -o ../media/js/main.min.js", + "uglify:deploy": "mkdir -p ../media/js && uglifyjs tmp/*.js -m -c drop_console=true -o ../media/js/main.min.js", + "cleanup": "rm -rf tmp", + "build": "pnpm run babel && pnpm run include && pnpm run uglify:local && pnpm run cleanup", + "deploy": "pnpm run babel && pnpm run include && pnpm run uglify && pnpm run cleanup", + "watch": "watch 'pnpm run build' ." + }, + "devDependencies": { + "@babel/cli": "^7.1.5", + "@babel/core": "^7.1.6", + "@babel/preset-env": "^7.1.6", + "eslint": "^5.9.0" + } } diff --git a/scripts/src/js.cookie.js b/scripts/src/js.cookie.js deleted file mode 100644 index 9a0945e..0000000 --- a/scripts/src/js.cookie.js +++ /dev/null @@ -1,165 +0,0 @@ -/*! - * JavaScript Cookie v2.2.0 - * https://github.com/js-cookie/js-cookie - * - * Copyright 2006, 2015 Klaus Hartl & Fagner Brack - * Released under the MIT license - */ -;(function (factory) { - var registeredInModuleLoader = false; - if (typeof define === 'function' && define.amd) { - define(factory); - registeredInModuleLoader = true; - } - if (typeof exports === 'object') { - module.exports = factory(); - registeredInModuleLoader = true; - } - if (!registeredInModuleLoader) { - var OldCookies = window.Cookies; - var api = window.Cookies = factory(); - api.noConflict = function () { - window.Cookies = OldCookies; - return api; - }; - } -}(function () { - function extend () { - var i = 0; - var result = {}; - for (; i < arguments.length; i++) { - var attributes = arguments[ i ]; - for (var key in attributes) { - result[key] = attributes[key]; - } - } - return result; - } - - function init (converter) { - function api (key, value, attributes) { - var result; - if (typeof document === 'undefined') { - return; - } - - // Write - - if (arguments.length > 1) { - attributes = extend({ - path: '/' - }, api.defaults, attributes); - - if (typeof attributes.expires === 'number') { - var expires = new Date(); - expires.setMilliseconds(expires.getMilliseconds() + attributes.expires * 864e+5); - attributes.expires = expires; - } - - // We're using "expires" because "max-age" is not supported by IE - attributes.expires = attributes.expires ? attributes.expires.toUTCString() : ''; - - try { - result = JSON.stringify(value); - if (/^[\{\[]/.test(result)) { - value = result; - } - } catch (e) {} - - if (!converter.write) { - value = encodeURIComponent(String(value)) - .replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g, decodeURIComponent); - } else { - value = converter.write(value, key); - } - - key = encodeURIComponent(String(key)); - key = key.replace(/%(23|24|26|2B|5E|60|7C)/g, decodeURIComponent); - key = key.replace(/[\(\)]/g, escape); - - var stringifiedAttributes = ''; - - for (var attributeName in attributes) { - if (!attributes[attributeName]) { - continue; - } - stringifiedAttributes += '; ' + attributeName; - if (attributes[attributeName] === true) { - continue; - } - stringifiedAttributes += '=' + attributes[attributeName]; - } - return (document.cookie = key + '=' + value + stringifiedAttributes); - } - - // Read - - if (!key) { - result = {}; - } - - // To prevent the for loop in the first place assign an empty array - // in case there are no cookies at all. Also prevents odd result when - // calling "get()" - var cookies = document.cookie ? document.cookie.split('; ') : []; - var rdecode = /(%[0-9A-Z]{2})+/g; - var i = 0; - - for (; i < cookies.length; i++) { - var parts = cookies[i].split('='); - var cookie = parts.slice(1).join('='); - - if (!this.json && cookie.charAt(0) === '"') { - cookie = cookie.slice(1, -1); - } - - try { - var name = parts[0].replace(rdecode, decodeURIComponent); - cookie = converter.read ? - converter.read(cookie, name) : converter(cookie, name) || - cookie.replace(rdecode, decodeURIComponent); - - if (this.json) { - try { - cookie = JSON.parse(cookie); - } catch (e) {} - } - - if (key === name) { - result = cookie; - break; - } - - if (!key) { - result[name] = cookie; - } - } catch (e) {} - } - - return result; - } - - api.set = api; - api.get = function (key) { - return api.call(api, key); - }; - api.getJSON = function () { - return api.apply({ - json: true - }, [].slice.call(arguments)); - }; - api.defaults = {}; - - api.remove = function (key, attributes) { - api(key, '', extend(attributes, { - expires: -1 - })); - }; - - api.withConverter = init; - - return api; - } - - return init(function () {}); -})); diff --git a/scripts/src/main-nav.js b/scripts/src/main-nav.js index fa6f25a..c85cb4f 100644 --- a/scripts/src/main-nav.js +++ b/scripts/src/main-nav.js @@ -5,44 +5,46 @@ function hideOnClickOutsided(element, btn) { const outsideClickListener = event => { if (!element.contains(event.target) && (event.target.id != btn)) { // or use: event.target.closest(selector) === null if (isVisible(element)) { - element.classList.remove('active') - removeClickListener() + element.classList.remove('active'); + removeClickListener(); } } - } + }; const removeClickListener = () => { - document.removeEventListener('click', outsideClickListener) - } - document.addEventListener('click', outsideClickListener) + document.removeEventListener('click', outsideClickListener); + }; + document.addEventListener('click', outsideClickListener); } -const isVisible = elem => !!elem && !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length ) // source (2018-03-11): https://github.com/jquery/jquery/blob/master/src/css/hiddenVisibleSelectors.js +const isVisible = elem => !!elem && !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length ); // source (2018-03-11): https://github.com/jquery/jquery/blob/master/src/css/hiddenVisibleSelectors.js //---------------------------------------- //Initialize main menu bar with progressive enhancements +//if we find the menu items //---------------------------------------------- // Account Menu -var account_a = document.getElementById("account-menu"); -var account_div = document.getElementById("user-menu"); -//Add button function -account_a.addEventListener('click', function(e){ - e.preventDefault(); - account_div.classList.toggle('active') - account_div.focus(); - hideOnClickOutsided(account_div, account_a.id); -}, false); - -// Notebooks Menu -var notebook_a = document.getElementById("notebook-menu-link"); -var notebook_div = document.getElementById("notebooks-menu"); -//Add button function -notebook_a.addEventListener('click', function(e){ - e.preventDefault(); - notebook_div.classList.toggle('active') - notebook_div.focus(); - hideOnClickOutsided(notebook_div, notebook_a.id); -}, false); +if (document.getElementById('account-menu')) { + var account_a = document.getElementById('account-menu'); + var account_div = document.getElementById('user-menu'); + //Add button function + account_a.addEventListener('click', function(e){ + e.preventDefault(); + account_div.classList.toggle('active'); + account_div.focus(); + hideOnClickOutsided(account_div, account_a.id); + }, false); + // Notebooks Menu + var notebook_a = document.getElementById('notebook-menu-link'); + var notebook_div = document.getElementById('notebooks-menu'); + //Add button function + notebook_a.addEventListener('click', function(e){ + e.preventDefault(); + notebook_div.classList.toggle('active'); + notebook_div.focus(); + hideOnClickOutsided(notebook_div, notebook_a.id); + }, false); +} //function buildNotebookMenu () { // var data = JSON.parse(this.responseText); diff --git a/scripts/src/overlay.js b/scripts/src/overlay.js deleted file mode 100644 index b40d911..0000000 --- a/scripts/src/overlay.js +++ /dev/null @@ -1,253 +0,0 @@ -'use strict'; -/** - * @name Novicell overlay - * @desc Simple script that opens an overlay / modal with some content form either a selector or an URL - * @author Danni Larsen (DLA), Michael Sølvsteen (MSL), Signe Helbo Poulsen (SHP), Emil Skytte Ankersen (EAN) - * @example novicell.overlay.create({ 'selector': SELECTOR, 'url': URL, 'class':'CLASSNAME', 'onCreate': FUNCTIONNAME, 'onLoaded': FUNCTIONNAME, 'onDestroy': FUNCTIONNAME }); - * @requires none - */ - -var novicell = novicell || {}; - -novicell.overlay = novicell.overlay || new function () { - var self = this; - var options = {}; - var overlayElem; - var overlayContainer; - var overlayContent; - var backdrop; - var content; - var onCreate; - var onLoaded; - var onDestroy; - var isVideo = false; - - this.create = function (opts) { - var self = this; - // Set global options - options = opts; - - // Call onCreate callback - if (typeof options.onCreate === 'function') { - options.onCreate(); - } - - // Remove existing overlays - self.destroy(); - - // Check if content comes from a DOM selector - if (options.hasOwnProperty('selector') && options.selector !== null) { - var element = document.querySelector(options.selector); - - if (element) { - content = element.innerHTML; - constructOverlay(); - } else { - console.warn('novicell.overlay: element does not exist. Please provide a valid selector for use in document.querySelector.'); - return; - } - } - - // Check if content comes from a HTML element - else if (options.hasOwnProperty('element') && options.element !== null) { - var element = options.element; - - if (element) { - content = element.innerHTML; - constructOverlay(); - } else { - console.warn('novicell.overlay: element does not exist. Please provide a valid DOM element.'); - return; - } - } - - // Or if content comes from an ID - else if (options.hasOwnProperty('videoId')) { - if (options.videoId !== null) { - var src = ''; - isVideo = true; - - if(options.type == 'vimeo') { - src = 'https://player.vimeo.com/video/' + options.videoId + '?autoplay=' + options.autoplay; - } - else if(options.type == 'youtube') { - src = 'https://www.youtube.com/embed/' + options.videoId + '?autoplay=' + options.autoplay + '&rel=0'; - } - else { - return; - } - - var iframe = document.createElement('iframe'); - iframe.setAttribute('src', src); - iframe.setAttribute('frameborder', 0); - iframe.setAttribute('allowfullscreen', ''); - iframe.setAttribute('width', '100%'); - iframe.setAttribute('height', '100%'); - - content = iframe.outerHTML; - - constructOverlay(); - } else { - console.warn('novicell.overlay: video-id is empty. Please provide a video-id for use in video embed code (we support only Vimeo and YouTube).'); - return; - } - } - // If nothing is working, send error to los consolé - else { - console.error('novicell.overlay: no content to display! Please set a selector or a url to load.') - return; - } - }; - - this.destroy = function () { - if(document.querySelector('#js-novi-overlay')) { - // Remove elements - overlayElem.parentElement.removeChild(overlayElem); - backdrop.parentElement.removeChild(backdrop); - - // Stop listening for close overlay events - document.removeEventListener('keyup', self.destroy); - - // Remove class on body - document.documentElement.classList.remove('no-scroll', 'novi-overlay--open'); - - // Reset video variable - isVideo = false; - - // Call onDestroy callback - if (typeof options.onDestroy === 'function') { - options.onDestroy(); - } - } - }; - - function constructOverlay() { - // Create backdrop - setupBackdrop(); - - // Create the overlay - setupOverlay(); - - // Create content for overlay - setupOverlayContainer(); - - // Create close button - setupCloseButton(); - - // Add class to body-element - document.documentElement.classList.add('no-scroll'); - - // Call onLoaded callback - if (typeof options.onLoaded === 'function') { - options.onLoaded(); - } - }; - - function setupBackdrop() { - // Create the backdrop - backdrop = document.createElement('div'); - backdrop.classList.add('novi-backdrop'); - backdrop.id = 'js-novi-backdrop'; - - backdrop.addEventListener('click', function(e){ - if(e.target.classList.contains('novi-overlay') || e.target.classList.contains('novi-overlay__container')) { - self.destroy(); - } - }); - - // Add backdrop to overlay element - document.querySelector('body').appendChild(backdrop); - }; - - /* - * Helper functions for HTML elements - */ - function setupOverlay() { - // Create the overlay - overlayElem = document.createElement('div'); - overlayElem.classList.add('novi-overlay'); - overlayElem.id = 'js-novi-overlay'; - - // Set class for the overlay, if set in options - if (options.hasOwnProperty('class')) { - overlayElem.classList.add(options.class); - } - - // Add overlay to overlay element - // document.querySelector('body').appendChild(overlayElem); - backdrop.appendChild(overlayElem); - }; - - function setupOverlayContainer() { - // Create content for overlay - overlayContainer = document.createElement('div'); - overlayContainer.classList.add('novi-overlay__container'); - - // Create scroll element - overlayContent = document.createElement('div'); - overlayContent.classList.add('novi-overlay__content'); - - if(isVideo) { - overlayContent.classList.add('novi-overlay__content--video') - } - - // Set content - overlayContent.innerHTML = content; - overlayContainer.appendChild(overlayContent); - - // Add overlayContainer to overlay element - overlayElem.appendChild(overlayContainer); - }; - - function setupCloseButton() { - // Create the button - var btnClose = document.createElement('button'); - btnClose.classList.add('novi-overlay-close', 'button--close'); - btnClose.type = 'button'; - btnClose.id = 'js-novi-overlay-close'; - - // Add eventlistener for button click - btnClose.addEventListener('click', self.destroy); - - // Add eventlistener for esc key - document.addEventListener('keydown', function (e) { - if (e.keyCode === 27) { - self.destroy(); - } - }); - - // Add close button to overlay element - overlayContent.appendChild(btnClose); - }; - - /* - * Helper functions for getting content - */ - function get(url) { - // Return a new promise. - return new Promise(function (resolve, reject) { - // Do the usual XHR stuff - var req = new XMLHttpRequest(); - req.open('GET', url); - - req.onload = function () { - if (req.status >= 200 && req.status < 400) { - // Success!! - resolve(req.response); - } else { - // Error!! - reject(Error(req.statusText)); - } - }; - - // Handle network errors - req.onerror = function () { - reject(Error("Network Error")); - }; - - // Make the request - req.send(); - }); - }; - -}(); diff --git a/scripts/src/util.js b/scripts/src/util.js index 3a4efc4..dfaeeea 100644 --- a/scripts/src/util.js +++ b/scripts/src/util.js @@ -1,87 +1,24 @@ -function getJSON(url, callback) { +function getJSON(method, url, callback) { var request = new XMLHttpRequest(); - request.addEventListener("load", callback); - request.open('GET', url, true); + request.addEventListener('load', callback); + request.open(method, url, true); request.onload = function() { if (request.status >= 200 && request.status < 400) { //console.log(request.responseText); } else { - console.log("server error"); + console.log('server error'); } }; request.onerror = function() { - console.log("error on request"); + console.log('error on request'); }; request.send(); } -function edit_note(btn, title, qcontainer, quill, url){ - var formElement = document.getElementById("note-edit-form"); - if (window.editing === false) { - title.setAttribute("contenteditable", true); - title.classList.add('highlight') - qcontainer.classList.remove('inactive') - quill.enable(true); - btn.innerHTML = "Save" - btn.classList.add("save"); - window.editing = true; - window.titlecontents = title.innerHTML - } else { - if (window.quillchange === true || window.titlecontents != title.innerHTML) { - var form_note_title = document.getElementById('id_title'); - var note_html = document.getElementById('id_body_html'); - var note_text = document.getElementById('id_body_text'); - var note_qjson = document.getElementById('id_body_qjson'); - var new_title = document.getElementById('id_title'); - new_title.value = title.innerHTML; - note_html.innerHTML = quill.root.innerHTML; - note_text.innerHTML = quill.getText(); - note_qjson.innerHTML = JSON.stringify(quill.getContents()); - console.log(note_text); - var request = new XMLHttpRequest(); - request.open("PATCH", url); - var csrftoken = Cookies.get('csrftoken'); - request.setRequestHeader("X-CSRFToken", csrftoken) - request.onload = function() { - if (request.status >= 200 && request.status < 400) { - console.log(request); - window.quillchange = false; - } else { - console.log(request); - console.log("server error"); - } - }; - request.onerror = function() { - console.log("error on request"); - }; - request.send(new FormData(formElement)); - } - title.setAttribute("contenteditable", false); - title.classList.remove('highlight') - qcontainer.classList.add('inactive'); - quill.enable(false); - btn.innerHTML = "Edit" - btn.classList.remove("save"); - document.body.focus(); - editing = false; - } - return false; -} - - function get_login_form() { - var request = new XMLHttpRequest(); - request.open('GET', '/login/', true); - request.onload = function() { - if (request.status >= 200 && request.status < 400) { - } else { - console.log("server error"); - } - }; - request.onerror = function() { - console.log("error on request"); - }; - request.send(); + getJSON('GET', '/login/', function(e){ + console.log(e); + }); } //Global init for Quill |