Add 'dd-sso/' from commit '53cd18d417d39500eef1d9bd4dcf05d226801e6a'
git-subtree-dir: dd-sso git-subtree-mainline:ee0c27c6ab
git-subtree-split:53cd18d417
|
@ -0,0 +1,25 @@
|
|||
.env
|
||||
**/.env
|
||||
main.conf
|
||||
docker-compose.yml
|
||||
#*.yml
|
||||
**/custom.yaml
|
||||
**/system.yaml
|
||||
|
||||
admin/src/node_modules
|
||||
admin/src/admin/node_modules/
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# Python compiled and cached
|
||||
*.pyc
|
||||
**/*.pyc
|
||||
__pycache__
|
||||
**/__pycache__
|
||||
|
||||
# api generated templates
|
||||
docker/api/src/api/static/templates/*.html
|
||||
docker/api/src/api/static/templates/*.json
|
|
@ -0,0 +1,8 @@
|
|||
[settings]
|
||||
multi_line_output = 3
|
||||
include_trailing_comma = True
|
||||
force_grid_wrap = 0
|
||||
use_parentheses = True
|
||||
ensure_newline_before_comments = True
|
||||
line_length = 88
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
# IsardVDI OpenID infrastructure
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [alpha1] - not released
|
||||
|
||||
### Added
|
||||
|
||||
- Auth containers: freeipa, mokey, hydra
|
||||
- Configuration file: main.conf
|
||||
- Build docker-compose yml: build.sh
|
||||
|
||||
### Fixed
|
||||
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
### Removed
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
#!make
|
||||
include main.conf
|
||||
export $(shell sed 's/=.*//' main.conf)
|
||||
|
||||
VERSION := 0.0.1-rc0
|
||||
export VERSION
|
||||
|
||||
BUILD_ROOT_PATH=$(shell pwd)
|
||||
|
||||
.PHONY: environment
|
||||
environment:
|
||||
cp main.conf .env
|
||||
echo "BUILD_ROOT_PATH=$(BUILD_ROOT_PATH)" >> .env
|
||||
|
||||
.PHONY: all
|
||||
all: environment
|
||||
cp .env docker-compose-parts
|
||||
docker-compose -f docker-compose-parts/haproxy.yml \
|
||||
-f docker-compose-parts/api.yml \
|
||||
-f docker-compose-parts/freeipa.yml \
|
||||
-f docker-compose-parts/keycloak.yml \
|
||||
-f docker-compose-parts/avatars.yml \
|
||||
-f docker-compose-parts/postgresql.yml \
|
||||
-f docker-compose-parts/network.yml \
|
||||
config > docker-compose.yml
|
||||
|
||||
.PHONY: up
|
||||
up: all
|
||||
docker-compose up -d
|
||||
|
||||
.PHONY: api
|
||||
api: environment
|
||||
cp .env docker-compose-parts
|
||||
docker-compose -f docker-compose-parts/haproxy.yml \
|
||||
-f docker-compose-parts/api.yml \
|
||||
-f docker-compose-parts/network.yml \
|
||||
config > api.yml
|
||||
|
||||
api-devel: environment
|
||||
cp .env docker-compose-parts
|
||||
docker-compose -f docker-compose-parts/haproxy.yml \
|
||||
-f docker-compose-parts/api.yml \
|
||||
-f docker-compose-parts/api.devel.yml \
|
||||
-f docker-compose-parts/network.yml \
|
||||
config > api.devel.yml
|
|
@ -0,0 +1,168 @@
|
|||
# IsardVDI - OpenID infrastructure
|
||||
|
||||
**NOTE**: This repo now is included in https://gitlab.com/digitaldemocratic/digitaldemocratic that is the repo that includes this repo. Maybe this SSO repo won't work alone by now...
|
||||
|
||||
|
||||
This will bring up a full OpenID auth infrastructure consisting in this hosts from $DOMAIN var set in main.conf:
|
||||
|
||||
- FreeIPA: https://ipa.$DOMAIN
|
||||
- Mokey: https://login.$DOMAIN
|
||||
- Hydra: https://hydra.$DOMAIN
|
||||
|
||||
NOTE: If you use the default example domain in main.conf.example you will need to add this domain mapping to IP in your hosts file at clients.
|
||||
|
||||
# Quick start
|
||||
|
||||
1. Edit main.conf (to suit your needs). If you set Letsencrypt vars it will ask and renew the certificate automatically.
|
||||
2. Build docker.compose.yml: ```./build-compose.sh```
|
||||
3. Bring containers up: ```docker-compose up -d```
|
||||
|
||||
Wait till freeipa container is ready (it can take SEVERAL minutes the first time): ```docker logs freeipa --follow```
|
||||
|
||||
And wait to be ready. It will log: *FreeIPA server configured.*
|
||||
|
||||
4. Add mokey client in freeipa: ```docker exec freeipa /bin/sh -c "scripts/mokey.sh"```
|
||||
5. check that mokey is up: ```docker logs mokey --follow```. It will log: *⇨ http server started on [::]:8080*
|
||||
|
||||
Access your IP/DNS and login page should come up.
|
||||
|
||||
# Firewall
|
||||
|
||||
You should open 80 and 443
|
||||
|
||||
# Add client apps
|
||||
|
||||
## The easy way
|
||||
|
||||
[**Not working yet! Refer to 'Do it yourself'**]
|
||||
|
||||
For most client apps the default script will be enough:
|
||||
|
||||
```
|
||||
docker exec \
|
||||
-e APP_ID=<app name> \
|
||||
-e APP_SECRET=<app secret> \
|
||||
-e APP_CALLBACKS=<app callback url wiht https://...> \
|
||||
hydra /bin/sh -c "scripts/add_app.sh"
|
||||
```
|
||||
|
||||
## Do it yourself
|
||||
For example we will be creating a moodle app client. You need to adapt the vars to your app
|
||||
|
||||
```
|
||||
DOMAIN=(your domain root as set in main.conf)
|
||||
APP_ID=moodle
|
||||
APP_SECRET=Sup3rS3cr3t
|
||||
```
|
||||
```
|
||||
docker-compose exec hydra \
|
||||
hydra clients create \
|
||||
--endpoint http://hydra:4445/ \
|
||||
--id $APP_ID \
|
||||
--secret $APP_SECRET \
|
||||
--grant-types client_credentials,authorization_code,refresh_token \
|
||||
--token-endpoint-auth-m01d4f1c0-8a53-4df5-8a46-de670f42a4dfethod client_secret_post \
|
||||
--response-types code \
|
||||
--scope openid,offline,profile,email \
|
||||
--callbacks https://moodle.${DOMAIN}/auth/oidc/
|
||||
```
|
||||
|
||||
And then validate the app:
|
||||
```
|
||||
docker-compose exec hydra \
|
||||
hydra token client \
|
||||
--endpoint http://hydra:4444/ \
|
||||
--client-id $APP_ID \
|
||||
--client-secret $APP_SECRET
|
||||
```
|
||||
|
||||
## Example configuration for Moodle OpenID Connect
|
||||
|
||||
- authendpoint: https://hydra.${DOMAIN}/oauth2/auth
|
||||
- tokenendpoint: https://hydra.${DOMAIN}/oauth2/token
|
||||
- oidcresource: https://hydra.${DOMAIN}/userinfo
|
||||
- oidcscope: openid profile email
|
||||
- single_sign_off: [checked]
|
||||
- logouturi: https://login.${DOMAIN}/auth/logout
|
||||
|
||||
## Example configuration for Nextcloud Custom OpenID Connect
|
||||
- Prevent creating an account if the email address exists
|
||||
- Update user profile every login
|
||||
- Do not prune not available user groups on login
|
||||
- automatically create groups if they do not exist
|
||||
|
||||
- Authorize url: https://hydra.${DOMAIN}/oauth2/auth
|
||||
- Token url: https://hydra.${DOMAIN}/oauth2/token
|
||||
- User info URL (optional): https://hydra.${DOMAIN}/userinfo
|
||||
- Scope: openid profile offline email
|
||||
- Logout URL (optional): https://login.${DOMAIN}/auth/logout
|
||||
|
||||
### Be aware on nextcloud behind proxy
|
||||
Behind proxy we should force nextcloud-app to use https. From documentation it should be NEXTCLOUD_OVERWRITEPROTOCOL but it is not:
|
||||
- https://docs.nextcloud.com/server/latest/admin_manual/configuration_server/reverse_proxy_configuration.html
|
||||
|
||||
Instead we used in docker/nextcloud/nextcloud.yml compose file the envvar NC_overwriteprotocol as stated in this thread:
|
||||
- https://github.com/nextcl01d4f1c0-8a53-4df5-8a46-de670f42a4dfoud/docker/pull/819
|
||||
|
||||
Extra information about this:
|
||||
Some users may get strange reply(Callback) url error from provider even if you pasted the right url, that's because your nextcloud
|
||||
server may generate http urls when you are actually using https. Please set 'overwriteprotocol' => 'https', in your config.php file.
|
||||
|
||||
<?php
|
||||
$CONFIG = array (
|
||||
'overwriteprotocol' => 'https',
|
||||
'memcache.local' => '\\OC\\Memcache\\APCu',
|
||||
'apps_paths' =>
|
||||
|
||||
## Nexcloud autoredirect for unauthorized users
|
||||
Set social_login_auto_redirect and social_login_http_client in config.php
|
||||
- https://apps.nextcloud.com/apps/sociallogin
|
||||
|
||||
### APPENDIX: Nextcloud Social Login plugin
|
||||
|
||||
|
||||
Config
|
||||
|
||||
You can use 'social_login_auto_redirect' => true setting in config.php for auto redirect unauthorized users to social login if only one
|
||||
provider is configured. If you want to temporary disable this function (e.g. for login as local admin), you can add noredir=1 query
|
||||
parameter in url for login page. Something like https://cloud.domain.com/login?noredir=1
|
||||
|
||||
To set timeout for http client, you can use
|
||||
|
||||
'social_login_http_client' => [
|
||||
'timeout' => 45,
|
||||
],
|
||||
|
||||
# IsardVDI office apps
|
||||
|
||||
Refer to https://gitlab.com/isard/isard-office repository for sample moodle, nextcloud, jitsi (and more) apps
|
||||
|
||||
# Troubleshooting
|
||||
|
||||
## FreeIPA
|
||||
|
||||
ldapsearch -x -b "dc=domain,dc=org" -H ldap://ipa.domain.org
|
||||
ldapsearch -x -b "dc=domain,dc=org" -H ldap://ipa.domain.org -D "uid=admin,cn=users,cn=compat,dc=domain,dc=org" -W
|
||||
|
||||
|
||||
# KEYCLOACK
|
||||
|
||||
Here’s a list of OIDC endpoints that the Keycloak publishes. These URLs are useful if you are using a non-Keycloak client adapter to talk OIDC with the auth server. These are all relative URLs and the root of the URL being the HTTP(S) protocol, hostname, and usually path prefixed with /auth: i.e. https://localhost:8080/auth
|
||||
|
||||
/realms/{realm-name}/protocol/isard-sso-connect/token
|
||||
|
||||
This is the URL endpoint for obtaining a temporary code in the Authorization Code Flow or for obtaining tokens via the Implicit Flow, Direct Grants, or Client Grants.
|
||||
/realms/{realm-name}/protocol/isard-sso-connect/auth
|
||||
|
||||
This is the URL endpoint for the Authorization Code Flow to turn a temporary code into a token.
|
||||
/realms/{realm-name}/protocol/isard-sso-connect/logout
|
||||
|
||||
This is the URL endpoint for performing logouts.
|
||||
/realms/{realm-name}/protocol/isard-sso-connect/userinfo
|
||||
|
||||
This is the URL endpoint for the User Info service described in the OIDC specification.
|
||||
|
||||
In all of these replace {realm-name} with the name of the realm.
|
||||
|
||||
|
||||
http://login.mydomain.duckns.org/auth/realms/master/protocol/isard-sso-connect/logout
|
|
@ -0,0 +1,44 @@
|
|||
FROM alpine:3.12.0 as production
|
||||
MAINTAINER isard <info@isardvdi.com>
|
||||
|
||||
RUN apk add python3 py3-pip py3-pyldap~=3.2.0
|
||||
RUN pip3 install --upgrade pip
|
||||
RUN apk add --no-cache --virtual .build_deps \
|
||||
build-base \
|
||||
python3-dev \
|
||||
libffi-dev \
|
||||
gcc python3-dev linux-headers musl-dev postgresql-dev
|
||||
COPY admin/docker/requirements.pip3 /requirements.pip3
|
||||
RUN pip3 install --no-cache-dir -r requirements.pip3
|
||||
RUN apk del .build_deps
|
||||
|
||||
RUN apk add --no-cache curl py3-yaml yarn libpq openssl py3-pillow
|
||||
|
||||
RUN wget -O /usr/lib/python3.8/site-packages/diceware/wordlists/wordlist_cat_ascii.txt https://raw.githubusercontent.com/1ma/diceware-cat/master/cat-wordlist-ascii.txt
|
||||
|
||||
# SSH configuration
|
||||
# ARG SSH_ROOT_PWD
|
||||
# RUN apk add openssh
|
||||
# RUN echo "root:$SSH_ROOT_PWD" |chpasswd
|
||||
# RUN sed -i \
|
||||
# -e 's|[#]*PermitRootLogin prohibit-password|PermitRootLogin yes|g' \
|
||||
# -e 's|[#]*PasswordAuthentication yes|PasswordAuthentication yes|g' \
|
||||
# -e 's|[#]*ChallengeResponseAuthentication yes|ChallengeResponseAuthentication yes|g' \
|
||||
# -e 's|[#]*UsePAM yes|UsePAM yes|g' \
|
||||
# -e 's|[#]#Port 22|Port 22|g' \
|
||||
# /etc/ssh/sshd_config
|
||||
|
||||
# Let's test 0.26.1 python-keycloak version
|
||||
# RUN apk add --no-cache git && \
|
||||
# git clone -b delete_realm_roles https://github.com/isard-vdi/python-keycloak.git && \
|
||||
# cd python-keycloak && \
|
||||
# python3 setup.py install && \
|
||||
# apk del git
|
||||
|
||||
COPY admin/src /admin
|
||||
RUN cd /admin/admin && yarn install
|
||||
|
||||
COPY admin/docker/run.sh /run.sh
|
||||
|
||||
#EXPOSE 7039
|
||||
CMD [ "/run.sh" ]
|
|
@ -0,0 +1,16 @@
|
|||
Flask==2.0.1
|
||||
Flask-Login==0.5.0
|
||||
eventlet==0.33.0
|
||||
Flask-SocketIO==5.1.0
|
||||
bcrypt==3.2.0
|
||||
diceware==0.9.6
|
||||
mysql-connector-python==8.0.25
|
||||
psycopg2==2.8.6
|
||||
python-keycloak==0.26.1
|
||||
minio==7.0.3
|
||||
urllib3==1.26.6
|
||||
schema==0.7.5
|
||||
Werkzeug~=2.0.0
|
||||
python-jose==3.3.0
|
||||
Cerberus==1.3.4
|
||||
PyYAML==6.0
|
|
@ -0,0 +1,11 @@
|
|||
#!/bin/sh
|
||||
# ssh-keygen -A
|
||||
## Only in development
|
||||
cd /admin/admin
|
||||
yarn install
|
||||
## End Only in development
|
||||
cd /admin
|
||||
export PYTHONWARNINGS="ignore:Unverified HTTPS request"
|
||||
python3 start.py
|
||||
#&
|
||||
# /usr/sbin/sshd -D -e -f /etc/ssh/sshd_config
|
|
@ -0,0 +1,111 @@
|
|||
#!flask/bin/python
|
||||
# coding=utf-8
|
||||
|
||||
import logging as log
|
||||
import os
|
||||
|
||||
from flask import Flask, render_template, send_from_directory
|
||||
|
||||
app = Flask(__name__, static_url_path="")
|
||||
app = Flask(__name__, template_folder="static/templates")
|
||||
app.url_map.strict_slashes = False
|
||||
|
||||
"""
|
||||
App secret key for encrypting cookies
|
||||
You can generate one with:
|
||||
import os
|
||||
os.urandom(24)
|
||||
And paste it here.
|
||||
"""
|
||||
app.secret_key = "Change this key!/\xf7\x83\xbe\x17\xfa\xa3zT\n\\]m\xa6\x8bF\xdd\r\xf7\x9e\x1d\x1f\x14'"
|
||||
|
||||
print("Starting isard-sso api...")
|
||||
|
||||
from admin.lib.load_config import loadConfig
|
||||
|
||||
try:
|
||||
loadConfig(app)
|
||||
except:
|
||||
print("Could not get environment variables...")
|
||||
|
||||
from admin.lib.postup import Postup
|
||||
|
||||
Postup()
|
||||
|
||||
from admin.lib.admin import Admin
|
||||
|
||||
app.admin = Admin()
|
||||
|
||||
app.ready = False
|
||||
|
||||
"""
|
||||
Debug should be removed on production!
|
||||
"""
|
||||
if app.debug:
|
||||
log.warning("Debug mode: {}".format(app.debug))
|
||||
else:
|
||||
log.info("Debug mode: {}".format(app.debug))
|
||||
|
||||
"""
|
||||
Serve static files
|
||||
"""
|
||||
|
||||
|
||||
@app.route("/build/<path:path>")
|
||||
def send_build(path):
|
||||
return send_from_directory(
|
||||
os.path.join(app.root_path, "node_modules/gentelella/build"), path
|
||||
)
|
||||
|
||||
|
||||
@app.route("/vendors/<path:path>")
|
||||
def send_vendors(path):
|
||||
return send_from_directory(
|
||||
os.path.join(app.root_path, "node_modules/gentelella/vendors"), path
|
||||
)
|
||||
|
||||
|
||||
@app.route("/node_modules/<path:path>")
|
||||
def send_nodes(path):
|
||||
return send_from_directory(os.path.join(app.root_path, "node_modules"), path)
|
||||
|
||||
|
||||
@app.route("/templates/<path:path>")
|
||||
def send_templates(path):
|
||||
return send_from_directory(os.path.join(app.root_path, "templates"), path)
|
||||
|
||||
|
||||
# @app.route('/templates/<path:path>')
|
||||
# def send_templates(path):
|
||||
# return send_from_directory(os.path.join(app.root_path, 'static/templates'), path)
|
||||
|
||||
|
||||
@app.route("/static/<path:path>")
|
||||
def send_static_js(path):
|
||||
return send_from_directory(os.path.join(app.root_path, "static"), path)
|
||||
|
||||
|
||||
@app.route("/avatars/<path:path>")
|
||||
def send_avatars_img(path):
|
||||
return send_from_directory(
|
||||
os.path.join(app.root_path, "../avatars/master-avatars"), path
|
||||
)
|
||||
|
||||
|
||||
@app.route("/custom/<path:path>")
|
||||
def send_custom(path):
|
||||
return send_from_directory(os.path.join(app.root_path, "../custom"), path)
|
||||
|
||||
|
||||
# @app.errorhandler(404)
|
||||
# def not_found_error(error):
|
||||
# return render_template('page_404.html'), 404
|
||||
|
||||
# @app.errorhandler(500)
|
||||
# def internal_error(error):
|
||||
# return render_template('page_500.html'), 500
|
||||
|
||||
"""
|
||||
Import all views
|
||||
"""
|
||||
from .views import ApiViews, AppViews, LoginViews, WebViews, WpViews
|
|
@ -0,0 +1,60 @@
|
|||
import os
|
||||
|
||||
from flask_login import LoginManager, UserMixin
|
||||
|
||||
from admin import app
|
||||
|
||||
""" OIDC TESTS """
|
||||
# from flask_oidc import OpenIDConnect
|
||||
# app.config.update({
|
||||
# 'SECRET_KEY': 'u\x91\xcf\xfa\x0c\xb9\x95\xe3t\xba2K\x7f\xfd\xca\xa3\x9f\x90\x88\xb8\xee\xa4\xd6\xe4',
|
||||
# 'TESTING': True,
|
||||
# 'DEBUG': True,
|
||||
# 'OIDC_CLIENT_SECRETS': 'client_secrets.json',
|
||||
# 'OIDC_ID_TOKEN_COOKIE_SECURE': False,
|
||||
# 'OIDC_REQUIRE_VERIFIED_EMAIL': False,
|
||||
# 'OIDC_VALID_ISSUERS': ['https://sso.mydomain.duckdns.org:8080/auth/realms/master'],
|
||||
# 'OIDC_OPENID_REALM': 'https://sso.mydomain.duckdns.org//custom_callback',
|
||||
# 'OVERWRITE_REDIRECT_URI': 'https://sso.mydomain.duckdns.org//custom_callback',
|
||||
# })
|
||||
# # 'OVERWRITE_REDIRECT_URI': 'https://sso.mydomain.duckdns.org//custom_callback',
|
||||
# # 'OIDC_CALLBACK_ROUTE': '//custom_callback'
|
||||
# oidc = OpenIDConnect(app)
|
||||
""" OIDC TESTS """
|
||||
|
||||
|
||||
login_manager = LoginManager()
|
||||
login_manager.init_app(app)
|
||||
login_manager.login_view = "login"
|
||||
|
||||
|
||||
ram_users = {
|
||||
os.environ["ADMINAPP_USER"]: {
|
||||
"id": os.environ["ADMINAPP_USER"],
|
||||
"password": os.environ["ADMINAPP_PASSWORD"],
|
||||
"role": "manager",
|
||||
},
|
||||
os.environ["KEYCLOAK_USER"]: {
|
||||
"id": os.environ["KEYCLOAK_USER"],
|
||||
"password": os.environ["KEYCLOAK_PASSWORD"],
|
||||
"role": "admin",
|
||||
},
|
||||
os.environ["WORDPRESS_MARIADB_USER"]: {
|
||||
"id": os.environ["WORDPRESS_MARIADB_USER"],
|
||||
"password": os.environ["WORDPRESS_MARIADB_PASSWORD"],
|
||||
"role": "manager",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class User(UserMixin):
|
||||
def __init__(self, dict):
|
||||
self.id = dict["id"]
|
||||
self.username = dict["id"]
|
||||
self.password = dict["password"]
|
||||
self.role = dict["role"]
|
||||
|
||||
|
||||
@login_manager.user_loader
|
||||
def user_loader(username):
|
||||
return User(ram_users[username])
|
|
@ -0,0 +1,99 @@
|
|||
# Copyright 2017 the Isard-vdi project authors:
|
||||
# Josep Maria Viñolas Auquer
|
||||
# Alberto Larraz Dalmases
|
||||
# License: AGPLv3
|
||||
|
||||
import json
|
||||
import logging as log
|
||||
import os
|
||||
import traceback
|
||||
from functools import wraps
|
||||
|
||||
from flask import request
|
||||
from jose import jwt
|
||||
|
||||
from admin import app
|
||||
|
||||
from ..lib.api_exceptions import Error
|
||||
|
||||
|
||||
def get_header_jwt_payload():
|
||||
return get_token_payload(get_token_auth_header())
|
||||
|
||||
|
||||
def get_token_header(header):
|
||||
"""Obtains the Access Token from the a Header"""
|
||||
auth = request.headers.get(header, None)
|
||||
if not auth:
|
||||
raise Error(
|
||||
"unauthorized",
|
||||
"Authorization header is expected",
|
||||
traceback.format_stack(),
|
||||
)
|
||||
|
||||
parts = auth.split()
|
||||
if parts[0].lower() != "bearer":
|
||||
raise Error(
|
||||
"unauthorized",
|
||||
"Authorization header must start with Bearer",
|
||||
traceback.format_stack(),
|
||||
)
|
||||
|
||||
elif len(parts) == 1:
|
||||
raise Error("bad_request", "Token not found")
|
||||
elif len(parts) > 2:
|
||||
raise Error(
|
||||
"unauthorized",
|
||||
"Authorization header must be Bearer token",
|
||||
traceback.format_stack(),
|
||||
)
|
||||
|
||||
return parts[1] # Token
|
||||
|
||||
|
||||
def get_token_auth_header():
|
||||
return get_token_header("Authorization")
|
||||
|
||||
|
||||
def get_token_payload(token):
|
||||
# log.warning("The received token in get_token_payload is: " + str(token))
|
||||
try:
|
||||
claims = jwt.get_unverified_claims(token)
|
||||
secret = app.config["API_SECRET"]
|
||||
|
||||
except:
|
||||
log.warning(
|
||||
"JWT token with invalid parameters. Can not parse it.: " + str(token)
|
||||
)
|
||||
raise Error(
|
||||
"unauthorized",
|
||||
"Unable to parse authentication parameters token.",
|
||||
traceback.format_stack(),
|
||||
)
|
||||
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
secret,
|
||||
algorithms=["HS256"],
|
||||
options=dict(verify_aud=False, verify_sub=False, verify_exp=True),
|
||||
)
|
||||
except jwt.ExpiredSignatureError:
|
||||
log.warning("Token expired")
|
||||
raise Error("unauthorized", "Token is expired", traceback.format_stack())
|
||||
|
||||
except jwt.JWTClaimsError:
|
||||
raise Error(
|
||||
"unauthorized",
|
||||
"Incorrect claims, please check the audience and issuer",
|
||||
traceback.format_stack(),
|
||||
)
|
||||
except Exception:
|
||||
raise Error(
|
||||
"unauthorized",
|
||||
"Unable to parse authentication token.",
|
||||
traceback.format_stack(),
|
||||
)
|
||||
if payload.get("data", False):
|
||||
return payload["data"]
|
||||
return payload
|
|
@ -0,0 +1,143 @@
|
|||
import inspect
|
||||
import json
|
||||
import logging as log
|
||||
import os
|
||||
import traceback
|
||||
|
||||
from flask import jsonify, request
|
||||
|
||||
from admin import app
|
||||
|
||||
content_type = {"Content-Type": "application/json"}
|
||||
ex = {
|
||||
"bad_request": {
|
||||
"error": {
|
||||
"error": "bad_request",
|
||||
"msg": "Bad request",
|
||||
},
|
||||
"status_code": 400,
|
||||
},
|
||||
"unauthorized": {
|
||||
"error": {
|
||||
"error": "unauthorized",
|
||||
"msg": "Unauthorized",
|
||||
},
|
||||
"status_code": 401,
|
||||
},
|
||||
"forbidden": {
|
||||
"error": {
|
||||
"error": "forbidden",
|
||||
"msg": "Forbidden",
|
||||
},
|
||||
"status_code": 403,
|
||||
},
|
||||
"not_found": {
|
||||
"error": {
|
||||
"error": "not_found",
|
||||
"msg": "Not found",
|
||||
},
|
||||
"status_code": 404,
|
||||
},
|
||||
"conflict": {
|
||||
"error": {
|
||||
"error": "conflict",
|
||||
"msg": "Conflict",
|
||||
},
|
||||
"status_code": 409,
|
||||
},
|
||||
"internal_server": {
|
||||
"error": {
|
||||
"error": "internal_server",
|
||||
"msg": "Internal server error",
|
||||
},
|
||||
"status_code": 500,
|
||||
},
|
||||
"gateway_timeout": {
|
||||
"error": {
|
||||
"error": "gateway_timeout",
|
||||
"msg": "Gateway timeout",
|
||||
},
|
||||
"status_code": 504,
|
||||
},
|
||||
"precondition_required": {
|
||||
"error": {
|
||||
"error": "precondition_required",
|
||||
"msg": "Precondition required",
|
||||
},
|
||||
"status_code": 428,
|
||||
},
|
||||
"insufficient_storage": {
|
||||
"error": {
|
||||
"error": "insufficient_storage",
|
||||
"msg": "Insufficient storage",
|
||||
},
|
||||
"status_code": 507,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
def __init__(self, error="bad_request", description="", debug="", data=None):
|
||||
self.error = ex[error]["error"].copy()
|
||||
self.error["function"] = (
|
||||
inspect.stack()[1][1].split(os.sep)[-1]
|
||||
+ ":"
|
||||
+ str(inspect.stack()[1][2])
|
||||
+ ":"
|
||||
+ inspect.stack()[1][3]
|
||||
)
|
||||
self.error["function_call"] = (
|
||||
inspect.stack()[2][1].split(os.sep)[-1]
|
||||
+ ":"
|
||||
+ str(inspect.stack()[2][2])
|
||||
+ ":"
|
||||
+ inspect.stack()[2][3]
|
||||
)
|
||||
self.error["description"] = str(description)
|
||||
self.error["debug"] = "{}\n\r{}{}".format(
|
||||
"----------- DEBUG START -------------",
|
||||
debug,
|
||||
"----------- DEBUG STOP -------------",
|
||||
)
|
||||
self.error["request"] = (
|
||||
"{}\n{}\r\n{}\r\n\r\n{}{}".format(
|
||||
"----------- REQUEST START -----------",
|
||||
request.method + " " + request.url,
|
||||
"\r\n".join("{}: {}".format(k, v) for k, v in request.headers.items()),
|
||||
request.body if hasattr(request, "body") else "",
|
||||
"----------- REQUEST STOP -----------",
|
||||
)
|
||||
if request
|
||||
else ""
|
||||
)
|
||||
self.error["data"] = (
|
||||
"{}\n{}\n{}".format(
|
||||
"----------- DATA START -----------",
|
||||
json.dumps(data, indent=2),
|
||||
"----------- DATA STOP -----------",
|
||||
)
|
||||
if data
|
||||
else ""
|
||||
)
|
||||
self.status_code = ex[error]["status_code"]
|
||||
self.content_type = content_type
|
||||
log.debug(
|
||||
"%s - %s - [%s -> %s]\r\n%s\r\n%s\r\n%s"
|
||||
% (
|
||||
error,
|
||||
str(description),
|
||||
self.error["function_call"],
|
||||
self.error["function"],
|
||||
self.error["debug"],
|
||||
self.error["request"],
|
||||
self.error["data"],
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@app.errorhandler(Error)
|
||||
def handle_user_error(ex):
|
||||
response = jsonify(ex.error)
|
||||
response.status_code = ex.status_code
|
||||
response.headers = {"content-type": content_type}
|
||||
return response
|
|
@ -0,0 +1,82 @@
|
|||
import logging as log
|
||||
import os
|
||||
from pprint import pprint
|
||||
|
||||
from minio import Minio
|
||||
from minio.commonconfig import REPLACE, CopySource
|
||||
from minio.deleteobjects import DeleteObject
|
||||
from requests import get, post
|
||||
|
||||
from admin import app
|
||||
|
||||
|
||||
class Avatars:
|
||||
def __init__(self):
|
||||
self.mclient = Minio(
|
||||
"isard-sso-avatars:9000",
|
||||
access_key="AKIAIOSFODNN7EXAMPLE",
|
||||
secret_key="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
|
||||
secure=False,
|
||||
)
|
||||
self.bucket = "master-avatars"
|
||||
self._minio_set_realm()
|
||||
# self.update_missing_avatars()
|
||||
|
||||
def add_user_default_avatar(self, userid, role="unknown"):
|
||||
self.mclient.fput_object(
|
||||
self.bucket,
|
||||
userid,
|
||||
os.path.join(app.root_path, "../custom/avatars/" + role + ".jpg"),
|
||||
content_type="image/jpeg ",
|
||||
)
|
||||
log.warning(
|
||||
" AVATARS: Updated avatar for user " + userid + " with role " + role
|
||||
)
|
||||
|
||||
def delete_user_avatar(self, userid):
|
||||
self.minio_delete_object(userid)
|
||||
|
||||
def update_missing_avatars(self, users):
|
||||
sys_roles = ["admin", "manager", "teacher", "student"]
|
||||
for u in self.get_users_without_image(users):
|
||||
try:
|
||||
img = [r + ".jpg" for r in sys_roles if r in u["roles"]][0]
|
||||
except:
|
||||
img = "unknown.jpg"
|
||||
|
||||
self.mclient.fput_object(
|
||||
self.bucket,
|
||||
u["id"],
|
||||
os.path.join(app.root_path, "../custom/avatars/" + img),
|
||||
content_type="image/jpeg ",
|
||||
)
|
||||
log.warning(
|
||||
" AVATARS: Updated avatar for user "
|
||||
+ u["username"]
|
||||
+ " with role "
|
||||
+ img.split(".")[0]
|
||||
)
|
||||
|
||||
def _minio_set_realm(self):
|
||||
if not self.mclient.bucket_exists(self.bucket):
|
||||
self.mclient.make_bucket(self.bucket)
|
||||
|
||||
def minio_get_objects(self):
|
||||
return [o.object_name for o in self.mclient.list_objects(self.bucket)]
|
||||
|
||||
def minio_delete_all_objects(self):
|
||||
delete_object_list = map(
|
||||
lambda x: DeleteObject(x.object_name),
|
||||
self.mclient.list_objects(self.bucket),
|
||||
)
|
||||
errors = self.mclient.remove_objects(self.bucket, delete_object_list)
|
||||
for error in errors:
|
||||
log.error(" AVATARS: Error occured when deleting avatar object: " + error)
|
||||
|
||||
def minio_delete_object(self, oid):
|
||||
errors = self.mclient.remove_objects(self.bucket, [DeleteObject(oid)])
|
||||
for error in errors:
|
||||
log.error(" AVATARS: Error occured when deleting avatar object: " + error)
|
||||
|
||||
def get_users_without_image(self, users):
|
||||
return [u for u in users if u["id"] and u["id"] not in self.minio_get_objects()]
|
|
@ -0,0 +1,87 @@
|
|||
import logging as log
|
||||
import os
|
||||
import shutil
|
||||
import traceback
|
||||
from io import BytesIO
|
||||
from pprint import pprint
|
||||
|
||||
import requests
|
||||
import yaml
|
||||
from PIL import Image
|
||||
from schema import And, Optional, Schema, SchemaError, Use
|
||||
|
||||
from admin import app
|
||||
|
||||
|
||||
class Dashboard:
|
||||
def __init__(
|
||||
self,
|
||||
):
|
||||
self.custom_menu = os.path.join(app.root_path, "../custom/menu/custom.yaml")
|
||||
|
||||
def _update_custom_menu(self, custom_menu_part):
|
||||
with open(self.custom_menu) as yml:
|
||||
menu = yaml.load(yml, Loader=yaml.FullLoader)
|
||||
menu = {**menu, **custom_menu_part}
|
||||
with open(self.custom_menu, "w") as yml:
|
||||
yml.write(yaml.dump(menu, default_flow_style=False))
|
||||
return True
|
||||
|
||||
def update_colours(self, colours):
|
||||
schema_template = Schema(
|
||||
{
|
||||
"background": And(Use(str)),
|
||||
"primary": And(Use(str)),
|
||||
"secondary": And(Use(str)),
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
schema_template.validate(colours)
|
||||
except SchemaError:
|
||||
return False
|
||||
|
||||
self._update_custom_menu({"colours": colours})
|
||||
return self.apply_updates()
|
||||
|
||||
def update_menu(self, menu):
|
||||
items = []
|
||||
for menu_item in menu.keys():
|
||||
for mustexist_key in ["href", "icon", "name", "shortname"]:
|
||||
if mustexist_key not in menu[menu_item].keys():
|
||||
return False
|
||||
items.append(menu[menu_item])
|
||||
self._update_custom_menu({"apps_external": items})
|
||||
return self.apply_updates()
|
||||
|
||||
def update_logo(self, logo):
|
||||
img = Image.open(logo.stream)
|
||||
img.save(os.path.join(app.root_path, "../custom/img/logo.png"))
|
||||
img.save(
|
||||
os.path.join(
|
||||
app.root_path,
|
||||
"../custom/system/keycloak/themes/liiibrelite/login/resources/img/logo.png",
|
||||
)
|
||||
)
|
||||
return self.apply_updates()
|
||||
|
||||
def update_background(self, background):
|
||||
img = Image.open(background.stream)
|
||||
img.save(os.path.join(app.root_path, "../custom/img/background.png"))
|
||||
img.save(
|
||||
os.path.join(
|
||||
app.root_path,
|
||||
"../custom/system/keycloak/themes/liiibrelite/login/resources/img/loginBG.png",
|
||||
)
|
||||
)
|
||||
img.save(
|
||||
os.path.join(
|
||||
app.root_path,
|
||||
"../custom/system/keycloak/themes/liiibrelite/login/resources/img/loginBG2.png",
|
||||
)
|
||||
)
|
||||
return self.apply_updates()
|
||||
|
||||
def apply_updates(self):
|
||||
resp = requests.get("http://isard-sso-api:7039/restart")
|
||||
return True
|
|
@ -0,0 +1,176 @@
|
|||
#!flask/bin/python
|
||||
# coding=utf-8
|
||||
import base64
|
||||
import json
|
||||
import logging as log
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
from time import sleep
|
||||
from uuid import uuid4
|
||||
|
||||
from flask import Response, jsonify, redirect, render_template, request, url_for
|
||||
from flask_socketio import (
|
||||
SocketIO,
|
||||
close_room,
|
||||
disconnect,
|
||||
emit,
|
||||
join_room,
|
||||
leave_room,
|
||||
rooms,
|
||||
send,
|
||||
)
|
||||
|
||||
from admin import app
|
||||
|
||||
|
||||
def sio_event_send(event, data):
|
||||
app.socketio.emit(
|
||||
event,
|
||||
json.dumps(data),
|
||||
namespace="/sio/events",
|
||||
room="events",
|
||||
)
|
||||
sleep(0.001)
|
||||
|
||||
|
||||
class Events:
|
||||
def __init__(self, title, text="", total=0, table=False, type="info"):
|
||||
# notice, info, success, and error
|
||||
self.eid = str(base64.b64encode(os.urandom(32))[:8])
|
||||
self.title = title
|
||||
self.text = text
|
||||
self.total = total
|
||||
self.table = table
|
||||
self.item = 0
|
||||
self.type = type
|
||||
self.create()
|
||||
|
||||
def create(self):
|
||||
log.info("START " + self.eid + ": " + self.text)
|
||||
app.socketio.emit(
|
||||
"notify-create",
|
||||
json.dumps(
|
||||
{
|
||||
"id": self.eid,
|
||||
"title": self.title,
|
||||
"text": self.text,
|
||||
"type": self.type,
|
||||
}
|
||||
),
|
||||
namespace="/sio",
|
||||
room="admin",
|
||||
)
|
||||
sleep(0.001)
|
||||
|
||||
def __del__(self):
|
||||
log.info("END " + self.eid + ": " + self.text)
|
||||
app.socketio.emit(
|
||||
"notify-destroy",
|
||||
json.dumps({"id": self.eid}),
|
||||
namespace="/sio",
|
||||
room="admin",
|
||||
)
|
||||
sleep(0.001)
|
||||
|
||||
def update_text(self, text):
|
||||
self.text = text
|
||||
app.socketio.emit(
|
||||
"notify-update",
|
||||
json.dumps(
|
||||
{
|
||||
"id": self.eid,
|
||||
"text": self.text,
|
||||
}
|
||||
),
|
||||
namespace="/sio",
|
||||
room="admin",
|
||||
)
|
||||
sleep(0.001)
|
||||
|
||||
def append_text(self, text):
|
||||
self.text = self.text + "<br>" + text
|
||||
app.socketio.emit(
|
||||
"notify-update",
|
||||
json.dumps(
|
||||
{
|
||||
"id": self.eid,
|
||||
"text": self.text,
|
||||
}
|
||||
),
|
||||
namespace="/sio",
|
||||
room="admin",
|
||||
)
|
||||
sleep(0.001)
|
||||
|
||||
def increment(self, data={"name": "", "data": []}):
|
||||
self.item += 1
|
||||
log.info("INCREMENT " + self.eid + ": " + self.text)
|
||||
app.socketio.emit(
|
||||
"notify-increment",
|
||||
json.dumps(
|
||||
{
|
||||
"id": self.eid,
|
||||
"title": self.title,
|
||||
"text": "["
|
||||
+ str(self.item)
|
||||
+ "/"
|
||||
+ str(self.total)
|
||||
+ "] "
|
||||
+ self.text
|
||||
+ " "
|
||||
+ data["name"],
|
||||
"item": self.item,
|
||||
"total": self.total,
|
||||
"table": self.table,
|
||||
"type": self.type,
|
||||
"data": data,
|
||||
}
|
||||
),
|
||||
namespace="/sio",
|
||||
room="admin",
|
||||
)
|
||||
sleep(0.0001)
|
||||
|
||||
def decrement(self, data={"name": "", "data": []}):
|
||||
self.item -= 1
|
||||
log.info("DECREMENT " + self.eid + ": " + self.text)
|
||||
app.socketio.emit(
|
||||
"notify-decrement",
|
||||
json.dumps(
|
||||
{
|
||||
"id": self.eid,
|
||||
"title": self.title,
|
||||
"text": "["
|
||||
+ str(self.item)
|
||||
+ "/"
|
||||
+ str(self.total)
|
||||
+ "] "
|
||||
+ self.text
|
||||
+ " "
|
||||
+ data["name"],
|
||||
"item": self.item,
|
||||
"total": self.total,
|
||||
"table": self.table,
|
||||
"type": self.type,
|
||||
"data": data,
|
||||
}
|
||||
),
|
||||
namespace="/sio",
|
||||
room="admin",
|
||||
)
|
||||
sleep(0.001)
|
||||
|
||||
def reload(self):
|
||||
app.socketio.emit("reload", json.dumps({}), namespace="/sio", room="admin")
|
||||
sleep(0.0001)
|
||||
|
||||
def table(self, event, table, data={}):
|
||||
# refresh, add, delete, update
|
||||
app.socketio.emit(
|
||||
"table_" + event,
|
||||
json.dumps({"table": table, "data": data}),
|
||||
namespace="/sio",
|
||||
room="admin",
|
||||
)
|
||||
sleep(0.0001)
|
|
@ -0,0 +1,8 @@
|
|||
#!/usr/bin/env python
|
||||
# coding=utf-8
|
||||
class UserExists(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class UserNotFound(Exception):
|
||||
pass
|
|
@ -0,0 +1,115 @@
|
|||
import random
|
||||
import string
|
||||
from collections import Counter
|
||||
from pprint import pprint
|
||||
|
||||
|
||||
def get_recursive_groups(l_groups, l):
|
||||
for d_group in l_groups:
|
||||
data = {}
|
||||
for key, value in d_group.items():
|
||||
if key == "subGroups":
|
||||
get_recursive_groups(value, l)
|
||||
else:
|
||||
data[key] = value
|
||||
l.append(data)
|
||||
return l
|
||||
|
||||
|
||||
def get_group_with_childs(keycloak_group):
|
||||
return [g["path"] for g in get_recursive_groups([keycloak_group], [])]
|
||||
|
||||
|
||||
def system_username(username):
|
||||
return (
|
||||
True
|
||||
if username in ["guest", "ddadmin", "admin"] or username.startswith("system_")
|
||||
else False
|
||||
)
|
||||
|
||||
|
||||
def system_group(groupname):
|
||||
return True if groupname in ["admin", "manager", "teacher", "student"] else False
|
||||
|
||||
|
||||
def get_group_from_group_id(group_id, groups):
|
||||
return next((d for d in groups if d.get("id") == group_id), None)
|
||||
|
||||
|
||||
def get_kid_from_kpath(kpath, groups):
|
||||
ids = [g["id"] for g in groups if g["path"] == kpath]
|
||||
if not len(ids) or len(ids) > 1:
|
||||
return False
|
||||
return ids[0]
|
||||
|
||||
|
||||
def get_gid_from_kgroup_id(kgroup_id, groups):
|
||||
return [
|
||||
g["path"].replace("/", ".")[1:] if len(g["path"].split("/")) else g["path"][1:]
|
||||
for g in groups
|
||||
if g["id"] == kgroup_id
|
||||
][0]
|
||||
|
||||
|
||||
def get_gids_from_kgroup_ids(kgroup_ids, groups):
|
||||
return [get_gid_from_kgroup_id(kgroup_id, groups) for kgroup_id in kgroup_ids]
|
||||
|
||||
|
||||
def kpath2gid(path):
|
||||
# print(path.replace('/','.')[1:])
|
||||
if path.startswith("/"):
|
||||
return path.replace("/", ".")[1:]
|
||||
return path.replace("/", ".")
|
||||
|
||||
|
||||
def kpath2gids(path):
|
||||
path = kpath2gid(path)
|
||||
l = []
|
||||
for i in range(len(path.split("."))):
|
||||
l.append(".".join(path.split(".")[: i + 1]))
|
||||
return l
|
||||
|
||||
|
||||
def kpath2kpaths(path):
|
||||
l = []
|
||||
for i in range(len(path.split("/"))):
|
||||
l.append("/".join(path.split("/")[: i + 1]))
|
||||
return l[1:]
|
||||
|
||||
|
||||
def gid2kpath(gid):
|
||||
return "/" + gid.replace(".", "/")
|
||||
|
||||
|
||||
def count_repeated(itemslist):
|
||||
print(Counter(itemslist))
|
||||
|
||||
|
||||
def groups_kname2gid(groups):
|
||||
return [name.replace(".", "/") for name in groups]
|
||||
|
||||
|
||||
def groups_path2id(groups):
|
||||
return [g.replace("/", ".")[1:] for g in groups]
|
||||
|
||||
|
||||
def groups_id2path(groups):
|
||||
return ["/" + g.replace(".", "/") for g in groups]
|
||||
|
||||
|
||||
def filter_roles_list(role_list):
|
||||
client_roles = ["admin", "manager", "teacher", "student"]
|
||||
return [r for r in role_list if r in client_roles]
|
||||
|
||||
|
||||
def filter_roles_listofdicts(role_listofdicts):
|
||||
client_roles = ["admin", "manager", "teacher", "student"]
|
||||
return [r for r in role_listofdicts if r["name"] in client_roles]
|
||||
|
||||
|
||||
def rand_password(lenght):
|
||||
characters = string.ascii_letters + string.digits + string.punctuation
|
||||
passwd = "".join(random.choice(characters) for i in range(lenght))
|
||||
while not any(ele.isupper() for ele in passwd):
|
||||
passwd = "".join(random.choice(characters) for i in range(lenght))
|
||||
return passwd
|
|
@ -0,0 +1,442 @@
|
|||
#!/usr/bin/env python
|
||||
# coding=utf-8
|
||||
import json
|
||||
import logging as log
|
||||
import os
|
||||
import time
|
||||
import traceback
|
||||
from datetime import datetime, timedelta
|
||||
from pprint import pprint
|
||||
|
||||
import yaml
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
from keycloak import KeycloakAdmin
|
||||
|
||||
from .api_exceptions import Error
|
||||
from .helpers import get_recursive_groups, kpath2kpaths
|
||||
from .postgres import Postgres
|
||||
|
||||
# from admin import app
|
||||
|
||||
|
||||
class KeycloakClient:
|
||||
"""https://www.keycloak.org/docs-api/13.0/rest-api/index.html
|
||||
https://github.com/marcospereirampj/python-keycloak
|
||||
https://gist.github.com/kaqfa/99829941121188d7cef8271f93f52f1f
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
url="http://isard-sso-keycloak:8080/auth/",
|
||||
username=os.environ["KEYCLOAK_USER"],
|
||||
password=os.environ["KEYCLOAK_PASSWORD"],
|
||||
realm="master",
|
||||
verify=True,
|
||||
):
|
||||
self.url = url
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.realm = realm
|
||||
self.verify = verify
|
||||
|
||||
self.keycloak_pg = Postgres(
|
||||
"isard-apps-postgresql",
|
||||
"keycloak",
|
||||
os.environ["KEYCLOAK_DB_USER"],
|
||||
os.environ["KEYCLOAK_DB_PASSWORD"],
|
||||
)
|
||||
|
||||
def connect(self):
|
||||
self.keycloak_admin = KeycloakAdmin(
|
||||
server_url=self.url,
|
||||
username=self.username,
|
||||
password=self.password,
|
||||
realm_name=self.realm,
|
||||
verify=self.verify,
|
||||
)
|
||||
|
||||
# from keycloak import KeycloakAdmin
|
||||
# keycloak_admin = KeycloakAdmin(server_url="http://isard-sso-keycloak:8080/auth/",username="admin",password="keycloakkeycloak",realm_name="master",verify=False)
|
||||
|
||||
""" USERS """
|
||||
|
||||
def get_user_id(self, username):
|
||||
self.connect()
|
||||
return self.keycloak_admin.get_user_id(username)
|
||||
|
||||
def get_users(self):
|
||||
self.connect()
|
||||
return self.keycloak_admin.get_users({})
|
||||
|
||||
def get_users_with_groups_and_roles(self):
|
||||
q = """select u.id, u.username, u.email, u.first_name, u.last_name, u.realm_id, u.enabled, ua.value as quota
|
||||
,json_agg(g."id") as group, json_agg(g_parent."name") as group_parent1, json_agg(g_parent2."name") as group_parent2
|
||||
,json_agg(r.name) as role
|
||||
from user_entity as u
|
||||
left join user_attribute as ua on ua.user_id=u.id and ua.name = 'quota'
|
||||
left join user_group_membership as ugm on ugm.user_id = u.id
|
||||
left join keycloak_group as g on g.id = ugm.group_id
|
||||
left join keycloak_group as g_parent on g.parent_group = g_parent.id
|
||||
left join keycloak_group as g_parent2 on g_parent.parent_group = g_parent2.id
|
||||
left join user_role_mapping as rm on rm.user_id = u.id
|
||||
left join keycloak_role as r on r.id = rm.role_id
|
||||
group by u.id,u.username,u.email,u.first_name,u.last_name, u.realm_id, u.enabled, ua.value
|
||||
order by u.username"""
|
||||
|
||||
(headers, users) = self.keycloak_pg.select_with_headers(q)
|
||||
|
||||
users_with_lists = [
|
||||
list(l[:-4])
|
||||
+ ([[]] if l[-4] == [None] else [list(set(l[-4]))])
|
||||
+ ([[]] if l[-3] == [None] else [list(set(l[-3]))])
|
||||
+ ([[]] if l[-3] == [None] else [list(set(l[-2]))])
|
||||
+ ([[]] if l[-1] == [None] else [list(set(l[-1]))])
|
||||
for l in users
|
||||
]
|
||||
|
||||
users_with_lists = [
|
||||
list(l[:-4])
|
||||
+ ([[]] if l[-4] == [None] else [list(set(l[-4]))])
|
||||
+ ([[]] if l[-3] == [None] else [list(set(l[-3]))])
|
||||
+ ([[]] if l[-3] == [None] else [list(set(l[-2]))])
|
||||
+ ([[]] if l[-1] == [None] else [list(set(l[-1]))])
|
||||
for l in users_with_lists
|
||||
]
|
||||
|
||||
list_dict_users = [dict(zip(headers, r)) for r in users_with_lists]
|
||||
|
||||
return list_dict_users
|
||||
|
||||
def getparent(self, group_id, data):
|
||||
# Recursively get full path from any group_id in the tree
|
||||
path = ""
|
||||
for item in data:
|
||||
if group_id == item[0]:
|
||||
path = self.getparent(item[2], data)
|
||||
path = f"{path}/{item[1]}"
|
||||
return path
|
||||
|
||||
def get_group_path(self, group_id):
|
||||
# Get full path using getparent recursive func
|
||||
# RETURNS: String with full path
|
||||
q = """SELECT * FROM keycloak_group"""
|
||||
groups = self.keycloak_pg.select(q)
|
||||
return self.getparent(group_id, groups)
|
||||
|
||||
def get_user_groups_paths(self, user_id):
|
||||
# Get full paths for user grups
|
||||
# RETURNS list of paths
|
||||
q = """SELECT group_id FROM user_group_membership WHERE user_id = '%s'""" % (
|
||||
user_id
|
||||
)
|
||||
user_group_ids = self.keycloak_pg.select(q)
|
||||
|
||||
paths = []
|
||||
for g in user_group_ids:
|
||||
paths.append(self.get_group_path(g[0]))
|
||||
return paths
|
||||
|
||||
## Too slow. Used the direct postgres
|
||||
# def get_users_with_groups_and_roles(self):
|
||||
# self.connect()
|
||||
# users=self.keycloak_admin.get_users({})
|
||||
# for user in users:
|
||||
# user['groups']=[g['path'] for g in self.keycloak_admin.get_user_groups(user_id=user['id'])]
|
||||
# user['roles']= [r['name'] for r in self.keycloak_admin.get_realm_roles_of_user(user_id=user['id'])]
|
||||
# return users
|
||||
|
||||
def add_user(
|
||||
self,
|
||||
username,
|
||||
first,
|
||||
last,
|
||||
email,
|
||||
password,
|
||||
group=False,
|
||||
password_temporary=True,
|
||||
enabled=True,
|
||||
):
|
||||
# RETURNS string with keycloak user id (the main id in this app)
|
||||
self.connect()
|
||||
username = username.lower()
|
||||
try:
|
||||
uid = self.keycloak_admin.create_user(
|
||||
{
|
||||
"email": email,
|
||||
"username": username,
|
||||
"enabled": enabled,
|
||||
"firstName": first,
|
||||
"lastName": last,
|
||||
"credentials": [
|
||||
{
|
||||
"type": "password",
|
||||
"value": password,
|
||||
"temporary": password_temporary,
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
log.error(traceback.format_exc())
|
||||
raise Error(
|
||||
"conflict",
|
||||
"user/email already exists: " + str(username) + "/" + str(email),
|
||||
)
|
||||
|
||||
if group:
|
||||
path = "/" + group if group[1:] != "/" else group
|
||||
try:
|
||||
gid = self.keycloak_admin.get_group_by_path(
|
||||
path=path, search_in_subgroups=False
|
||||
)["id"]
|
||||
except:
|
||||
self.keycloak_admin.create_group({"name": group})
|
||||
gid = self.keycloak_admin.get_group_by_path(path)["id"]
|
||||
self.keycloak_admin.group_user_add(uid, gid)
|
||||
return uid
|
||||
|
||||
def update_user_pwd(self, user_id, password, password_temporary=True):
|
||||
# Updates
|
||||
payload = {
|
||||
"credentials": [
|
||||
{"type": "password", "value": password, "temporary": password_temporary}
|
||||
]
|
||||
}
|
||||
self.connect()
|
||||
return self.keycloak_admin.update_user(user_id, payload)
|
||||
|
||||
def user_update(self, user_id, enabled, email, first, last, groups=[], roles=[]):
|
||||
## NOTE: Roles didn't seem to be updated/added. Also not confident with groups
|
||||
# Updates
|
||||
payload = {
|
||||
"enabled": enabled,
|
||||
"email": email,
|
||||
"firstName": first,
|
||||
"lastName": last,
|
||||
"groups": groups,
|
||||
"realmRoles": roles,
|
||||
}
|
||||
self.connect()
|
||||
return self.keycloak_admin.update_user(user_id, payload)
|
||||
|
||||
def user_enable(self, user_id):
|
||||
payload = {"enabled": True}
|
||||
self.connect()
|
||||
return self.keycloak_admin.update_user(user_id, payload)
|
||||
|
||||
def user_disable(self, user_id):
|
||||
payload = {"enabled": False}
|
||||
self.connect()
|
||||
return self.keycloak_admin.update_user(user_id, payload)
|
||||
|
||||
def group_user_remove(self, user_id, group_id):
|
||||
self.connect()
|
||||
return self.keycloak_admin.group_user_remove(user_id, group_id)
|
||||
|
||||
# def add_user_role(self,user_id,role_id):
|
||||
# self.connect()
|
||||
# return self.keycloak_admin.assign_role(client_id=client_id, user_id=user_id, role_id=role_id, role_name="test")
|
||||
|
||||
def remove_user_realm_roles(self, user_id, roles):
|
||||
self.connect()
|
||||
roles = [
|
||||
r
|
||||
for r in self.get_user_realm_roles(user_id)
|
||||
if r["name"] in ["admin", "manager", "teacher", "student"]
|
||||
]
|
||||
return self.keycloak_admin.delete_user_realm_role(user_id, roles)
|
||||
|
||||
def delete_user(self, userid):
|
||||
self.connect()
|
||||
return self.keycloak_admin.delete_user(user_id=userid)
|
||||
|
||||
def get_user_groups(self, userid):
|
||||
self.connect()
|
||||
return self.keycloak_admin.get_user_groups(user_id=userid)
|
||||
|
||||
def get_user_realm_roles(self, userid):
|
||||
self.connect()
|
||||
return self.keycloak_admin.get_realm_roles_of_user(user_id=userid)
|
||||
|
||||
def add_user_client_role(self, client_id, user_id, role_id, role_name):
|
||||
self.connect()
|
||||
return self.keycloak_admin.assign_client_role(
|
||||
client_id=client_id, user_id=user_id, role_id=role_id, role_name="test"
|
||||
)
|
||||
|
||||
## GROUPS
|
||||
def get_all_groups(self):
|
||||
## RETURNS ONLY MAIN GROUPS WITH NESTED subGroups list
|
||||
self.connect()
|
||||
return self.keycloak_admin.get_groups()
|
||||
|
||||
def get_groups(self, with_subgroups=True):
|
||||
## RETURNS ALL GROUPS in root list
|
||||
self.connect()
|
||||
groups = self.keycloak_admin.get_groups()
|
||||
return get_recursive_groups(groups, [])
|
||||
|
||||
def get_group_by_id(self, group_id):
|
||||
self.connect()
|
||||
return self.keycloak_admin.get_group(group_id=group_id)
|
||||
|
||||
def get_group_by_path(self, path, recursive=True):
|
||||
self.connect()
|
||||
return self.keycloak_admin.get_group_by_path(
|
||||
path=path, search_in_subgroups=recursive
|
||||
)
|
||||
|
||||
def add_group(self, name, parent=None, skip_exists=False):
|
||||
self.connect()
|
||||
if parent != None:
|
||||
parent = self.get_group_by_path(parent)["id"]
|
||||
return self.keycloak_admin.create_group({"name": name}, parent=parent)
|
||||
|
||||
def delete_group(self, group_id):
|
||||
self.connect()
|
||||
return self.keycloak_admin.delete_group(group_id=group_id)
|
||||
|
||||
def group_user_add(self, user_id, group_id):
|
||||
self.connect()
|
||||
return self.keycloak_admin.group_user_add(user_id, group_id)
|
||||
|
||||
def add_group_tree(self, path):
|
||||
paths = kpath2kpaths(path)
|
||||
parent = "/"
|
||||
for path in paths:
|
||||
try:
|
||||
parent_path = None if parent == "/" else parent
|
||||
# print("parent: "+str(parent_path)+" path: "+path.split("/")[-1])
|
||||
self.add_group(path.split("/")[-1], parent_path, skip_exists=True)
|
||||
parent = path
|
||||
except:
|
||||
# print(traceback.format_exc())
|
||||
log.warning("KEYCLOAK: Group :" + path + " already exists.")
|
||||
parent = path
|
||||
|
||||
def add_user_with_groups_and_role(
|
||||
self, username, first, last, email, password, role, groups
|
||||
):
|
||||
## Add user
|
||||
uid = self.add_user(username, first, last, email, password)
|
||||
## Add user to role
|
||||
log.info("User uid: " + str(uid) + " role: " + str(role))
|
||||
try:
|
||||
therole = role[0]
|
||||
except:
|
||||
therole = ""
|
||||
log.info(self.assign_realm_roles(uid, role))
|
||||
## Create groups in user
|
||||
for g in groups:
|
||||
log.warning("Creating keycloak group: " + g)
|
||||
parts = g.split("/")
|
||||
parent_path = None
|
||||
for i in range(1, len(parts)):
|
||||
# parent_id=None if parent_path==None else self.get_group(parent_path)['id']
|
||||
try:
|
||||
self.add_group(parts[i], parent_path, skip_exists=True)
|
||||
except:
|
||||
log.warning(
|
||||
"Group "
|
||||
+ str(parent_path)
|
||||
+ " already exists. Skipping creation"
|
||||
)
|
||||
pass
|
||||
if parent_path is None:
|
||||
thepath = "/" + parts[i]
|
||||
else:
|
||||
thepath = parent_path + "/" + parts[i]
|
||||
if thepath == "/":
|
||||
log.warning(
|
||||
"Not adding the user "
|
||||
+ username
|
||||
+ " to any group as does not have any..."
|
||||
)
|
||||
continue
|
||||
gid = self.get_group_by_path(path=thepath)["id"]
|
||||
|
||||
log.warning(
|
||||
"Adding "
|
||||
+ username
|
||||
+ " with uuid: "
|
||||
+ uid
|
||||
+ " to group "
|
||||
+ g
|
||||
+ " with uuid: "
|
||||
+ gid
|
||||
)
|
||||
self.keycloak_admin.group_user_add(uid, gid)
|
||||
|
||||
if parent_path == None:
|
||||
parent_path = ""
|
||||
parent_path = parent_path + "/" + parts[i]
|
||||
|
||||
# self.group_user_add(uid,gid)
|
||||
|
||||
## ROLES
|
||||
def get_roles(self):
|
||||
self.connect()
|
||||
return self.keycloak_admin.get_realm_roles()
|
||||
|
||||
def get_role(self, name):
|
||||
self.connect()
|
||||
return self.keycloak_admin.get_realm_role(name)
|
||||
|
||||
def add_role(self, name, description=""):
|
||||
self.connect()
|
||||
return self.keycloak_admin.create_realm_role(
|
||||
{"name": name, "description": description}
|
||||
)
|
||||
|
||||
def delete_role(self, name):
|
||||
self.connect()
|
||||
return self.keycloak_admin.delete_realm_role(name)
|
||||
|
||||
## CLIENTS
|
||||
|
||||
def get_client_roles(self, client_id):
|
||||
self.connect()
|
||||
return self.keycloak_admin.get_client_roles(client_id=client_id)
|
||||
|
||||
def add_client_role(self, client_id, name, description=""):
|
||||
self.connect()
|
||||
return self.keycloak_admin.create_client_role(
|
||||
client_id, {"name": name, "description": description, "clientRole": True}
|
||||
)
|
||||
|
||||
## SYSTEM
|
||||
def get_server_info(self):
|
||||
self.connect()
|
||||
return self.keycloak_admin.get_server_info()
|
||||
|
||||
def get_server_clients(self):
|
||||
self.connect()
|
||||
return self.keycloak_admin.get_clients()
|
||||
|
||||
def get_server_rsa_key(self):
|
||||
self.connect()
|
||||
rsa_key = [
|
||||
k for k in self.keycloak_admin.get_keys()["keys"] if k["type"] == "RSA"
|
||||
][0]
|
||||
return {"name": rsa_key["kid"], "certificate": rsa_key["certificate"]}
|
||||
|
||||
## REALM
|
||||
def assign_realm_roles(self, user_id, role):
|
||||
self.connect()
|
||||
try:
|
||||
role = [
|
||||
r for r in self.keycloak_admin.get_realm_roles() if r["name"] == role
|
||||
]
|
||||
except:
|
||||
return False
|
||||
return self.keycloak_admin.assign_realm_roles(user_id=user_id, roles=role)
|
||||
# return self.keycloak_admin.assign_realm_roles(user_id=user_id, client_id=None, roles=role)
|
||||
|
||||
## CLIENTS
|
||||
def delete_client(self, clientid):
|
||||
self.connect()
|
||||
return self.keycloak_admin.delete_client(clientid)
|
||||
|
||||
def add_client(self, client):
|
||||
self.connect()
|
||||
return self.keycloak_admin.create_client(client)
|
|
@ -0,0 +1,27 @@
|
|||
import logging as log
|
||||
import os
|
||||
import traceback
|
||||
|
||||
from admin import app
|
||||
from pprint import pprint
|
||||
|
||||
from minio import Minio
|
||||
from minio.commonconfig import REPLACE, CopySource
|
||||
from minio.deleteobjects import DeleteObject
|
||||
from requests import get, post
|
||||
|
||||
legal_path= os.path.join(app.root_path, "static/templates/pages/legal/")
|
||||
|
||||
def get_legal(lang):
|
||||
with open(legal_path+lang, "r") as languagefile:
|
||||
return languagefile.read()
|
||||
|
||||
def gen_legal_if_not_exists(lang):
|
||||
if not os.path.isfile(legal_path+lang):
|
||||
log.debug("Creating new language file")
|
||||
with open(legal_path+lang, "w") as languagefile:
|
||||
languagefile.write("<b>Legal</b><br>This is the default legal page for language " + lang)
|
||||
|
||||
def new_legal(lang,html):
|
||||
with open(legal_path+lang, "w") as languagefile:
|
||||
languagefile.write(html)
|
|
@ -0,0 +1,76 @@
|
|||
#!/usr/bin/env python
|
||||
# coding=utf-8
|
||||
|
||||
import logging as log
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import yaml
|
||||
from cerberus import Validator, rules_set_registry, schema_registry
|
||||
|
||||
from admin import app
|
||||
|
||||
|
||||
class AdminValidator(Validator):
|
||||
None
|
||||
# def _normalize_default_setter_genid(self, document):
|
||||
# return _parse_string(document["name"])
|
||||
|
||||
# def _normalize_default_setter_genidlower(self, document):
|
||||
# return _parse_string(document["name"]).lower()
|
||||
|
||||
# def _normalize_default_setter_gengroupid(self, document):
|
||||
# return _parse_string(
|
||||
# document["parent_category"] + "-" + document["uid"]
|
||||
# ).lower()
|
||||
|
||||
|
||||
def load_validators(purge_unknown=True):
|
||||
validators = {}
|
||||
schema_path = os.path.join(app.root_path, "schemas")
|
||||
for schema_filename in os.listdir(schema_path):
|
||||
try:
|
||||
with open(os.path.join(schema_path, schema_filename)) as file:
|
||||
schema_yml = file.read()
|
||||
schema = yaml.load(schema_yml, Loader=yaml.FullLoader)
|
||||
validators[schema_filename.split(".")[0]] = AdminValidator(
|
||||
schema, purge_unknown=purge_unknown
|
||||
)
|
||||
except IsADirectoryError:
|
||||
None
|
||||
return validators
|
||||
|
||||
|
||||
app.validators = load_validators()
|
||||
|
||||
|
||||
class loadConfig:
|
||||
def __init__(self, app=None):
|
||||
try:
|
||||
app.config.setdefault("DOMAIN", os.environ["DOMAIN"])
|
||||
app.config.setdefault(
|
||||
"KEYCLOAK_POSTGRES_USER", os.environ["KEYCLOAK_DB_USER"]
|
||||
)
|
||||
app.config.setdefault(
|
||||
"KEYCLOAK_POSTGRES_PASSWORD", os.environ["KEYCLOAK_DB_PASSWORD"]
|
||||
)
|
||||
app.config.setdefault(
|
||||
"MOODLE_POSTGRES_USER", os.environ["MOODLE_POSTGRES_USER"]
|
||||
)
|
||||
app.config.setdefault(
|
||||
"MOODLE_POSTGRES_PASSWORD", os.environ["MOODLE_POSTGRES_PASSWORD"]
|
||||
)
|
||||
app.config.setdefault(
|
||||
"NEXTCLOUD_POSTGRES_USER", os.environ["NEXTCLOUD_POSTGRES_USER"]
|
||||
)
|
||||
app.config.setdefault(
|
||||
"NEXTCLOUD_POSTGRES_PASSWORD", os.environ["NEXTCLOUD_POSTGRES_PASSWORD"]
|
||||
)
|
||||
app.config.setdefault(
|
||||
"VERIFY", True if os.environ["VERIFY"] == "true" else False
|
||||
)
|
||||
app.config.setdefault("API_SECRET", os.environ.get("API_SECRET"))
|
||||
except Exception as e:
|
||||
log.error(traceback.format_exc())
|
||||
raise
|
|
@ -0,0 +1,266 @@
|
|||
import logging as log
|
||||
import traceback
|
||||
from pprint import pprint
|
||||
|
||||
from requests import get, post
|
||||
|
||||
from admin import app
|
||||
|
||||
from .exceptions import UserExists, UserNotFound
|
||||
from .postgres import Postgres
|
||||
|
||||
# Module variables to connect to moodle api
|
||||
|
||||
|
||||
class Moodle:
|
||||
"""https://github.com/mrcinv/moodle_api.py
|
||||
https://docs.moodle.org/dev/Web_service_API_functions
|
||||
https://docs.moodle.org/311/en/Using_web_services
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
key=app.config["MOODLE_WS_TOKEN"],
|
||||
url="https://moodle." + app.config["DOMAIN"],
|
||||
endpoint="/webservice/rest/server.php",
|
||||
verify=app.config["VERIFY"],
|
||||
):
|
||||
self.key = key
|
||||
self.url = url
|
||||
self.endpoint = endpoint
|
||||
self.verify = verify
|
||||
|
||||
self.moodle_pg = Postgres(
|
||||
"isard-apps-postgresql",
|
||||
"moodle",
|
||||
app.config["MOODLE_POSTGRES_USER"],
|
||||
app.config["MOODLE_POSTGRES_PASSWORD"],
|
||||
)
|
||||
|
||||
def rest_api_parameters(self, in_args, prefix="", out_dict=None):
|
||||
"""Transform dictionary/array structure to a flat dictionary, with key names
|
||||
defining the structure.
|
||||
Example usage:
|
||||
>>> rest_api_parameters({'courses':[{'id':1,'name': 'course1'}]})
|
||||
{'courses[0][id]':1,
|
||||
'courses[0][name]':'course1'}
|
||||
"""
|
||||
if out_dict == None:
|
||||
out_dict = {}
|
||||
if not type(in_args) in (list, dict):
|
||||
out_dict[prefix] = in_args
|
||||
return out_dict
|
||||
if prefix == "":
|
||||
prefix = prefix + "{0}"
|
||||
else:
|
||||
prefix = prefix + "[{0}]"
|
||||
if type(in_args) == list:
|
||||
for idx, item in enumerate(in_args):
|
||||
self.rest_api_parameters(item, prefix.format(idx), out_dict)
|
||||
elif type(in_args) == dict:
|
||||
for key, item in in_args.items():
|
||||
self.rest_api_parameters(item, prefix.format(key), out_dict)
|
||||
return out_dict
|
||||
|
||||
def call(self, fname, **kwargs):
|
||||
"""Calls moodle API function with function name fname and keyword arguments.
|
||||
Example:
|
||||
>>> call_mdl_function('core_course_update_courses',
|
||||
courses = [{'id': 1, 'fullname': 'My favorite course'}])
|
||||
"""
|
||||
parameters = self.rest_api_parameters(kwargs)
|
||||
parameters.update(
|
||||
{"wstoken": self.key, "moodlewsrestformat": "json", "wsfunction": fname}
|
||||
)
|
||||
response = post(self.url + self.endpoint, parameters, verify=self.verify)
|
||||
response = response.json()
|
||||
if type(response) == dict and response.get("exception"):
|
||||
raise SystemError(response)
|
||||
return response
|
||||
|
||||
def create_user(self, email, username, password, first_name="-", last_name="-"):
|
||||
if len(self.get_user_by("username", username)["users"]):
|
||||
raise UserExists
|
||||
try:
|
||||
data = [
|
||||
{
|
||||
"username": username,
|
||||
"email": email,
|
||||
"password": password,
|
||||
"firstname": first_name,
|
||||
"lastname": last_name,
|
||||
}
|
||||
]
|
||||
user = self.call("core_user_create_users", users=data)
|
||||
return user # [{'id': 8, 'username': 'asdfw'}]
|
||||
except SystemError as se:
|
||||
raise SystemError(se.args[0]["message"])
|
||||
|
||||
def update_user(self, username, email, first_name, last_name, enabled=True):
|
||||
user = self.get_user_by("username", username)["users"][0]
|
||||
if not len(user):
|
||||
raise UserNotFound
|
||||
try:
|
||||
data = [
|
||||
{
|
||||
"id": user["id"],
|
||||
"username": username,
|
||||
"email": email,
|
||||
"firstname": first_name,
|
||||
"lastname": last_name,
|
||||
"suspended": 0 if enabled else 1,
|
||||
}
|
||||
]
|
||||
user = self.call("core_user_update_users", users=data)
|
||||
return user
|
||||
except SystemError as se:
|
||||
raise SystemError(se.args[0]["message"])
|
||||
|
||||
def delete_user(self, user_id):
|
||||
user = self.call("core_user_delete_users", userids=[user_id])
|
||||
return user
|
||||
|
||||
def delete_users(self, userids):
|
||||
user = self.call("core_user_delete_users", userids=userids)
|
||||
return user
|
||||
|
||||
def get_user_by(self, key, value):
|
||||
criteria = [{"key": key, "value": value}]
|
||||
try:
|
||||
user = self.call("core_user_get_users", criteria=criteria)
|
||||
except:
|
||||
raise SystemError("Error calling Moodle API\n", traceback.format_exc())
|
||||
return user
|
||||
# {'users': [{'id': 8, 'username': 'asdfw', 'firstname': 'afowie', 'lastname': 'aokjdnfwe', 'fullname': 'afowie aokjdnfwe', 'email': 'awfewe@ads.com', 'department': '', 'firstaccess': 0, 'lastaccess': 0, 'auth': 'manual', 'suspended': False, 'confirmed': True, 'lang': 'ca', 'theme': '', 'timezone': '99', 'mailformat': 1, 'profileimageurlsmall': 'https://moodle.mydomain.duckdns.org/theme/image.php/cbe/core/1630941606/u/f2', 'profileimageurl': 'https://DOMAIN/theme/image.php/cbe/core/1630941606/u/f1'}], 'warnings': []}
|
||||
|
||||
def get_users_with_groups_and_roles(self):
|
||||
q = """select u.id as id, username, firstname as first, lastname as last, email, json_agg(h.name) as groups, json_agg(r.shortname) as roles
|
||||
from mdl_user as u
|
||||
LEFT JOIN mdl_cohort_members AS hm on hm.userid = u.id
|
||||
left join mdl_cohort AS h ON h.id = hm.cohortid
|
||||
left join mdl_role_assignments AS ra ON ra.id = u.id
|
||||
left join mdl_role as r on r.id = ra.roleid
|
||||
where u.deleted = 0
|
||||
group by u.id , username, first, last, email"""
|
||||
(headers, users) = self.moodle_pg.select_with_headers(q)
|
||||
users_with_lists = [
|
||||
list(l[:-2])
|
||||
+ ([[]] if l[-2] == [None] else [list(set(l[-2]))])
|
||||
+ ([[]] if l[-1] == [None] else [list(set(l[-1]))])
|
||||
for l in users
|
||||
]
|
||||
list_dict_users = [dict(zip(headers, r)) for r in users_with_lists]
|
||||
return list_dict_users
|
||||
|
||||
## NOT USED. Too slow
|
||||
# def get_users_with_groups_and_roles(self):
|
||||
# users=self.get_user_by('email','%%')['users']
|
||||
# for user in users:
|
||||
# user['groups']=[c['name'] for c in self.get_user_cohorts(user['id'])]
|
||||
# user['roles']=[]
|
||||
# return users
|
||||
|
||||
def enroll_user_to_course(self, user_id, course_id, role_id=5):
|
||||
# 5 is student
|
||||
data = [{"roleid": role_id, "userid": user_id, "courseid": course_id}]
|
||||
enrolment = self.call("enrol_manual_enrol_users", enrolments=data)
|
||||
return enrolment
|
||||
|
||||
def get_quiz_attempt(self, quiz_id, user_id):
|
||||
attempts = self.call(
|
||||
"mod_quiz_get_user_attempts", quizid=quiz_id, userid=user_id
|
||||
)
|
||||
return attempts
|
||||
|
||||
def get_cohorts(self):
|
||||
cohorts = self.call("core_cohort_get_cohorts")
|
||||
return cohorts
|
||||
|
||||
def add_system_cohort(self, name, description="", visible=True):
|
||||
visible = 1 if visible else 0
|
||||
data = [
|
||||
{
|
||||
"categorytype": {"type": "system", "value": ""},
|
||||
"name": name,
|
||||
"idnumber": name,
|
||||
"description": description,
|
||||
"visible": visible,
|
||||
}
|
||||
]
|
||||
cohort = self.call("core_cohort_create_cohorts", cohorts=data)
|
||||
return cohort
|
||||
|
||||
# def add_users_to_cohort(self,users,cohort):
|
||||
# criteria = [{'key': key, 'value': value}]
|
||||
# user = self.call('core_cohort_add_cohort_members', criteria=criteria)
|
||||
# return user
|
||||
|
||||
def add_user_to_cohort(self, userid, cohortid):
|
||||
members = [
|
||||
{
|
||||
"cohorttype": {"type": "id", "value": cohortid},
|
||||
"usertype": {"type": "id", "value": userid},
|
||||
}
|
||||
]
|
||||
user = self.call("core_cohort_add_cohort_members", members=members)
|
||||
return user
|
||||
|
||||
def delete_user_in_cohort(self, userid, cohortid):
|
||||
members = [{"cohortid": cohortid, "userid": userid}]
|
||||
user = self.call("core_cohort_delete_cohort_members", members=members)
|
||||
return user
|
||||
|
||||
def get_cohort_members(self, cohort_ids):
|
||||
members = self.call("core_cohort_get_cohort_members", cohortids=cohort_ids)
|
||||
# [0]['userids']
|
||||
return members
|
||||
|
||||
def delete_cohorts(self, cohortids):
|
||||
deleted = self.call("core_cohort_delete_cohorts", cohortids=cohortids)
|
||||
return deleted
|
||||
|
||||
def get_user_cohorts(self, user_id):
|
||||
user_cohorts = []
|
||||
cohorts = self.get_cohorts()
|
||||
for cohort in cohorts:
|
||||
if user_id in self.get_cohort_members(cohort["id"]):
|
||||
user_cohorts.append(cohort)
|
||||
return user_cohorts
|
||||
|
||||
def add_user_to_siteadmin(self, user_id):
|
||||
q = """SELECT value FROM mdl_config WHERE name='siteadmins'"""
|
||||
value = self.moodle_pg.select(q)[0][0]
|
||||
if str(user_id) not in value:
|
||||
value = value + "," + str(user_id)
|
||||
q = """UPDATE mdl_config SET value = '%s' WHERE name='siteadmins'""" % (
|
||||
value
|
||||
)
|
||||
self.moodle_pg.update(q)
|
||||
log.warning(
|
||||
"MOODLE:ADDING THE USER TO ADMINS: This needs a purge cache in moodle!"
|
||||
)
|
||||
|
||||
# def add_role_to_user(self, user_id, role='admin', context='missing'):
|
||||
# if role=='admin':
|
||||
# role_id=1
|
||||
# else:
|
||||
# return False
|
||||
# assignments = [{'roleid': role_id, 'userid': user_id, 'contextid': 0}]
|
||||
# self.call('core_role_assign_roles', assignments=assignments)
|
||||
# userid=user_id, role_id=role_id)
|
||||
# 'contextlevel': 1,
|
||||
|
||||
|
||||
# define('CONTEXT_SYSTEM', 10);
|
||||
# define('CONTEXT_USER', 30);
|
||||
# define('CONTEXT_COURSECAT', 40);
|
||||
# define('CONTEXT_COURSE', 50);
|
||||
# define('CONTEXT_MODULE', 70);
|
||||
# define('CONTEXT_BLOCK', 80);
|
||||
|
||||
# 'contextlevel': , 'instanceid'
|
||||
# $assignment = array( 'roleid' => $role_id, 'userid' => $user_id, 'contextid' => $context_id );
|
||||
# $assignments = array( $assignment );
|
||||
# $params = array( 'assignments' => $assignments );
|
||||
|
||||
# $response = call_moodle( 'core_role_assign_roles', $params, $token );
|
|
@ -0,0 +1,32 @@
|
|||
#!/usr/bin/env python
|
||||
# coding=utf-8
|
||||
import json
|
||||
import logging as log
|
||||
import time
|
||||
import traceback
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import mysql.connector
|
||||
import yaml
|
||||
|
||||
# from admin import app
|
||||
|
||||
|
||||
class Mysql:
|
||||
def __init__(self, host, database, user, password):
|
||||
self.conn = mysql.connector.connect(
|
||||
host=host, database=database, user=user, password=password
|
||||
)
|
||||
|
||||
def select(self, sql):
|
||||
self.cur = self.conn.cursor()
|
||||
self.cur.execute(sql)
|
||||
data = self.cur.fetchall()
|
||||
self.cur.close()
|
||||
return data
|
||||
|
||||
def update(self, sql):
|
||||
self.cur = self.conn.cursor()
|
||||
self.cur.execute(sql)
|
||||
self.conn.commit()
|
||||
self.cur.close()
|
|
@ -0,0 +1,565 @@
|
|||
#!/usr/bin/env python
|
||||
# coding=utf-8
|
||||
|
||||
import json
|
||||
import logging as log
|
||||
import os
|
||||
import pprint
|
||||
import time
|
||||
import traceback
|
||||
import urllib
|
||||
|
||||
import requests
|
||||
from psycopg2 import sql
|
||||
|
||||
# from ..lib.log import *
|
||||
from admin import app
|
||||
|
||||
from .nextcloud_exc import *
|
||||
from .postgres import Postgres
|
||||
|
||||
|
||||
class Nextcloud:
|
||||
def __init__(
|
||||
self,
|
||||
url="https://nextcloud." + app.config["DOMAIN"],
|
||||
username=os.environ["NEXTCLOUD_ADMIN_USER"],
|
||||
password=os.environ["NEXTCLOUD_ADMIN_PASSWORD"],
|
||||
verify=True,
|
||||
):
|
||||
|
||||
self.verify_cert = verify
|
||||
self.apiurl = url + "/ocs/v1.php/cloud/"
|
||||
self.shareurl = url + "/ocs/v2.php/apps/files_sharing/api/v1/"
|
||||
self.davurl = url + "/remote.php/dav/files/"
|
||||
self.auth = (username, password)
|
||||
self.user = username
|
||||
|
||||
self.nextcloud_pg = Postgres(
|
||||
"isard-apps-postgresql",
|
||||
"nextcloud",
|
||||
app.config["NEXTCLOUD_POSTGRES_USER"],
|
||||
app.config["NEXTCLOUD_POSTGRES_PASSWORD"],
|
||||
)
|
||||
|
||||
def _request(
|
||||
self, method, url, data={}, headers={"OCS-APIRequest": "true"}, auth=False
|
||||
):
|
||||
if auth == False:
|
||||
auth = self.auth
|
||||
try:
|
||||
response = requests.request(
|
||||
method,
|
||||
url,
|
||||
data=data,
|
||||
auth=auth,
|
||||
verify=self.verify_cert,
|
||||
headers=headers,
|
||||
)
|
||||
if "meta" in response.text:
|
||||
if "<statuscode>997</statuscode>" in response.text:
|
||||
raise ProviderUnauthorized
|
||||
# if '<statuscode>998</statuscode>' in response.text: raise ProviderInvalidQuery
|
||||
return response.text
|
||||
|
||||
## At least the ProviderSslError is not being catched or not raised correctly
|
||||
except requests.exceptions.HTTPError as errh:
|
||||
raise ProviderConnError
|
||||
except requests.exceptions.Timeout as errt:
|
||||
raise ProviderConnTimeout
|
||||
except requests.exceptions.SSLError as err:
|
||||
raise ProviderSslError
|
||||
except requests.exceptions.ConnectionError as errc:
|
||||
raise ProviderConnError
|
||||
# except requests.exceptions.RequestException as err:
|
||||
# raise ProviderError
|
||||
except Exception as e:
|
||||
if str(e) == "an integer is required (got type bytes)":
|
||||
raise ProviderConnError
|
||||
raise ProviderError
|
||||
|
||||
def check_connection(self):
|
||||
url = self.apiurl + "users/" + self.user + "?format=json"
|
||||
try:
|
||||
result = self._request("GET", url)
|
||||
if json.loads(result)["ocs"]["meta"]["statuscode"] == 100:
|
||||
return True
|
||||
raise ProviderError
|
||||
except requests.exceptions.HTTPError as errh:
|
||||
raise ProviderConnError
|
||||
except requests.exceptions.ConnectionError as errc:
|
||||
raise ProviderConnError
|
||||
except requests.exceptions.Timeout as errt:
|
||||
raise ProviderConnTimeout
|
||||
except requests.exceptions.SSLError as err:
|
||||
raise ProviderSslError
|
||||
except requests.exceptions.RequestException as err:
|
||||
raise ProviderError
|
||||
except Exception as e:
|
||||
if str(e) == "an integer is required (got type bytes)":
|
||||
raise ProviderConnError
|
||||
raise ProviderError
|
||||
|
||||
def get_user(self, userid):
|
||||
url = self.apiurl + "users/" + userid + "?format=json"
|
||||
try:
|
||||
result = json.loads(self._request("GET", url))
|
||||
if result["ocs"]["meta"]["statuscode"] == 100:
|
||||
return result["ocs"]["data"]
|
||||
raise ProviderItemNotExists
|
||||
except:
|
||||
# log.error(traceback.format_exc())
|
||||
raise
|
||||
# 100 - successful
|
||||
|
||||
# q = """select u.uid as username, adn.value as displayname, ade.value as email, json_agg(gg.displayname) as admin_groups,json_agg(g.displayname) as groups
|
||||
# from oc_users as u
|
||||
# left join oc_group_user as gu on gu.uid = u.uid
|
||||
# left join oc_groups as g on gu.gid = g.gid
|
||||
# left join oc_group_admin as ga on ga.uid = u.uid
|
||||
# left join oc_groups as gg on gg.gid = ga.gid
|
||||
# left join oc_accounts_data as adn on adn.uid = u.uid and adn.name = 'displayname'
|
||||
# left join oc_accounts_data as ade on ade.uid = u.uid and ade.name = 'email'
|
||||
# group by u.uid, adn.value, ade.value"""
|
||||
# cur.execute(q)
|
||||
# users = cur.fetchall()
|
||||
# fields = [a.name for a in cur.description]
|
||||
# cur.close()
|
||||
# conn.close()
|
||||
|
||||
# users_with_lists = [list(l[:-2])+([[]] if l[-2] == [None] else [list(set(l[-2]))]) + ([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users]
|
||||
# users_with_lists = [list(l[:-2])+([[]] if l[-2] == [None] else [list(set(l[-2]))]) + ([[]] if l[-1] == [None] else [list(set(l[-1]))]) for l in users_with_lists]
|
||||
# list_dict_users = [dict(zip(fields, r)) for r in users_with_lists]
|
||||
def get_users_list(self):
|
||||
# q = """select u.uid as username, adn.value as displayname, ade.value as email, json_agg(gg.displayname) as admin_groups,json_agg(g.displayname) as groups
|
||||
# from oc_users as u
|
||||
# left join oc_group_user as gu on gu.uid = u.uid
|
||||
# left join oc_groups as g on gu.gid = g.gid
|
||||
# left join oc_group_admin as ga on ga.uid = u.uid
|
||||
# left join oc_groups as gg on gg.gid = ga.gid
|
||||
# left join oc_accounts_data as adn on adn.uid = u.uid and adn.name = 'displayname'
|
||||
# left join oc_accounts_data as ade on ade.uid = u.uid and ade.name = 'email'
|
||||
# group by u.uid, adn.value, ade.value"""
|
||||
|
||||
# With quotas
|
||||
q = """select u.uid as username, configvalue as quota, sum(size) as total_bytes, adn.value as displayname, ade.value as email, json_agg(gg.displayname) as admin_groups,json_agg(g.displayname) as groups
|
||||
from oc_accounts as u
|
||||
left join oc_group_user as gu on gu.uid = u.uid
|
||||
left join oc_groups as g on gu.gid = g.gid
|
||||
left join oc_group_admin as ga on ga.uid = u.uid
|
||||
left join oc_groups as gg on gg.gid = ga.gid
|
||||
left join oc_accounts_data as adn on adn.uid = u.uid and adn.name = 'displayname'
|
||||
left join oc_accounts_data as ade on ade.uid = u.uid and ade.name = 'email'
|
||||
left join oc_preferences as pref on u.uid=pref.userid and appid='files' and configkey='quota'
|
||||
left join oc_storages as s on s.id=CONCAT('home::',u.uid)
|
||||
left join oc_filecache as fc on fc.storage = numeric_id
|
||||
group by u.uid, adn.value, ade.value, pref.configvalue"""
|
||||
(headers, users) = self.nextcloud_pg.select_with_headers(q)
|
||||
users_with_lists = [
|
||||
list(l[:-2])
|
||||
+ ([[]] if l[-2] == [None] else [list(set(l[-2]))])
|
||||
+ ([[]] if l[-1] == [None] else [list(set(l[-1]))])
|
||||
for l in users
|
||||
]
|
||||
users_with_lists = [
|
||||
list(l[:-2])
|
||||
+ ([[]] if l[-2] == [None] else [list(set(l[-2]))])
|
||||
+ ([[]] if l[-1] == [None] else [list(set(l[-1]))])
|
||||
for l in users_with_lists
|
||||
]
|
||||
list_dict_users = [dict(zip(headers, r)) for r in users_with_lists]
|
||||
return list_dict_users
|
||||
|
||||
### Too slow...
|
||||
# def get_users_list(self):
|
||||
# url = self.apiurl + "users?format=json"
|
||||
# try:
|
||||
# result = json.loads(self._request('GET',url))
|
||||
# if result['ocs']['meta']['statuscode'] == 100: return result['ocs']['data']['users']
|
||||
# log.error('Get Nextcloud provider users list error: '+str(result))
|
||||
# raise ProviderOpError
|
||||
# except:
|
||||
# log.error(traceback.format_exc())
|
||||
# raise
|
||||
|
||||
def add_user(
|
||||
self, userid, userpassword, quota=False, group=False, email="", displayname=""
|
||||
):
|
||||
data = {
|
||||
"userid": userid,
|
||||
"password": userpassword,
|
||||
"quota": quota,
|
||||
"groups[]": group,
|
||||
"email": email,
|
||||
"displayname": displayname,
|
||||
}
|
||||
if not group:
|
||||
del data["groups[]"]
|
||||
if not quota:
|
||||
del data["quota"]
|
||||
# if group:
|
||||
# data={'userid':userid,'password':userpassword,'quota':quota,'groups[]':group,'email':email,'displayname':displayname}
|
||||
# else:
|
||||
# data={'userid':userid,'password':userpassword,'quota':quota,'email':email,'displayname':displayname}
|
||||
url = self.apiurl + "users?format=json"
|
||||
headers = {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"OCS-APIRequest": "true",
|
||||
}
|
||||
try:
|
||||
result = json.loads(self._request("POST", url, data=data, headers=headers))
|
||||
if result["ocs"]["meta"]["statuscode"] == 100:
|
||||
return True
|
||||
if result["ocs"]["meta"]["statuscode"] == 102:
|
||||
raise ProviderItemExists
|
||||
if result["ocs"]["meta"]["statuscode"] == 104:
|
||||
self.add_group(group)
|
||||
# raise ProviderGroupNotExists
|
||||
log.error("Get Nextcloud provider user add error: " + str(result))
|
||||
raise ProviderOpError
|
||||
except:
|
||||
# log.error(traceback.format_exc())
|
||||
raise
|
||||
# 100 - successful
|
||||
# 101 - invalid input data
|
||||
# 102 - username already exists
|
||||
# 103 - unknown error occurred whilst adding the user
|
||||
# 104 - group does not exist
|
||||
# 105 - insufficient privileges for group
|
||||
# 106 - no group specified (required for subadmins)
|
||||
# 107 - all errors that contain a hint - for example “Password is among the 1,000,000 most common ones. Please make it unique.” (this code was added in 12.0.6 & 13.0.1)
|
||||
|
||||
def update_user(self, userid, key_values):
|
||||
# key_values={'quota':quota,'email':email,'displayname':displayname}
|
||||
|
||||
url = self.apiurl + "users/" + userid + "?format=json"
|
||||
headers = {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"OCS-APIRequest": "true",
|
||||
}
|
||||
for k, v in key_values.items():
|
||||
data = {"key": k, "value": v}
|
||||
|
||||
try:
|
||||
result = json.loads(
|
||||
self._request("PUT", url, data=data, headers=headers)
|
||||
)
|
||||
if result["ocs"]["meta"]["statuscode"] == 100:
|
||||
return True
|
||||
if result["ocs"]["meta"]["statuscode"] == 102:
|
||||
raise ProviderItemExists
|
||||
if result["ocs"]["meta"]["statuscode"] == 104:
|
||||
raise ProviderGroupNotExists
|
||||
log.error("Get Nextcloud provider user add error: " + str(result))
|
||||
raise ProviderOpError
|
||||
except:
|
||||
# log.error(traceback.format_exc())
|
||||
raise
|
||||
|
||||
def add_user_to_group(self, userid, group_id):
|
||||
data = {"groupid": group_id}
|
||||
|
||||
url = self.apiurl + "users/" + userid + "/groups?format=json"
|
||||
headers = {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"OCS-APIRequest": "true",
|
||||
}
|
||||
try:
|
||||
result = json.loads(self._request("POST", url, data=data, headers=headers))
|
||||
if result["ocs"]["meta"]["statuscode"] == 100:
|
||||
return True
|
||||
if result["ocs"]["meta"]["statuscode"] == 102:
|
||||
raise ProviderItemExists
|
||||
if result["ocs"]["meta"]["statuscode"] == 104:
|
||||
raise ProviderGroupNotExists
|
||||
log.error("Get Nextcloud provider user add error: " + str(result))
|
||||
raise ProviderOpError
|
||||
except:
|
||||
# log.error(traceback.format_exc())
|
||||
raise
|
||||
|
||||
def remove_user_from_group(self, userid, group_id):
|
||||
data = {"groupid": group_id}
|
||||
|
||||
url = self.apiurl + "users/" + userid + "/groups?format=json"
|
||||
headers = {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"OCS-APIRequest": "true",
|
||||
}
|
||||
try:
|
||||
result = json.loads(
|
||||
self._request("DELETE", url, data=data, headers=headers)
|
||||
)
|
||||
if result["ocs"]["meta"]["statuscode"] == 100:
|
||||
return True
|
||||
if result["ocs"]["meta"]["statuscode"] == 102:
|
||||
raise ProviderItemExists
|
||||
if result["ocs"]["meta"]["statuscode"] == 104:
|
||||
self.add_group(group)
|
||||
# raise ProviderGroupNotExists
|
||||
log.error("Get Nextcloud provider user add error: " + str(result))
|
||||
raise ProviderOpError
|
||||
except:
|
||||
# log.error(traceback.format_exc())
|
||||
raise
|
||||
|
||||
def add_user_with_groups(
|
||||
self, userid, userpassword, quota=False, groups=[], email="", displayname=""
|
||||
):
|
||||
data = {
|
||||
"userid": userid,
|
||||
"password": userpassword,
|
||||
"quota": quota,
|
||||
"groups[]": groups,
|
||||
"email": email,
|
||||
"displayname": displayname,
|
||||
}
|
||||
# if not group: del data['groups[]']
|
||||
if not quota:
|
||||
del data["quota"]
|
||||
# if group:
|
||||
# data={'userid':userid,'password':userpassword,'quota':quota,'groups[]':group,'email':email,'displayname':displayname}
|
||||
# else:
|
||||
# data={'userid':userid,'password':userpassword,'quota':quota,'email':email,'displayname':displayname}
|
||||
url = self.apiurl + "users?format=json"
|
||||
headers = {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"OCS-APIRequest": "true",
|
||||
}
|
||||
try:
|
||||
result = json.loads(self._request("POST", url, data=data, headers=headers))
|
||||
if result["ocs"]["meta"]["statuscode"] == 100:
|
||||
return True
|
||||
if result["ocs"]["meta"]["statuscode"] == 102:
|
||||
raise ProviderItemExists
|
||||
if result["ocs"]["meta"]["statuscode"] == 104:
|
||||
# self.add_group(group)
|
||||
None
|
||||
# raise ProviderGroupNotExists
|
||||
log.error("Get Nextcloud provider user add error: " + str(result))
|
||||
raise ProviderOpError
|
||||
except:
|
||||
# log.error(traceback.format_exc())
|
||||
raise
|
||||
# 100 - successful
|
||||
# 101 - invalid input data
|
||||
# 102 - username already exists
|
||||
# 103 - unknown error occurred whilst adding the user
|
||||
# 104 - group does not exist
|
||||
# 105 - insufficient privileges for group
|
||||
# 106 - no group specified (required for subadmins)
|
||||
# 107 - all errors that contain a hint - for example “Password is among the 1,000,000 most common ones. Please make it unique.” (this code was added in 12.0.6 & 13.0.1)
|
||||
|
||||
def delete_user(self, userid):
|
||||
url = self.apiurl + "users/" + userid + "?format=json"
|
||||
try:
|
||||
result = json.loads(self._request("DELETE", url))
|
||||
if result["ocs"]["meta"]["statuscode"] == 100:
|
||||
return True
|
||||
if result["ocs"]["meta"]["statuscode"] == 101:
|
||||
raise ProviderUserNotExists
|
||||
# log.error(traceback.format_exc())
|
||||
raise ProviderOpError
|
||||
except:
|
||||
# log.error(traceback.format_exc())
|
||||
raise
|
||||
# 100 - successful
|
||||
# 101 - failure
|
||||
|
||||
def enable_user(self, userid):
|
||||
None
|
||||
|
||||
def disable_user(self, userid):
|
||||
None
|
||||
|
||||
def exists_user_folder(self, userid, userpassword, folder="IsardVDI"):
|
||||
auth = (userid, userpassword)
|
||||
url = self.davurl + userid + "/" + folder + "?format=json"
|
||||
headers = {
|
||||
"Depth": "0",
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"OCS-APIRequest": "true",
|
||||
}
|
||||
try:
|
||||
result = self._request("PROPFIND", url, auth=auth, headers=headers)
|
||||
if "<d:status>HTTP/1.1 200 OK</d:status>" in result:
|
||||
return True
|
||||
return False
|
||||
except:
|
||||
# log.error(traceback.format_exc())
|
||||
raise
|
||||
|
||||
def add_user_folder(self, userid, userpassword, folder="IsardVDI"):
|
||||
auth = (userid, userpassword)
|
||||
url = self.davurl + userid + "/" + folder + "?format=json"
|
||||
headers = {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"OCS-APIRequest": "true",
|
||||
}
|
||||
try:
|
||||
result = self._request("MKCOL", url, auth=auth, headers=headers)
|
||||
if result == "":
|
||||
return True
|
||||
if (
|
||||
"<s:message>The resource you tried to create already exists</s:message>"
|
||||
in result
|
||||
):
|
||||
raise ProviderItemExists
|
||||
log.error(result.split("message>")[1].split("<")[0])
|
||||
raise ProviderOpError
|
||||
except:
|
||||
# log.error(traceback.format_exc())
|
||||
raise
|
||||
|
||||
def exists_user_share_folder(self, userid, userpassword, folder="IsardVDI"):
|
||||
auth = (userid, userpassword)
|
||||
url = self.shareurl + "shares?format=json"
|
||||
headers = {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"OCS-APIRequest": "true",
|
||||
}
|
||||
try:
|
||||
result = json.loads(self._request("GET", url, auth=auth, headers=headers))
|
||||
if result["ocs"]["meta"]["statuscode"] == 200:
|
||||
share = [s for s in result["ocs"]["data"] if s["path"] == "/" + folder]
|
||||
if len(share) >= 1:
|
||||
# Should we delete all but the first (0) one?
|
||||
return {"token": share[0]["token"], "url": share[0]["url"]}
|
||||
raise ProviderItemNotExists
|
||||
raise ProviderOpError
|
||||
except:
|
||||
# log.error(traceback.format_exc())
|
||||
raise
|
||||
|
||||
def add_user_share_folder(self, userid, userpassword, folder="IsardVDI"):
|
||||
auth = (userid, userpassword)
|
||||
data = {"path": "/" + folder, "shareType": 3}
|
||||
url = self.shareurl + "shares?format=json"
|
||||
headers = {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"OCS-APIRequest": "true",
|
||||
}
|
||||
try:
|
||||
result = json.loads(
|
||||
self._request("POST", url, data=data, auth=auth, headers=headers)
|
||||
)
|
||||
if (
|
||||
result["ocs"]["meta"]["statuscode"] == 100
|
||||
or result["ocs"]["meta"]["statuscode"] == 200
|
||||
):
|
||||
return {
|
||||
"token": result["ocs"]["data"]["token"],
|
||||
"url": result["ocs"]["data"]["url"],
|
||||
}
|
||||
log.error(
|
||||
"Add user share folder error: " + result["ocs"]["meta"]["message"]
|
||||
)
|
||||
raise ProviderFolderNotExists
|
||||
except:
|
||||
# log.error(traceback.format_exc())
|
||||
raise
|
||||
|
||||
def get_group(self, userid):
|
||||
None
|
||||
|
||||
def get_groups_list(self):
|
||||
url = self.apiurl + "groups?format=json"
|
||||
try:
|
||||
result = json.loads(self._request("GET", url))
|
||||
if result["ocs"]["meta"]["statuscode"] == 100:
|
||||
return [g for g in result["ocs"]["data"]["groups"]]
|
||||
raise ProviderOpError
|
||||
except:
|
||||
# log.error(traceback.format_exc())
|
||||
raise
|
||||
|
||||
def add_group(self, groupid):
|
||||
data = {"groupid": groupid}
|
||||
url = self.apiurl + "groups?format=json"
|
||||
headers = {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"OCS-APIRequest": "true",
|
||||
}
|
||||
try:
|
||||
result = json.loads(
|
||||
self._request("POST", url, data=data, auth=self.auth, headers=headers)
|
||||
)
|
||||
if result["ocs"]["meta"]["statuscode"] == 100:
|
||||
return True
|
||||
if result["ocs"]["meta"]["statuscode"] == 102:
|
||||
raise ProviderItemExists
|
||||
raise ProviderOpError
|
||||
except:
|
||||
# log.error(traceback.format_exc())
|
||||
raise
|
||||
# 100 - successful
|
||||
# 101 - invalid input data
|
||||
# 102 - group already exists
|
||||
# 103 - failed to add the group
|
||||
|
||||
def delete_group(self, groupid):
|
||||
group = urllib.parse.quote(groupid, safe="")
|
||||
url = self.apiurl + "groups/" + group + "?format=json"
|
||||
headers = {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"OCS-APIRequest": "true",
|
||||
}
|
||||
try:
|
||||
result = json.loads(
|
||||
self._request("DELETE", url, auth=self.auth, headers=headers)
|
||||
)
|
||||
if result["ocs"]["meta"]["statuscode"] == 100:
|
||||
return True
|
||||
# log.error(traceback.format_exc())
|
||||
raise ProviderOpError
|
||||
except:
|
||||
# log.error(traceback.format_exc())
|
||||
raise
|
||||
# 100 - successful
|
||||
# 101 - invalid input data
|
||||
# 102 - group already exists
|
||||
# 103 - failed to add the group
|
||||
|
||||
def set_user_mail(self, data):
|
||||
query = """SELECT * FROM "oc_mail_accounts" WHERE "email" = '%s'"""
|
||||
sql_query = sql.SQL(query.format(data["email"]))
|
||||
if not len(self.nextcloud_pg.select(sql_query)):
|
||||
query = """INSERT INTO "oc_mail_accounts" ("user_id","name","email","inbound_host","inbound_port","inbound_ssl_mode","inbound_user","inbound_password","outbound_host","outbound_port","outbound_ssl_mode","outbound_user","outbound_password") VALUES
|
||||
('%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s');"""
|
||||
account = [
|
||||
data["user_id"],
|
||||
data["name"],
|
||||
data["email"],
|
||||
data["inbound_host"],
|
||||
data["inbound_port"],
|
||||
data["inbound_ssl_mode"],
|
||||
data["inbound_user"],
|
||||
data["inbound_password"],
|
||||
data["outbound_host"],
|
||||
data["outbound_port"],
|
||||
data["outbound_ssl_mode"],
|
||||
data["outbound_user"],
|
||||
data["outbound_password"],
|
||||
]
|
||||
else:
|
||||
query = """UPDATE "oc_mail_accounts" SET ("user_id","name","email","inbound_host","inbound_port","inbound_ssl_mode","inbound_user","inbound_password","outbound_host","outbound_port","outbound_ssl_mode","outbound_user","outbound_password") =
|
||||
('%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s') WHERE email = '%s';"""
|
||||
|
||||
account = [
|
||||
data["user_id"],
|
||||
data["name"],
|
||||
data["email"],
|
||||
data["inbound_host"],
|
||||
data["inbound_port"],
|
||||
data["inbound_ssl_mode"],
|
||||
data["inbound_user"],
|
||||
data["inbound_password"],
|
||||
data["outbound_host"],
|
||||
data["outbound_port"],
|
||||
data["outbound_ssl_mode"],
|
||||
data["outbound_user"],
|
||||
data["outbound_password"],
|
||||
data["email"],
|
||||
]
|
||||
sql_query = sql.SQL(query.format(",".join([str(acc) for acc in account])))
|
||||
self.nextcloud_pg.update(sql_query)
|
|
@ -0,0 +1,40 @@
|
|||
#!/usr/bin/env python
|
||||
# coding=utf-8
|
||||
class ProviderUnauthorized(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ProviderConnError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ProviderSslError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ProviderConnTimeout(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ProviderError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ProviderItemExists(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ProviderItemNotExists(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ProviderGroupNotExists(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ProviderFolderNotExists(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ProviderOpError(Exception):
|
||||
pass
|
|
@ -0,0 +1,53 @@
|
|||
#!/usr/bin/env python
|
||||
# coding=utf-8
|
||||
import json
|
||||
import logging as log
|
||||
import time
|
||||
import traceback
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import psycopg2
|
||||
import yaml
|
||||
|
||||
# from admin import app
|
||||
|
||||
|
||||
class Postgres:
|
||||
def __init__(self, host, database, user, password):
|
||||
self.conn = psycopg2.connect(
|
||||
host=host, database=database, user=user, password=password
|
||||
)
|
||||
|
||||
# def __del__(self):
|
||||
# self.cur.close()
|
||||
# self.conn.close()
|
||||
|
||||
def select(self, sql):
|
||||
self.cur = self.conn.cursor()
|
||||
self.cur.execute(sql)
|
||||
data = self.cur.fetchall()
|
||||
self.cur.close()
|
||||
return data
|
||||
|
||||
def update(self, sql):
|
||||
self.cur = self.conn.cursor()
|
||||
self.cur.execute(sql)
|
||||
self.conn.commit()
|
||||
self.cur.close()
|
||||
# return self.cur.fetchall()
|
||||
|
||||
def select_with_headers(self, sql):
|
||||
self.cur = self.conn.cursor()
|
||||
self.cur.execute(sql)
|
||||
data = self.cur.fetchall()
|
||||
fields = [a.name for a in self.cur.description]
|
||||
self.cur.close()
|
||||
return (fields, data)
|
||||
|
||||
# def update_moodle_saml_plugin(self):
|
||||
# plugin[('idpmetadata', '<md:EntitiesDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Name="urn:keycloak"><md:EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" entityID="https://sso.'+app.config['DOMAIN']+'/auth/realms/master"><md:IDPSSODescriptor WantAuthnRequestsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"><md:KeyDescriptor use="signing"><ds:KeyInfo><ds:KeyName>NrtA5ynG0htowP3SXw7dBJRIAMxn-1PwuuXwOwNhlRw</ds:KeyName><ds:X509Data><ds:X509Certificate>MIICmzCCAYMCBgF5jb0RCTANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjEwNTIxMDcwMjI4WhcNMzEwNTIxMDcwNDA4WjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCI8xh/C0+frz3kgWiUbziTDls71R2YiXLSVE+bw7gbEgZUGCLhoEI679azMtIxmnzM/snIX+yTb12+XoYkgbiLTMPQfnH+Kiab6g3HL3KPfhqS+yWkFxOoCp6Ibmp7yPlVWuHH+MBfO8OBr/r8Ao7heFbuzjiLd1KG67rcoaxfDgMuBoEomg1bgEjFgHaQIrSC6OZzH0h987/arqufZXeXlfyiqScMPUi+u5IpDWSwz06UKP0k8mxzNSlpZ93CKOUSsV0SMLxqg7FQ3SGiOk577bGW9o9BDTkkmSo3Up6smc0LzwvvUwuNd0B1irGkWZFQN9OXJnJYf1InEebIMtmPAgMBAAEwDQYJKoZIhvcNAQELBQADggEBADM34+qEGeBQ22luphVTuVJtGxcbxLx7DfsT0QfJD/OuxTTbNAa1VRyarb5juIAkqdj4y2quZna9ZXLecVo4RkwpzPoKoAkYA8b+kHnWqEwJi9iPrDvKb+GR0bBkLPN49YxIZ8IdKX/PRa3yuLHe+loiNsCaS/2ZK2KO46COsqU4QX1iVhF9kWphNLybjNAX45B6cJLsa1g0vXLdm3kv3SB4I2fErFVaOoDtFIjttoYlXdpUiThkPXBfr7N67P3dZHaS4tjJh+IZ8I6TINpcsH8dBkUhzYEIPHCePwSiC1w6WDBLNDuKt1mj1CZrLq+1x+Yhrs+QNRheEKGi89HZ8N0=</ds:X509Certificate></ds:X509Data></ds:KeyInfo></md:KeyDescriptor><md:ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://sso.mydomain.duckdns.org/auth/realms/master/protocol/saml/resolve" index="0"/><md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://sso.mydomain.duckdns.org/auth/realms/master/protocol/saml"/><md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://sso.mydomain.duckdns.org/auth/realms/master/protocol/saml"/><md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://sso.mydomain.duckdns.org/auth/realms/master/protocol/saml"/><md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</md:NameIDFormat><md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat><md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat><md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://sso.mydomain.duckdns.org/auth/realms/master/protocol/saml"/><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://sso.mydomain.duckdns.org/auth/realms/master/protocol/saml"/><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://sso.mydomain.duckdns.org/auth/realms/master/protocol/saml"/><md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://sso.mydomain.duckdns.org/auth/realms/master/protocol/saml"/></md:IDPSSODescriptor></md:EntityDescriptor></md:EntitiesDescriptor>')]
|
||||
# pg_update = """UPDATE mdl_config_plugins set title = %s where plugin = auth_saml2 and name ="""
|
||||
# cursor.execute(pg_update, (title, bookid))
|
||||
# connection.commit()
|
||||
# count = cursor.rowcount
|
||||
# print(count, "Successfully Updated!")
|
|
@ -0,0 +1,210 @@
|
|||
#!/usr/bin/env python
|
||||
# coding=utf-8
|
||||
import json
|
||||
import logging as log
|
||||
import os
|
||||
import random
|
||||
|
||||
# from .keycloak import Keycloak
|
||||
# from .moodle import Moodle
|
||||
import string
|
||||
import time
|
||||
import traceback
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import psycopg2
|
||||
import yaml
|
||||
|
||||
from admin import app
|
||||
|
||||
from .postgres import Postgres
|
||||
|
||||
|
||||
class Postup:
|
||||
def __init__(self):
|
||||
ready = False
|
||||
while not ready:
|
||||
try:
|
||||
self.pg = Postgres(
|
||||
"isard-apps-postgresql",
|
||||
"moodle",
|
||||
app.config["MOODLE_POSTGRES_USER"],
|
||||
app.config["MOODLE_POSTGRES_PASSWORD"],
|
||||
)
|
||||
ready = True
|
||||
except:
|
||||
log.warning("Could not connect to moodle database. Retrying...")
|
||||
time.sleep(2)
|
||||
log.info("Connected to moodle database.")
|
||||
|
||||
ready = False
|
||||
while not ready:
|
||||
try:
|
||||
with open(
|
||||
os.path.join(
|
||||
app.root_path,
|
||||
"../moodledata/saml2/moodle." + app.config["DOMAIN"] + ".crt",
|
||||
),
|
||||
"r",
|
||||
) as crt:
|
||||
app.config.setdefault("SP_CRT", crt.read())
|
||||
ready = True
|
||||
except IOError:
|
||||
log.warning("Could not get moodle SAML2 crt certificate. Retrying...")
|
||||
time.sleep(2)
|
||||
except:
|
||||
log.error(traceback.format_exc())
|
||||
log.info("Got moodle srt certificate.")
|
||||
|
||||
ready = False
|
||||
while not ready:
|
||||
try:
|
||||
with open(
|
||||
os.path.join(
|
||||
app.root_path,
|
||||
"../moodledata/saml2/moodle." + app.config["DOMAIN"] + ".pem",
|
||||
),
|
||||
"r",
|
||||
) as pem:
|
||||
app.config.setdefault("SP_PEM", pem.read())
|
||||
ready = True
|
||||
except IOError:
|
||||
log.warning("Could not get moodle SAML2 pem certificate. Retrying...")
|
||||
time.sleep(2)
|
||||
log.info("Got moodle pem certificate.")
|
||||
|
||||
self.select_and_configure_theme()
|
||||
self.configure_tipnc()
|
||||
self.add_moodle_ws_token()
|
||||
|
||||
def select_and_configure_theme(self, theme="cbe"):
|
||||
try:
|
||||
self.pg.update(
|
||||
"""UPDATE "mdl_config" SET value = '%s' WHERE "name" = 'theme';"""
|
||||
% (theme)
|
||||
)
|
||||
except:
|
||||
log.error(traceback.format_exc())
|
||||
exit(1)
|
||||
None
|
||||
|
||||
try:
|
||||
self.pg.update(
|
||||
"""UPDATE "mdl_config_plugins" SET value = '%s' WHERE "plugin" = 'theme_cbe' AND "name" = 'host';"""
|
||||
% (os.environ["DOMAIN"])
|
||||
)
|
||||
self.pg.update(
|
||||
"""UPDATE "mdl_config_plugins" SET value = '%s' WHERE "plugin" = 'theme_cbe' AND "name" = 'logourl';"""
|
||||
% ("https://api." + os.environ["DOMAIN"] + "/img/logo.png")
|
||||
)
|
||||
self.pg.update(
|
||||
"""UPDATE "mdl_config_plugins" SET value = '1' WHERE "plugin" = 'theme_cbe' AND "name" = 'header_api';"""
|
||||
)
|
||||
self.pg.update(
|
||||
"""UPDATE "mdl_config_plugins" SET value = '1' WHERE "plugin" = 'theme_cbe' AND "name" = 'vclasses_direct';"""
|
||||
)
|
||||
self.pg.update(
|
||||
"""UPDATE "mdl_config_plugins" SET value = '1' WHERE "plugin" = 'theme_cbe' AND "name" = 'uniquenamecourse';"""
|
||||
)
|
||||
except:
|
||||
log.error(traceback.format_exc())
|
||||
exit(1)
|
||||
None
|
||||
|
||||
def configure_tipnc(self):
|
||||
try:
|
||||
self.pg.update(
|
||||
"""UPDATE "mdl_config_plugins" SET value = '%s' WHERE "plugin" = 'assignsubmission_tipnc' AND "name" = 'host';"""
|
||||
% ("https://nextcloud." + os.environ["DOMAIN"] + "/")
|
||||
)
|
||||
self.pg.update(
|
||||
"""UPDATE "mdl_config_plugins" SET value = '%s' WHERE "plugin" = 'assignsubmission_tipnc' AND "name" = 'password';"""
|
||||
% (os.environ["NEXTCLOUD_ADMIN_PASSWORD"])
|
||||
)
|
||||
self.pg.update(
|
||||
"""UPDATE "mdl_config_plugins" SET value = 'template.docx' WHERE "plugin" = 'assignsubmission_tipnc' AND "name" = 'template';"""
|
||||
)
|
||||
self.pg.update(
|
||||
"""UPDATE "mdl_config_plugins" SET value = '/apps/onlyoffice/' WHERE "plugin" = 'assignsubmission_tipnc' AND "name" = 'location';"""
|
||||
)
|
||||
self.pg.update(
|
||||
"""UPDATE "mdl_config_plugins" SET value = '%s' WHERE "plugin" = 'assignsubmission_tipnc' AND "name" = 'user';"""
|
||||
% (os.environ["NEXTCLOUD_ADMIN_USER"])
|
||||
)
|
||||
self.pg.update(
|
||||
"""UPDATE "mdl_config_plugins" SET value = 'tasks' WHERE "plugin" = 'assignsubmission_tipnc' AND "name" = 'folder';"""
|
||||
)
|
||||
except:
|
||||
log.error(traceback.format_exc())
|
||||
exit(1)
|
||||
None
|
||||
|
||||
def add_moodle_ws_token(self):
|
||||
try:
|
||||
token = self.pg.select(
|
||||
"""SELECT * FROM "mdl_external_tokens" WHERE "externalserviceid" = 3"""
|
||||
)[0][1]
|
||||
app.config.setdefault("MOODLE_WS_TOKEN", token)
|
||||
return
|
||||
except:
|
||||
# log.error(traceback.format_exc())
|
||||
None
|
||||
|
||||
try:
|
||||
self.pg.update(
|
||||
"""INSERT INTO "mdl_external_services" ("name", "enabled", "requiredcapability", "restrictedusers", "component", "timecreated", "timemodified", "shortname", "downloadfiles", "uploadfiles") VALUES
|
||||
('dd admin', 1, '', 1, NULL, 1621719763, 1621719850, 'dd_admin', 0, 0);"""
|
||||
)
|
||||
|
||||
self.pg.update(
|
||||
"""INSERT INTO "mdl_external_services_functions" ("externalserviceid", "functionname") VALUES
|
||||
(3, 'core_course_update_courses'),
|
||||
(3, 'core_user_get_users'),
|
||||
(3, 'core_user_get_users_by_field'),
|
||||
(3, 'core_user_update_picture'),
|
||||
(3, 'core_user_update_users'),
|
||||
(3, 'core_user_delete_users'),
|
||||
(3, 'core_user_create_users'),
|
||||
(3, 'core_cohort_get_cohort_members'),
|
||||
(3, 'core_cohort_add_cohort_members'),
|
||||
(3, 'core_cohort_delete_cohort_members'),
|
||||
(3, 'core_cohort_create_cohorts'),
|
||||
(3, 'core_cohort_delete_cohorts'),
|
||||
(3, 'core_cohort_search_cohorts'),
|
||||
(3, 'core_cohort_update_cohorts'),
|
||||
(3, 'core_role_assign_roles'),
|
||||
(3, 'core_cohort_get_cohorts');"""
|
||||
)
|
||||
|
||||
self.pg.update(
|
||||
"""INSERT INTO "mdl_external_services_users" ("externalserviceid", "userid", "iprestriction", "validuntil", "timecreated") VALUES
|
||||
(3, 2, NULL, NULL, 1621719871);"""
|
||||
)
|
||||
|
||||
b32 = "".join(
|
||||
random.choices(
|
||||
string.ascii_uppercase
|
||||
+ string.ascii_uppercase
|
||||
+ string.ascii_lowercase,
|
||||
k=32,
|
||||
)
|
||||
)
|
||||
b64 = "".join(
|
||||
random.choices(
|
||||
string.ascii_uppercase
|
||||
+ string.ascii_uppercase
|
||||
+ string.ascii_lowercase,
|
||||
k=64,
|
||||
)
|
||||
)
|
||||
self.pg.update(
|
||||
"""INSERT INTO "mdl_external_tokens" ("token", "privatetoken", "tokentype", "userid", "externalserviceid", "sid", "contextid", "creatorid", "iprestriction", "validuntil", "timecreated", "lastaccess") VALUES
|
||||
('%s', '%s', 0, 2, 3, NULL, 1, 2, NULL, 0, 1621831206, NULL);"""
|
||||
% (b32, b64)
|
||||
)
|
||||
|
||||
app.config.setdefault("MOODLE_WS_TOKEN", b32)
|
||||
except:
|
||||
log.error(traceback.format_exc())
|
||||
exit(1)
|
||||
None
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"font-linux": "^0.6.1",
|
||||
"gentelella": "^1.4.0",
|
||||
"socket.io": "^4.1.3"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
name:
|
||||
required: true
|
||||
type: string
|
||||
description:
|
||||
required: false
|
||||
type: string
|
||||
default: "Api created"
|
||||
parent:
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
|
@ -0,0 +1,34 @@
|
|||
user_id:
|
||||
type: string
|
||||
required: true
|
||||
name:
|
||||
type: string
|
||||
required: false
|
||||
email:
|
||||
type: string
|
||||
required: true
|
||||
regex: ^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$
|
||||
inbound_host:
|
||||
type: string
|
||||
required: true
|
||||
inbound_port:
|
||||
type: integer
|
||||
required: true
|
||||
inbound_ssl_mode:
|
||||
type: string
|
||||
default: ssl
|
||||
inbound_user:
|
||||
type: string
|
||||
required: true
|
||||
outbound_host:
|
||||
type: string
|
||||
required: true
|
||||
outbound_port:
|
||||
type: integer
|
||||
required: true
|
||||
outbound_ssl_mode:
|
||||
type: string
|
||||
default: ssl
|
||||
outbound_user:
|
||||
type: string
|
||||
required: true
|
|
@ -0,0 +1,37 @@
|
|||
mails:
|
||||
type: list
|
||||
schema:
|
||||
user_id:
|
||||
type: string
|
||||
required: true
|
||||
name:
|
||||
type: string
|
||||
required: false
|
||||
email:
|
||||
type: string
|
||||
required: true
|
||||
regex: ^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$
|
||||
inbound_host:
|
||||
type: string
|
||||
required: true
|
||||
inbound_port:
|
||||
type: integer
|
||||
required: true
|
||||
inbound_ssl_mode:
|
||||
type: string
|
||||
default: ssl
|
||||
inbound_user:
|
||||
type: string
|
||||
required: true
|
||||
outbound_host:
|
||||
type: string
|
||||
required: true
|
||||
outbound_port:
|
||||
type: integer
|
||||
required: true
|
||||
outbound_ssl_mode:
|
||||
type: string
|
||||
default: ssl
|
||||
outbound_user:
|
||||
type: string
|
||||
required: true
|
|
@ -0,0 +1,34 @@
|
|||
username:
|
||||
required: true
|
||||
type: string
|
||||
first:
|
||||
required: true
|
||||
type: string
|
||||
last:
|
||||
required: true
|
||||
type: string
|
||||
email:
|
||||
required: true
|
||||
type: string
|
||||
regex: ^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$
|
||||
password:
|
||||
required: true
|
||||
type: string
|
||||
password_temporary:
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
quota:
|
||||
required: true
|
||||
type: string
|
||||
enabled:
|
||||
required: true
|
||||
type: boolean
|
||||
role:
|
||||
required: true
|
||||
type: string
|
||||
empty: false
|
||||
groups:
|
||||
required: true
|
||||
type: list
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
first:
|
||||
required: false
|
||||
type: string
|
||||
last:
|
||||
required: false
|
||||
type: string
|
||||
email:
|
||||
required: false
|
||||
type: string
|
||||
regex: ^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$
|
||||
password:
|
||||
required: false
|
||||
type: string
|
||||
password_temporary:
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
quota:
|
||||
required: false
|
||||
type: string
|
||||
enabled:
|
||||
required: false
|
||||
type: boolean
|
||||
role:
|
||||
required: false
|
||||
type: string
|
||||
empty: false
|
||||
groups:
|
||||
required: false
|
||||
type: list
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
body {
|
||||
color: #3b3e47;
|
||||
background: #3b3e47
|
||||
}
|
||||
|
||||
.dataTables_filter {float: left; position: absolute;}
|
||||
.dataTables_filter input { max-width:90px;}
|
||||
|
||||
.roundbox{border-radius:4px;border:1px solid #AAAAAA;}
|
||||
|
||||
.blink {
|
||||
animation: blink 2s steps(5, start) infinite;
|
||||
-webkit-animation: blink 1s steps(5, start) infinite;
|
||||
}
|
||||
@keyframes blink {
|
||||
to {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes blink {
|
||||
to {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.pnotify-center {
|
||||
right: calc(50% - 150px) !important;
|
||||
}
|
||||
|
||||
.ui-select-match-text{
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding-right: 40px;
|
||||
}
|
||||
.ui-select-toggle > .btn.btn-link {
|
||||
margin-right: 10px;
|
||||
top: 6px;
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
|
||||
.fancytree-plain` span.fancytree-selected span.fancytree-title {
|
||||
background-color: yellow;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.fancytree-plain span.fancytree-active span.fancytree-title {
|
||||
background-color: blue;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.quota-form-input {
|
||||
width: 100% !important;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/*
|
||||
* Workaround to fix select2 placeholder cut off
|
||||
* https://github.com/select2/select2/issues/291
|
||||
* https://github.com/kartik-v/yii2-widgets/issues/324
|
||||
*/
|
||||
.select2-search, .select2-search__field {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
table.dataTable td.details-show > button > i:before,
|
||||
table.dataTable td.details-control > button > i:before {
|
||||
content: '\f067';
|
||||
font-family: FontAwesome;
|
||||
cursor: pointer;
|
||||
color: white;
|
||||
}
|
||||
|
||||
table.dataTable tr.shown td.details-show > button > i:before,
|
||||
table.dataTable tr.shown td.details-control > button > i:before {
|
||||
content: '\f068';
|
||||
color: white;
|
||||
}
|
||||
|
||||
.howto-desktops {
|
||||
background-color:rgb(238, 238, 238);
|
||||
cursor: pointer;
|
||||
padding: 5px 17px;
|
||||
}
|
||||
|
||||
.x_title h4, h3 {
|
||||
margin: 5px 0 6px;
|
||||
float: left;
|
||||
display: block;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.x_panel {
|
||||
border: none
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
|
||||
.sidebar-footer {
|
||||
background: #3b3e47;
|
||||
}
|
||||
|
||||
#menu_toggle {
|
||||
color: #3b3e47;
|
||||
}
|
||||
|
||||
.logo_white {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
.left_col {
|
||||
background: #3b3e47;
|
||||
}
|
||||
|
||||
.nav.side-menu>li.active>a {
|
||||
background: #3b3e47;
|
||||
}
|
||||
|
||||
.nav_title {
|
||||
background: #3b3e47;
|
||||
}
|
||||
|
||||
.nav_menu {
|
||||
padding: 10px;
|
||||
background: white;
|
||||
height: 75px;
|
||||
max-height: 75px;
|
||||
}
|
||||
|
||||
.nav_menu_logo {
|
||||
margin-top: 5px;
|
||||
width: 100;
|
||||
height: 45px;
|
||||
}
|
|
@ -0,0 +1,354 @@
|
|||
/*
|
||||
* Copyright 2017 the Isard-vdi project authors:
|
||||
* Josep Maria Viñolas Auquer
|
||||
* Alberto Larraz Dalmases
|
||||
* License: AGPLv3
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resize function without multiple trigger
|
||||
*
|
||||
* Usage:
|
||||
* $(window).smartresize(function(){
|
||||
* // code here
|
||||
* });
|
||||
*/
|
||||
(function($,sr){
|
||||
// debouncing function from John Hann
|
||||
// http://unscriptable.com/index.php/2009/03/20/debouncing-javascript-methods/
|
||||
var debounce = function (func, threshold, execAsap) {
|
||||
var timeout;
|
||||
|
||||
return function debounced () {
|
||||
var obj = this, args = arguments;
|
||||
function delayed () {
|
||||
if (!execAsap)
|
||||
func.apply(obj, args);
|
||||
timeout = null;
|
||||
}
|
||||
|
||||
if (timeout)
|
||||
clearTimeout(timeout);
|
||||
else if (execAsap)
|
||||
func.apply(obj, args);
|
||||
|
||||
timeout = setTimeout(delayed, threshold || 100);
|
||||
};
|
||||
};
|
||||
|
||||
// smartresize
|
||||
jQuery.fn[sr] = function(fn){ return fn ? this.bind('resize', debounce(fn)) : this.trigger(sr); };
|
||||
|
||||
})(jQuery,'smartresize');
|
||||
/**
|
||||
* To change this license header, choose License Headers in Project Properties.
|
||||
* To change this template file, choose Tools | Templates
|
||||
* and open the template in the editor.
|
||||
*/
|
||||
|
||||
// Validator.js
|
||||
// initialize the validator function
|
||||
validator.message.date = 'not a real date';
|
||||
|
||||
// validate a field on "blur" event, a 'select' on 'change' event & a '.reuired' classed multifield on 'keyup':
|
||||
$('form')
|
||||
.on('blur', 'input[required], input.optional, select.required', validator.checkField)
|
||||
.on('change', 'select.required', validator.checkField)
|
||||
.on('keypress', 'input[required][pattern]', validator.keypress);
|
||||
//~ .on('keypress', 'input[required][pattern]', function(){console.log('press')});
|
||||
|
||||
$('.multi.required').on('keyup blur', 'input', function() {
|
||||
validator.checkField.apply($(this).siblings().last()[0]);
|
||||
});
|
||||
|
||||
$('form').submit(function(e) {
|
||||
e.preventDefault();
|
||||
var submit = true;
|
||||
|
||||
// evaluate the form using generic validaing
|
||||
if (!validator.checkAll($(this))) {
|
||||
submit = false;
|
||||
}
|
||||
|
||||
if (submit)
|
||||
this.submit();
|
||||
|
||||
return false;
|
||||
});
|
||||
// /Validator.js
|
||||
|
||||
//PNotify
|
||||
var stack_center = {"dir1": "down", "dir2": "right", "firstpos1": 25, "firstpos2": ($(window).width() / 2) - (Number(PNotify.prototype.options.width.replace(/\D/g, '')) / 2)};
|
||||
$(window).resize(function(){
|
||||
stack_center.firstpos2 = ($(window).width() / 2) - (Number(PNotify.prototype.options.width.replace(/\D/g, '')) / 2);
|
||||
});
|
||||
PNotify.prototype.options.styling = "bootstrap3";
|
||||
// /PNotify
|
||||
|
||||
// Sidebar
|
||||
var CURRENT_URL = window.location.href.split('#')[0].split('?')[0],
|
||||
$BODY = $('body'),
|
||||
$MENU_TOGGLE = $('#menu_toggle'),
|
||||
$SIDEBAR_MENU = $('#sidebar-menu'),
|
||||
$SIDEBAR_FOOTER = $('.sidebar-footer'),
|
||||
$LEFT_COL = $('.left_col'),
|
||||
$RIGHT_COL = $('.right_col'),
|
||||
$NAV_MENU = $('.nav_menu'),
|
||||
$FOOTER = $('footer');
|
||||
|
||||
|
||||
function init_sidebar() {
|
||||
// TODO: This is some kind of easy fix, maybe we can improve this
|
||||
var setContentHeight = function () {
|
||||
// reset height
|
||||
$RIGHT_COL.css('min-height', $(window).height());
|
||||
|
||||
var bodyHeight = $BODY.outerHeight(),
|
||||
footerHeight = $BODY.hasClass('footer_fixed') ? -10 : $FOOTER.height(),
|
||||
leftColHeight = $LEFT_COL.eq(1).height() + $SIDEBAR_FOOTER.height(),
|
||||
contentHeight = bodyHeight < leftColHeight ? leftColHeight : bodyHeight;
|
||||
|
||||
// normalize content
|
||||
contentHeight -= $NAV_MENU.height() + footerHeight;
|
||||
|
||||
$RIGHT_COL.css('min-height', contentHeight);
|
||||
};
|
||||
|
||||
$SIDEBAR_MENU.find('a').on('click', function(ev) {
|
||||
var $li = $(this).parent();
|
||||
|
||||
if ($li.is('.active')) {
|
||||
$li.removeClass('active active-sm');
|
||||
$('ul:first', $li).slideUp(function() {
|
||||
setContentHeight();
|
||||
});
|
||||
} else {
|
||||
// prevent closing menu if we are on child menu
|
||||
if (!$li.parent().is('.child_menu')) {
|
||||
$SIDEBAR_MENU.find('li').removeClass('active active-sm');
|
||||
$SIDEBAR_MENU.find('li ul').slideUp();
|
||||
}else
|
||||
{
|
||||
if ( $BODY.is( ".nav-sm" ) )
|
||||
{
|
||||
$SIDEBAR_MENU.find( "li" ).removeClass( "active active-sm" );
|
||||
$SIDEBAR_MENU.find( "li ul" ).slideUp();
|
||||
}
|
||||
}
|
||||
$li.addClass('active');
|
||||
|
||||
$('ul:first', $li).slideDown(function() {
|
||||
setContentHeight();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// toggle small or large menu
|
||||
$MENU_TOGGLE.on('click', function() {
|
||||
if ($BODY.hasClass('nav-md')) {
|
||||
$SIDEBAR_MENU.find('li.active ul').hide();
|
||||
$SIDEBAR_MENU.find('li.active').addClass('active-sm').removeClass('active');
|
||||
} else {
|
||||
$SIDEBAR_MENU.find('li.active-sm ul').show();
|
||||
$SIDEBAR_MENU.find('li.active-sm').addClass('active').removeClass('active-sm');
|
||||
}
|
||||
|
||||
$BODY.toggleClass('nav-md nav-sm');
|
||||
|
||||
setContentHeight();
|
||||
});
|
||||
|
||||
// check active menu
|
||||
$SIDEBAR_MENU.find('a[href="' + CURRENT_URL + '"]').parent('li').addClass('current-page');
|
||||
|
||||
$SIDEBAR_MENU.find('a').filter(function () {
|
||||
return this.href == CURRENT_URL;
|
||||
}).parent('li').addClass('current-page').parents('ul').slideDown(function() {
|
||||
setContentHeight();
|
||||
}).parent().addClass('active');
|
||||
|
||||
// recompute content when resizing
|
||||
$(window).smartresize(function(){
|
||||
setContentHeight();
|
||||
});
|
||||
|
||||
setContentHeight();
|
||||
|
||||
// fixed sidebar
|
||||
if ($.fn.mCustomScrollbar) {
|
||||
$('.menu_fixed').mCustomScrollbar({
|
||||
autoHideScrollbar: true,
|
||||
theme: 'minimal',
|
||||
mouseWheel:{ preventDefault: true }
|
||||
});
|
||||
}
|
||||
};
|
||||
// /Sidebar
|
||||
|
||||
$(document).ready(function() {
|
||||
init_sidebar();
|
||||
$('input').iCheck({
|
||||
checkboxClass: 'icheckbox_flat-green',
|
||||
radioClass: 'iradio_flat-green',
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
// Form serialization
|
||||
|
||||
(function($){
|
||||
$.fn.serializeObject = function(){
|
||||
var self = this,
|
||||
json = {},
|
||||
push_counters = {},
|
||||
patterns = {
|
||||
"validate": /^[a-z][a-z0-9_-]*(?:\[(?:\d*|[a-z0-9_-]+)\])*$/i,
|
||||
"key": /[a-z0-9_-]+|(?=\[\])/gi,
|
||||
"named": /^[a-z0-9_-]+$/i,
|
||||
//~ "validate": /^[a-zA-Z][a-zA-Z0-9_]*(?:\[(?:\d*|[a-zA-Z0-9_]+)\])*$/,
|
||||
//~ "key": /[a-zA-Z0-9_]+|(?=\[\])/g,
|
||||
"push": /^$/,
|
||||
"fixed": /^\d+$/,
|
||||
//~ "named": /^[a-zA-Z0-9_]+$/
|
||||
};
|
||||
|
||||
|
||||
this.build = function(base, key, value){
|
||||
base[key] = value;
|
||||
return base;
|
||||
};
|
||||
|
||||
this.push_counter = function(key){
|
||||
if(push_counters[key] === undefined){
|
||||
push_counters[key] = 0;
|
||||
}
|
||||
return push_counters[key]++;
|
||||
};
|
||||
|
||||
$.each($(this).serializeArray(), function(){
|
||||
|
||||
// skip invalid keys
|
||||
if(!patterns.validate.test(this.name)){
|
||||
return;
|
||||
}
|
||||
|
||||
var k,
|
||||
keys = this.name.match(patterns.key),
|
||||
merge = this.value,
|
||||
reverse_key = this.name;
|
||||
|
||||
while((k = keys.pop()) !== undefined){
|
||||
|
||||
// adjust reverse_key
|
||||
reverse_key = reverse_key.replace(new RegExp("\\[" + k + "\\]$"), '');
|
||||
|
||||
// push
|
||||
if(k.match(patterns.push)){
|
||||
merge = self.build([], self.push_counter(reverse_key), merge);
|
||||
}
|
||||
|
||||
// fixed
|
||||
else if(k.match(patterns.fixed)){
|
||||
merge = self.build([], k, merge);
|
||||
}
|
||||
|
||||
// named
|
||||
else if(k.match(patterns.named)){
|
||||
merge = self.build({}, k, merge);
|
||||
}
|
||||
}
|
||||
|
||||
json = $.extend(true, json, merge);
|
||||
});
|
||||
|
||||
return json;
|
||||
};
|
||||
})(jQuery);
|
||||
|
||||
|
||||
function dtUpdateInsertoLD(table, data, append){
|
||||
//Quickly appends new data rows. Does not update rows
|
||||
if(append == true){
|
||||
table.rows.add(data);
|
||||
|
||||
//Locate and update rows by rowId or add if new
|
||||
}else{
|
||||
|
||||
found=false;
|
||||
table.rows().every( function ( rowIdx, tableLoop, rowLoop ) {
|
||||
if(this.data().id==data.id){
|
||||
table.row(rowIdx).data(data).invalidate();
|
||||
found=true;
|
||||
return false; //Break
|
||||
}
|
||||
});
|
||||
if(!found){
|
||||
table.row.add(data);
|
||||
}
|
||||
}
|
||||
|
||||
//Redraw table maintaining paging
|
||||
table.draw(false);
|
||||
}
|
||||
|
||||
function dtUpdateInsert(table, data, append){
|
||||
table=$("#"+table).DataTable()
|
||||
//Quickly appends new data rows. Does not update rows
|
||||
new_id=false
|
||||
if(append == true){
|
||||
table.rows.add(data);
|
||||
new_id=true
|
||||
//Locate and update rows by rowId or add if new
|
||||
}else{
|
||||
if(typeof(table.row('#'+data.id).id())=='undefined'){
|
||||
// Does not exists yes
|
||||
table.row.add(data);
|
||||
new_id=true
|
||||
}else{
|
||||
// Exists, do update
|
||||
table.row('#'+data.id).data(data).invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
//Redraw table maintaining paging
|
||||
table.draw(false);
|
||||
return new_id
|
||||
}
|
||||
|
||||
function dtUpdateOnly(table, data){
|
||||
if(typeof(table.row('#'+data.id).id())=='undefined'){
|
||||
// Does not exists yes
|
||||
}else{
|
||||
// Exists, do update
|
||||
table.row('#'+data.id).data(data).invalidate();
|
||||
}
|
||||
//Redraw table maintaining paging
|
||||
table.draw(false);
|
||||
}
|
||||
|
||||
// Panel toolbox
|
||||
$(document).ready(function() {
|
||||
$('.collapse-link').on('click', function() {
|
||||
var $BOX_PANEL = $(this).closest('.x_panel'),
|
||||
$ICON = $(this).find('i'),
|
||||
$BOX_CONTENT = $BOX_PANEL.find('.x_content');
|
||||
|
||||
// fix for some div with hardcoded fix class
|
||||
if ($BOX_PANEL.attr('style')) {
|
||||
$BOX_CONTENT.slideToggle(200, function(){
|
||||
$BOX_PANEL.removeAttr('style');
|
||||
});
|
||||
} else {
|
||||
$BOX_CONTENT.slideToggle(200);
|
||||
$BOX_PANEL.css('height', 'auto');
|
||||
}
|
||||
|
||||
$ICON.toggleClass('fa-chevron-up fa-chevron-down');
|
||||
});
|
||||
|
||||
$('.close-link').click(function () {
|
||||
var $BOX_PANEL = $(this).closest('.x_panel');
|
||||
|
||||
$BOX_PANEL.remove();
|
||||
});
|
||||
});
|
After Width: | Height: | Size: 1.6 MiB |
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 4.1 KiB |
|
@ -0,0 +1,459 @@
|
|||
|
||||
$(document).ready(function() {
|
||||
|
||||
$('.icon-dropdown').select2({
|
||||
width: "100%",
|
||||
templateSelection: formatText,
|
||||
templateResult: formatText
|
||||
});
|
||||
|
||||
function formatText (icon) {
|
||||
return $('<span><i class="fa ' + $(icon.element).data('icon') + '"></i> ' + icon.text + '</span>');
|
||||
};
|
||||
|
||||
// Update background input when colorpicker is used
|
||||
$('#colorpicker-background').colorpicker().on('changeColor', function (e) {
|
||||
$('#colorpicker-background-input').val(e.color.toHex());
|
||||
});
|
||||
|
||||
// Update background colorpicker when input is used
|
||||
$('#colorpicker-background-input').on('keyup', function (e) {
|
||||
if ($('#colorpicker-background-input').val()[0] == '#' && $('#colorpicker-background-input').val().length == 7) {
|
||||
$('#colorpicker-background').colorpicker('setValue', $('#colorpicker-background-input').val());
|
||||
}
|
||||
})
|
||||
|
||||
// Update primary input when colorpicker is used
|
||||
$('#colorpicker-primary').colorpicker().on('changeColor', function (e) {
|
||||
$('#colorpicker-primary-input').val(e.color.toHex());
|
||||
});
|
||||
|
||||
// Update primary colorpicker when input is used
|
||||
$('#colorpicker-primary-input').on('keyup', function (e) {
|
||||
if ($('#colorpicker-primary-input').val()[0] == '#' && $('#colorpicker-primary-input').val().length == 7) {
|
||||
$('#colorpicker-primary').colorpicker('setValue', $('#colorpicker-primary-input').val());
|
||||
}
|
||||
})
|
||||
|
||||
// Update secondary input when colorpicker is used
|
||||
$('#colorpicker-secondary').colorpicker().on('changeColor', function (e) {
|
||||
$('#colorpicker-secondary-input').val(e.color.toHex());
|
||||
});
|
||||
|
||||
// Update primary colorpicker when input is used
|
||||
$('#colorpicker-secondary-input').on('keyup', function (e) {
|
||||
if ($('#colorpicker-secondary-input').val()[0] == '#' && $('#colorpicker-secondary-input').val().length == 7) {
|
||||
$('#colorpicker-secondary').colorpicker('setValue', $('#colorpicker-secondary-input').val());
|
||||
}
|
||||
})
|
||||
|
||||
init_logo_cropper()
|
||||
init_background_cropper()
|
||||
|
||||
$('#save-colors').click(function () {
|
||||
// console.log({
|
||||
// 'background': $('#colorpicker-background-input').val(),
|
||||
// 'primary': $('#colorpicker-primary-input').val(),
|
||||
// 'secondary': $('#colorpicker-secondary-input').val()
|
||||
// })
|
||||
$.ajax({
|
||||
type: "PUT",
|
||||
url:"/api/dashboard/colours",
|
||||
data: JSON.stringify({
|
||||
'background': $('#colorpicker-background-input').val(),
|
||||
'primary': $('#colorpicker-primary-input').val(),
|
||||
'secondary': $('#colorpicker-secondary-input').val()
|
||||
}),
|
||||
success: function(data)
|
||||
{
|
||||
$('#colorpicker-background').attr('data-container', data.colours.background);
|
||||
$('#colorpicker-background-input').val(data.colours.background);
|
||||
$('#colorpicker-primary').attr('data-container', data.colours.primary);
|
||||
$('#colorpicker-primary-input').val(data.colours.primary);
|
||||
$('#colorpicker-secondary').attr('data-container', data.colours.secondary);
|
||||
$('#colorpicker-secondary-input').val(data.colours.secondary);
|
||||
},
|
||||
error: function(data)
|
||||
{
|
||||
console.log('ERROR!')
|
||||
console.log(data)
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
$('#save-menu').click(function () {
|
||||
ids = $('[id^="apps_external-"]')
|
||||
var menu_options = {};
|
||||
ids.each(function( index ) {
|
||||
// console.log(ids[index].id)
|
||||
if(!(ids[index].id.split('-')[1] in menu_options)){
|
||||
menu_options[ids[index].id.split('-')[1]]={}
|
||||
}
|
||||
menu_options[ids[index].id.split('-')[1]][ids[index].id.split('-')[2]]=$('#'+ids[index].id).val()
|
||||
})
|
||||
$.ajax({
|
||||
type: "PUT",
|
||||
url:"/api/dashboard/menu",
|
||||
data: JSON.stringify(menu_options),
|
||||
success: function(data)
|
||||
{
|
||||
// $('#colorpicker-background').attr('data-container', data.colours.toHex());
|
||||
// $('#colorpicker-background-input').val(data.colours.toHex());
|
||||
// $('#colorpicker-primary').attr('data-container', data.colours.toHex());
|
||||
// $('#colorpicker-primary-input').val(data.colours.toHex());
|
||||
// $('#colorpicker-secondary').attr('data-container', data.colours.toHex());
|
||||
// $('#colorpicker-secondary-input').val(data.colours.toHex());
|
||||
},
|
||||
error: function(data)
|
||||
{
|
||||
console.log('ERROR!')
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
/* CROPPER */
|
||||
|
||||
|
||||
function init_logo_cropper() {
|
||||
|
||||
|
||||
if (typeof ($.fn.cropper) === 'undefined') { return; }
|
||||
// console.log('init_logo_cropper');
|
||||
|
||||
var $image = $('#image_logo');
|
||||
var $dataX = $('#dataX');
|
||||
var $dataY = $('#dataY');
|
||||
var $dataHeight = $('#dataHeight');
|
||||
var $dataWidth = $('#dataWidth');
|
||||
var $dataRotate = $('#dataRotate');
|
||||
var $dataScaleX = $('#dataScaleX');
|
||||
var $dataScaleY = $('#dataScaleY');
|
||||
var cropWidth = 80;
|
||||
var cropHeight = 45;
|
||||
var aspectRatio = cropWidth / cropHeight;
|
||||
var options = {
|
||||
aspectRatio: aspectRatio,
|
||||
preview: '.img-preview-logo',
|
||||
crop: function (e) {
|
||||
$dataX.val(Math.round(e.x));
|
||||
$dataY.val(Math.round(e.y));
|
||||
$dataHeight.val(Math.round(e.height));
|
||||
$dataWidth.val(Math.round(e.width));
|
||||
$dataRotate.val(e.rotate);
|
||||
$dataScaleX.val(e.scaleX);
|
||||
$dataScaleY.val(e.scaleY);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Tooltip
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
|
||||
|
||||
// Cropper
|
||||
$image.on({
|
||||
'build.cropper': function (e) {
|
||||
// console.log(e.type);
|
||||
},
|
||||
'built.cropper': function (e) {
|
||||
// console.log(e.type);
|
||||
},
|
||||
'cropstart.cropper': function (e) {
|
||||
// console.log(e.type, e.action);
|
||||
},
|
||||
'cropmove.cropper': function (e) {
|
||||
// console.log(e.type, e.action);
|
||||
},
|
||||
'cropend.cropper': function (e) {
|
||||
// console.log(e.type, e.action);
|
||||
},
|
||||
'crop.cropper': function (e) {
|
||||
// console.log(e.type, e.x, e.y, e.width, e.height, e.rotate, e.scaleX, e.scaleY);
|
||||
},
|
||||
'zoom.cropper': function (e) {
|
||||
// console.log(e.type, e.ratio);
|
||||
}
|
||||
}).cropper(options);
|
||||
|
||||
$('#save-logo-crop').click(function () {
|
||||
$image.data('cropper').getCroppedCanvas({ width: cropWidth, height: cropHeight }).toBlob(function (blob) {
|
||||
var uri = URL.createObjectURL(blob);
|
||||
var img = new Image();
|
||||
|
||||
img.src = uri;
|
||||
$('.nav_menu_logo').attr('src', img.src)
|
||||
|
||||
var formData = new FormData();
|
||||
formData.append('croppedImage', blob);
|
||||
$.ajax('/api/dashboard/logo', {
|
||||
method: "PUT",
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
success: function () {
|
||||
// Update logo image
|
||||
$('.nav_menu_logo').attr('src', img.src)
|
||||
console.log('Upload success');
|
||||
},
|
||||
error: function () {
|
||||
console.log('Upload error');
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
|
||||
// Methods
|
||||
$('.docs-buttons-logo').on('click', '[data-method]', function () {
|
||||
var $this = $(this);
|
||||
var data = $this.data();
|
||||
var $target;
|
||||
var result;
|
||||
|
||||
if ($this.prop('disabled') || $this.hasClass('disabled')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($image.data('cropper') && data.method) {
|
||||
data = $.extend({}, data); // Clone a new one
|
||||
|
||||
if (typeof data.target !== 'undefined') {
|
||||
$target = $(data.target);
|
||||
|
||||
if (typeof data.option === 'undefined') {
|
||||
try {
|
||||
data.option = JSON.parse($target.val());
|
||||
} catch (e) {
|
||||
console.log(e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result = $image.cropper(data.method, data.option, data.secondOption);
|
||||
|
||||
switch (data.method) {
|
||||
case 'scaleX':
|
||||
case 'scaleY':
|
||||
$(this).data('option', -data.option);
|
||||
break;
|
||||
}
|
||||
|
||||
if ($.isPlainObject(result) && $target) {
|
||||
try {
|
||||
$target.val(JSON.stringify(result));
|
||||
} catch (e) {
|
||||
console.log(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
// Import image
|
||||
var $inputImage = $('#inputImageLogo');
|
||||
var URL = window.URL || window.webkitURL;
|
||||
var blobURL;
|
||||
|
||||
if (URL) {
|
||||
$inputImage.change(function () {
|
||||
var files = this.files;
|
||||
var file;
|
||||
|
||||
if (!$image.data('cropper')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (files && files.length) {
|
||||
file = files[0];
|
||||
|
||||
if (/^image\/\w+$/.test(file.type)) {
|
||||
blobURL = URL.createObjectURL(file);
|
||||
$image.one('built.cropper', function () {
|
||||
|
||||
// Revoke when load complete
|
||||
URL.revokeObjectURL(blobURL);
|
||||
}).cropper('reset').cropper('replace', blobURL);
|
||||
$inputImage.val('');
|
||||
} else {
|
||||
window.alert('Please choose an image file.');
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
$inputImage.prop('disabled', true).parent().addClass('disabled');
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
/* CROPPER --- end */
|
||||
|
||||
function init_background_cropper() {
|
||||
|
||||
|
||||
if (typeof ($.fn.cropper) === 'undefined') { return; }
|
||||
// console.log('init_background_cropper');
|
||||
|
||||
var $image = $('#image_background');
|
||||
var $dataX = $('#dataX');
|
||||
var $dataY = $('#dataY');
|
||||
var $dataHeight = $('#dataHeight');
|
||||
var $dataWidth = $('#dataWidth');
|
||||
var $dataRotate = $('#dataRotate');
|
||||
var $dataScaleX = $('#dataScaleX');
|
||||
var $dataScaleY = $('#dataScaleY');
|
||||
var cropWidth = 1920;
|
||||
var cropHeight = 1080;
|
||||
var aspectRatio = cropWidth / cropHeight;
|
||||
var options = {
|
||||
aspectRatio: aspectRatio,
|
||||
preview: '.img-preview-background',
|
||||
crop: function (e) {
|
||||
$dataX.val(Math.round(e.x));
|
||||
$dataY.val(Math.round(e.y));
|
||||
$dataHeight.val(Math.round(e.height));
|
||||
$dataWidth.val(Math.round(e.width));
|
||||
$dataRotate.val(e.rotate);
|
||||
$dataScaleX.val(e.scaleX);
|
||||
$dataScaleY.val(e.scaleY);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Tooltip
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
|
||||
|
||||
// Cropper
|
||||
$image.on({
|
||||
'build.cropper': function (e) {
|
||||
// console.log(e.type);
|
||||
},
|
||||
'built.cropper': function (e) {
|
||||
// console.log(e.type);
|
||||
},
|
||||
'cropstart.cropper': function (e) {
|
||||
// console.log(e.type, e.action);
|
||||
},
|
||||
'cropmove.cropper': function (e) {
|
||||
// console.log(e.type, e.action);
|
||||
},
|
||||
'cropend.cropper': function (e) {
|
||||
// console.log(e.type, e.action);
|
||||
},
|
||||
'crop.cropper': function (e) {
|
||||
// console.log(e.type, e.x, e.y, e.width, e.height, e.rotate, e.scaleX, e.scaleY);
|
||||
},
|
||||
'zoom.cropper': function (e) {
|
||||
// console.log(e.type, e.ratio);
|
||||
}
|
||||
}).cropper(options);
|
||||
|
||||
$('#save-background-crop').click(function () {
|
||||
$image.data('cropper').getCroppedCanvas({ width: cropWidth, height: cropHeight }).toBlob(function (blob) {
|
||||
var uri = URL.createObjectURL(blob);
|
||||
var img = new Image();
|
||||
|
||||
img.src = uri;
|
||||
|
||||
var formData = new FormData();
|
||||
formData.append('croppedImage', blob);
|
||||
$.ajax('/api/dashboard/background', {
|
||||
method: "PUT",
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
success: function () {
|
||||
// Update background image
|
||||
// console.log('Upload success');
|
||||
},
|
||||
error: function () {
|
||||
// console.log('Upload error');
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
|
||||
// Methods
|
||||
$('.docs-buttons-background').on('click', '[data-method]', function () {
|
||||
var $this = $(this);
|
||||
var data = $this.data();
|
||||
var $target;
|
||||
var result;
|
||||
|
||||
if ($this.prop('disabled') || $this.hasClass('disabled')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($image.data('cropper') && data.method) {
|
||||
data = $.extend({}, data); // Clone a new one
|
||||
|
||||
if (typeof data.target !== 'undefined') {
|
||||
$target = $(data.target);
|
||||
|
||||
if (typeof data.option === 'undefined') {
|
||||
try {
|
||||
data.option = JSON.parse($target.val());
|
||||
} catch (e) {
|
||||
console.log(e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result = $image.cropper(data.method, data.option, data.secondOption);
|
||||
|
||||
switch (data.method) {
|
||||
case 'scaleX':
|
||||
case 'scaleY':
|
||||
$(this).data('option', -data.option);
|
||||
break;
|
||||
}
|
||||
|
||||
if ($.isPlainObject(result) && $target) {
|
||||
try {
|
||||
$target.val(JSON.stringify(result));
|
||||
} catch (e) {
|
||||
console.log(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
// Import image
|
||||
var $inputImage = $('#inputImageBackground');
|
||||
var URL = window.URL || window.webkitURL;
|
||||
var blobURL;
|
||||
|
||||
if (URL) {
|
||||
$inputImage.change(function () {
|
||||
var files = this.files;
|
||||
var file;
|
||||
|
||||
if (!$image.data('cropper')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (files && files.length) {
|
||||
file = files[0];
|
||||
|
||||
if (/^image\/\w+$/.test(file.type)) {
|
||||
blobURL = URL.createObjectURL(file);
|
||||
$image.one('built.cropper', function () {
|
||||
|
||||
// Revoke when load complete
|
||||
URL.revokeObjectURL(blobURL);
|
||||
}).cropper('reset').cropper('replace', blobURL);
|
||||
$inputImage.val('');
|
||||
} else {
|
||||
window.alert('Please choose an image file.');
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
$inputImage.prop('disabled', true).parent().addClass('disabled');
|
||||
}
|
||||
|
||||
|
||||
};
|
|
@ -0,0 +1,228 @@
|
|||
|
||||
$(document).on('shown.bs.modal', '#modalAddDesktop', function () {
|
||||
modal_add_desktops.columns.adjust().draw();
|
||||
});
|
||||
|
||||
$(document).ready(function() {
|
||||
|
||||
update_modal_groups();
|
||||
|
||||
$('.btn-global-resync').on('click', function () {
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
url:"api/resync",
|
||||
success: function(data)
|
||||
{
|
||||
table.ajax.reload();
|
||||
// $("#modalImport").modal('hide');
|
||||
// users_table.ajax.reload();
|
||||
// groups_table.ajax.reload();
|
||||
},
|
||||
error: function(data)
|
||||
{
|
||||
alert('Something went wrong on our side...')
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Open new group modal
|
||||
$('.btn-new').on('click', function () {
|
||||
update_modal_groups()
|
||||
$('#modalAddGroup').modal({
|
||||
backdrop: 'static',
|
||||
keyboard: false
|
||||
}).modal('show');
|
||||
});
|
||||
|
||||
// Send new group form
|
||||
$('#modalAddGroup #send').on('click', function () {
|
||||
var form = $('#modalAddGroupForm');
|
||||
formdata = form.serializeObject()
|
||||
console.log('NEW GROUP')
|
||||
console.log(formdata)
|
||||
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
"url": "/api/group",
|
||||
data: JSON.stringify(formdata),
|
||||
complete: function(jqXHR, textStatus) {
|
||||
switch (jqXHR.status) {
|
||||
case 200:
|
||||
$("#modalAddGroup").modal('hide');
|
||||
table.ajax.reload();
|
||||
break;
|
||||
case 409:
|
||||
new PNotify({
|
||||
title: "Add group error",
|
||||
text: $.parseJSON(jqXHR.responseText)['msg'],
|
||||
hide: true,
|
||||
delay: 3000,
|
||||
icon: 'fa fa-alert-sign',
|
||||
opacity: 1,
|
||||
type: 'error'
|
||||
});
|
||||
break;
|
||||
case 412:
|
||||
new PNotify({
|
||||
title: "Add group error",
|
||||
text: $.parseJSON(jqXHR.responseText)['msg'],
|
||||
hide: true,
|
||||
delay: 3000,
|
||||
icon: 'fa fa-alert-sign',
|
||||
opacity: 1,
|
||||
type: 'error'
|
||||
});
|
||||
break;
|
||||
default:
|
||||
alert("Server error.");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
$('.btn-delete_keycloak').on('click', function () {
|
||||
$.ajax({
|
||||
type: "DELETE",
|
||||
url:"/api/groups/keycloak",
|
||||
success: function(data)
|
||||
{
|
||||
console.log('SUCCESS')
|
||||
// $("#modalImport").modal('hide');
|
||||
// users_table.ajax.reload();
|
||||
// groups_table.ajax.reload();
|
||||
},
|
||||
error: function(data)
|
||||
{
|
||||
alert('Something went wrong on our side...')
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
//DataTable Main renderer
|
||||
var table = $('#groups').DataTable({
|
||||
"ajax": {
|
||||
"url": "/api/groups",
|
||||
"dataSrc": ""
|
||||
},
|
||||
"language": {
|
||||
"loadingRecords": '<i class="fa fa-spinner fa-pulse fa-3x fa-fw"></i><span class="sr-only">Loading...</span>',
|
||||
"emptyTable": "<h1>You don't have any group created yet.</h1><br><h2>Create one using the +Add new button on top right of this page.</h2>"
|
||||
},
|
||||
"rowId": "id",
|
||||
"deferRender": true,
|
||||
"columns": [
|
||||
{
|
||||
"className": 'actions-control',
|
||||
"orderable": false,
|
||||
"data": null,
|
||||
"width": "80px",
|
||||
"defaultContent": '<button id="btn-delete" class="btn btn-xs" type="button" data-placement="top" ><i class="fa fa-times" style="color:darkred"></i></button>'
|
||||
// \
|
||||
// <button id="btn-edit" class="btn btn-xs" type="button" data-placement="top" ><i class="fa fa-pencil" style="color:darkblue"></i></button>'
|
||||
},
|
||||
{ "data": "name", "width": "10px" },
|
||||
{ "data": "path", "width": "10px" },
|
||||
],
|
||||
"order": [[2, 'asc']],
|
||||
// "columnDefs": [ ]
|
||||
} );
|
||||
|
||||
$('#groups').find(' tbody').on( 'click', 'button', function () {
|
||||
var data = table.row( $(this).parents('tr') ).data();
|
||||
// var closest=$(this).closest("div").parent();
|
||||
// var pk=closest.attr("data-pk");
|
||||
// console.log(pk)
|
||||
switch($(this).attr('id')){
|
||||
case 'btn-edit':
|
||||
$("#modalEditGroupForm")[0].reset();
|
||||
$('#modalEditGroup').modal({
|
||||
backdrop: 'static',
|
||||
keyboard: false
|
||||
}).modal('show');
|
||||
// $('#modalEditGroup #user-avatar').attr("src","/avatar/"+data.id)
|
||||
// setUserDefault('#modalEditGroup', data.id);
|
||||
$('#modalEdit').parsley();
|
||||
break;
|
||||
case 'btn-delete':
|
||||
new PNotify({
|
||||
title: 'Confirmation Needed',
|
||||
text: "Are you sure you want to delete group: "+data['name']+"?",
|
||||
hide: false,
|
||||
opacity: 0.9,
|
||||
confirm: {
|
||||
confirm: true
|
||||
},
|
||||
buttons: {
|
||||
closer: false,
|
||||
sticker: false
|
||||
},
|
||||
history: {
|
||||
history: false
|
||||
},
|
||||
addclass: 'pnotify-center'
|
||||
}).get().on('pnotify.confirm', function() {
|
||||
console.log(data)
|
||||
if(data.id == false){
|
||||
$.ajax({
|
||||
type: "DELETE",
|
||||
url:"/api/group",
|
||||
data: JSON.stringify(data),
|
||||
success: function(data)
|
||||
{
|
||||
table.ajax.reload();
|
||||
},
|
||||
error: function(data)
|
||||
{
|
||||
alert('Something went wrong on our side...')
|
||||
}
|
||||
});
|
||||
}else{
|
||||
$.ajax({
|
||||
type: "DELETE",
|
||||
url:"/api/group/"+data.id,
|
||||
success: function(data)
|
||||
{
|
||||
table.ajax.reload();
|
||||
},
|
||||
error: function(data)
|
||||
{
|
||||
alert('Something went wrong on our side...')
|
||||
}
|
||||
});
|
||||
}
|
||||
}).on('pnotify.cancel', function() {
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
function update_modal_groups(){
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
"url": "/api/groups",
|
||||
success: function(data)
|
||||
{
|
||||
$(".groups-select").empty().append(
|
||||
'<option value="" default>None</option>'
|
||||
)
|
||||
data.forEach(element => {
|
||||
var groupOrigins = [];
|
||||
['keycloak'].forEach(o => {
|
||||
if (element[o]) {
|
||||
groupOrigins.push(o)
|
||||
}
|
||||
})
|
||||
$(".groups-select").append(
|
||||
'<option value="' + element.name + '">' + element.name + '</option>'
|
||||
)
|
||||
});
|
||||
$('.groups-select').select2();
|
||||
},
|
||||
error: function(data)
|
||||
{
|
||||
alert('Something went wrong on our side...')
|
||||
}
|
||||
});
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
$(document).ready(function () {
|
||||
init_wysiwyg();
|
||||
|
||||
var lang = getCookie('KEYCLOAK_LOCALE') ? getCookie('KEYCLOAK_LOCALE') : 'ca'
|
||||
$('#legal-lang').val(lang)
|
||||
getLangLegal(lang)
|
||||
// $('#privacy-lang').val(lang)
|
||||
|
||||
// $.ajax({
|
||||
// type: "GET",
|
||||
// url: "/api/legal/privacy",
|
||||
// data: {
|
||||
// lang: lang
|
||||
// },
|
||||
// success: function (data) {
|
||||
// $('#editor-privacy').html(data.html)
|
||||
// }
|
||||
// })
|
||||
|
||||
$("#save-legal").click(function () {
|
||||
console.log($('#editor-legal').cleanHtml())
|
||||
console.log($('#legal-lang').val())
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "/api/legal/legal",
|
||||
data: JSON.stringify({
|
||||
'html': $('#editor-legal').cleanHtml(),
|
||||
'lang': $('#legal-lang').val()
|
||||
}),
|
||||
success: function () {
|
||||
new PNotify({
|
||||
title: "Legal text",
|
||||
text: "Updated for "+$('#legal-lang').val()+" language",
|
||||
hide: true,
|
||||
delay: 3000,
|
||||
icon: 'fa fa-alert-sign',
|
||||
opacity: 1,
|
||||
type: 'info'
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
$('#legal-lang').on('change', function() {
|
||||
getLangLegal(this.value)
|
||||
});
|
||||
|
||||
// $("#save-privacy").click(function () {
|
||||
// $.ajax({
|
||||
// type: "POST",
|
||||
// url: "/api/legal/privacy",
|
||||
// data: {
|
||||
// 'html': $('#editor-privacy').cleanHtml(),
|
||||
// 'lang': $('#legal-lang').val()
|
||||
// },
|
||||
// success: function () {
|
||||
// },
|
||||
// });
|
||||
// });
|
||||
});
|
||||
|
||||
function getLangLegal(lang) {
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
url: "/api/legal/legal",
|
||||
data: {
|
||||
lang: lang
|
||||
},
|
||||
success: function (data) {
|
||||
$('#editor-legal').html(data.html)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getCookie(cname) {
|
||||
let name = cname + "=";
|
||||
let decodedCookie = decodeURIComponent(document.cookie);
|
||||
let ca = decodedCookie.split(';');
|
||||
for(let i = 0; i <ca.length; i++) {
|
||||
let c = ca[i];
|
||||
while (c.charAt(0) == ' ') {
|
||||
c = c.substring(1);
|
||||
}
|
||||
if (c.indexOf(name) == 0) {
|
||||
return c.substring(name.length, c.length);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function init_wysiwyg() {
|
||||
$("#editor-legal").wysiwyg({
|
||||
toolbarSelector: '[data-target="#editor-legal"]'
|
||||
});
|
||||
// $("#editor-privacy").wysiwyg({
|
||||
// toolbarSelector: '[data-target="#editor-privacy"]'
|
||||
// });
|
||||
window.prettyPrint;
|
||||
prettyPrint();
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
|
||||
$(document).on('shown.bs.modal', '#modalAddDesktop', function () {
|
||||
modal_add_desktops.columns.adjust().draw();
|
||||
});
|
||||
|
||||
$(document).ready(function() {
|
||||
|
||||
$('.btn-global-resync').on('click', function () {
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
url:"/api/resync",
|
||||
success: function(data)
|
||||
{
|
||||
table.ajax.reload();
|
||||
// $("#modalImport").modal('hide');
|
||||
// users_table.ajax.reload();
|
||||
// groups_table.ajax.reload();
|
||||
},
|
||||
error: function(data)
|
||||
{
|
||||
alert('Something went wrong on our side...')
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('.btn-new').on('click', function () {
|
||||
$("#modalAdd")[0].reset();
|
||||
$('#modalAddDesktop').modal({
|
||||
backdrop: 'static',
|
||||
keyboard: false
|
||||
}).modal('show');
|
||||
$('#modalAdd').parsley();
|
||||
});
|
||||
|
||||
//DataTable Main renderer
|
||||
var table = $('#roles').DataTable({
|
||||
"ajax": {
|
||||
"url": "/api/roles",
|
||||
"dataSrc": ""
|
||||
},
|
||||
"language": {
|
||||
"loadingRecords": '<i class="fa fa-spinner fa-pulse fa-3x fa-fw"></i><span class="sr-only">Loading...</span>',
|
||||
"emptyTable": "<h1>You don't have any role created yet.</h1><br><h2>Create one using the +Add new button on top right of this page.</h2>"
|
||||
},
|
||||
"rowId": "id",
|
||||
"deferRender": true,
|
||||
"columns": [
|
||||
{ "data": "id", "width": "10px" },
|
||||
{ "data": "name", "width": "10px" },
|
||||
],
|
||||
"order": [[1, 'asc']],
|
||||
// "columnDefs": [ {
|
||||
// "targets": 0,
|
||||
// "render": function ( data, type, full, meta ) {
|
||||
// // return '<object data="/static/img/missing.jpg" type="image/jpeg" width="25" height="25"><img src="/avatar/'+full.id+'" title="'+full.id+'" width="25" height="25"></object>'
|
||||
// return '<img src="/avatar/'+full.name+'" title="'+full.name+'" width="25" height="25" onerror="if (this.src != \'/static/img/missing.jpg\') this.src = \'/static/img/missing.jpg\';">'
|
||||
// }}]
|
||||
} );
|
||||
});
|
|
@ -0,0 +1,100 @@
|
|||
notice={}
|
||||
$lost=0;
|
||||
|
||||
socket = io.connect(location.protocol+'//' + document.domain +'/sio');
|
||||
console.log(location.protocol+'//' + document.domain +'/sio')
|
||||
socket.on('connect', function() {
|
||||
if($lost){location.reload();}
|
||||
console.log('Listening status socket');
|
||||
});
|
||||
|
||||
socket.on('connect_error', function(data) {
|
||||
$lost=$lost+1;
|
||||
$('#modal-lostconnection').modal({
|
||||
backdrop: 'static',
|
||||
keyboard: false
|
||||
}).modal('show');
|
||||
});
|
||||
|
||||
socket.on('notify-create', function(data) {
|
||||
var data = JSON.parse(data);
|
||||
notice[data.id] = new PNotify({
|
||||
title: data.title,
|
||||
text: data.text,
|
||||
hide: false,
|
||||
type: data.type
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('notify-destroy', function(data) {
|
||||
var data = JSON.parse(data);
|
||||
notice[data.id].remove()
|
||||
});
|
||||
|
||||
socket.on('notify-increment', function(data) {
|
||||
var data = JSON.parse(data);
|
||||
if(!( data.id in notice)){
|
||||
notice[data.id] = new PNotify({
|
||||
title: data.title,
|
||||
text: data.text,
|
||||
hide: false,
|
||||
type: data.type
|
||||
});
|
||||
}
|
||||
// console.log(data.text)
|
||||
notice[data.id].update({
|
||||
text: data.text
|
||||
})
|
||||
if(! data.table == false){
|
||||
dtUpdateInsert(data.table,data['data']['data'])
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
// new PNotify({
|
||||
// title: "Quota for creating desktops full.",
|
||||
// text: "Can't create another desktop, user quota full.",
|
||||
// hide: true,
|
||||
// delay: 3000,
|
||||
// icon: 'fa fa-alert-sign',
|
||||
// opacity: 1,
|
||||
// type: 'error'
|
||||
// });
|
||||
|
||||
// socket.on('update', function(data) {
|
||||
// var data = JSON.parse(data);
|
||||
// console.log('Status update')
|
||||
// console.log(data)
|
||||
// // var data = JSON.parse(data);
|
||||
// // drawUserQuota(data);
|
||||
// });
|
||||
|
||||
socket.on('update', function(data) {
|
||||
var data = JSON.parse(data);
|
||||
console.log('Status update')
|
||||
console.log(data)
|
||||
// var data = JSON.parse(data);
|
||||
// drawUserQuota(data);
|
||||
});
|
||||
|
||||
// {'event':'traceback',
|
||||
// 'id':u['id'],
|
||||
// 'item':'group',
|
||||
// 'action':'add'
|
||||
// 'name':g['name'],
|
||||
// 'progress':str(item)+'/'+str(total),
|
||||
// 'status':False,
|
||||
// 'msg':,
|
||||
// 'payload':{'traceback':traceback.format_exc(),
|
||||
// 'data':g})
|
||||
|
||||
socket.on('progress', function(data) {
|
||||
var data = JSON.parse(data);
|
||||
console.log(data)
|
||||
// $('.modal-progress #item').html(data.item)
|
||||
});
|
||||
|
||||
|
||||
|
||||
////
|
|
@ -0,0 +1,379 @@
|
|||
|
||||
$(document).on('shown.bs.modal', '#modalAddDesktop', function () {
|
||||
modal_add_desktops.columns.adjust().draw();
|
||||
});
|
||||
|
||||
$(document).ready(function() {
|
||||
$('#action_role option[value=""]').prop("selected",true);
|
||||
var path = "";
|
||||
items = [];
|
||||
document.getElementById('file-upload').addEventListener('change', readFile, false);
|
||||
$('.btn-upload').on('click', function () {
|
||||
$('#modalImport').modal({backdrop: 'static', keyboard: false}).modal('show');
|
||||
$('#modalImportForm')[0].reset();
|
||||
});
|
||||
|
||||
$('.btn-sync').on('click', function () {
|
||||
ids={}
|
||||
$.each(users_table.rows().data(),function(key, value){
|
||||
ids[value['id']]=value['roles']
|
||||
});
|
||||
// console.log(ids)
|
||||
$.ajax({
|
||||
type: "PUT",
|
||||
url:"/api/external",
|
||||
data: JSON.stringify(ids),
|
||||
success: function(data)
|
||||
{
|
||||
console.log('SUCCESS')
|
||||
// $("#modalImport").modal('hide');
|
||||
users_table.ajax.reload();
|
||||
groups_table.ajax.reload();
|
||||
},
|
||||
error: function(data)
|
||||
{
|
||||
alert('Something went wrong on our side...')
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('.btn-clear-upload').on('click', function () {
|
||||
new PNotify({
|
||||
title: 'Cleaning imported data',
|
||||
text: 'Are you sure you want to clean imported data?',
|
||||
hide: false,
|
||||
opacity: 0.9,
|
||||
confirm: {
|
||||
confirm: true
|
||||
},
|
||||
buttons: {
|
||||
closer: false,
|
||||
sticker: false
|
||||
},
|
||||
history: {
|
||||
history: false
|
||||
},
|
||||
addclass: 'pnotify-center'
|
||||
}).get().on('pnotify.confirm', function() {
|
||||
$.ajax({
|
||||
type: "DELETE",
|
||||
url:"/api/external",
|
||||
success: function(data)
|
||||
{
|
||||
console.log('SUCCESS')
|
||||
users_table.ajax.reload();
|
||||
groups_table.ajax.reload();
|
||||
},
|
||||
error: function(data)
|
||||
{
|
||||
alert('Something went wrong on our side...')
|
||||
}
|
||||
});
|
||||
}).on('pnotify.cancel', function() {
|
||||
$('#action_role option[value=""]').prop("selected",true);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
$('.btn-sample').on('click', function () {
|
||||
var viewerFile = new Blob(["groups;firstname;lastname;email;username;password;password_temporal;role;quota\n/alumnes/6è;John;Doe;jdoe@digitaldemocratic.net;jdoe;SuperSecret;no;student;1GB\n/alumnes/6è;Magdalena;Martí;mm_profe@email.cat;mm_profe;SuperSecret;no;teacher;3GB\n/managers;Pere;Isard;pisardmgr@email.cat;pisardmgr;SuperSecret;no;manager;Unlimited\n/alumnes/4t,/alumnes/5è;Marc;Gómez;marcgt@email.cat;marcgt;SuperSecret;no;student;1GB"], {type: "text/csv"});
|
||||
var a = document.createElement('a');
|
||||
a.download = 'dd_sample_upload.csv';
|
||||
a.href = window.URL.createObjectURL(viewerFile);
|
||||
var ev = document.createEvent("MouseEvents");
|
||||
ev.initMouseEvent("click", true, false, self, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
|
||||
a.dispatchEvent(ev);
|
||||
});
|
||||
|
||||
$('.btn-download').on('click', function () {
|
||||
|
||||
data=users_table.rows().data()
|
||||
csv_data='TYPE,EXT_ID,EMAIL,FIRST,LAST,USERNAME,PATHS,GROUPS,QUOTA,ROLE,PASS_TEMP,PASSWORD'+ '\r\n'
|
||||
csv_data=csv_data+convertToCSV(data)
|
||||
console.log(csv_data)
|
||||
exportCSVFile(csv_data, 'users_data')
|
||||
})
|
||||
|
||||
$("#modalImport #send").on('click', function(e){
|
||||
// console.log(users_table.rows().data())
|
||||
var form = $('#modalImportForm');
|
||||
form.parsley().validate();
|
||||
if (form.parsley().isValid()){
|
||||
formdata = form.serializeObject()
|
||||
if($('#format').val() == 'csv-ug'){
|
||||
formdata['data']=parseCSV(filecontents)
|
||||
}else{
|
||||
formdata['data']=JSON.parse(filecontents)
|
||||
}
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url:"/api/external",
|
||||
data: JSON.stringify(formdata),
|
||||
success: function(data)
|
||||
{
|
||||
console.log('SUCCESS')
|
||||
$("#modalImport").modal('hide');
|
||||
users_table.ajax.reload();
|
||||
groups_table.ajax.reload();
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
var err = eval("(" + xhr.responseText + ")");
|
||||
alert(JSON.parse(xhr.responseText).msg)
|
||||
users_table.ajax.reload();
|
||||
groups_table.ajax.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$('#action_role').on('change', function () {
|
||||
action=$(this).val();
|
||||
names=''
|
||||
ids=[]
|
||||
|
||||
if(users_table.rows('.active').data().length){
|
||||
$.each(users_table.rows('.active').data(),function(key, value){
|
||||
names+=value['name']+'\n';
|
||||
ids.push(value['id']);
|
||||
});
|
||||
var text = "You are about to assign role "+action+" these users:\n\n "+names
|
||||
}else{
|
||||
$.each(users_table.rows({filter: 'applied'}).data(),function(key, value){
|
||||
ids.push(value['id']);
|
||||
});
|
||||
var text = "You are about to assign role "+action+" "+users_table.rows({filter: 'applied'}).data().length+" users!\n All the users in list!"
|
||||
}
|
||||
new PNotify({
|
||||
title: 'Role assignment!',
|
||||
text: 'You will asign the role '+action,
|
||||
hide: false,
|
||||
opacity: 0.9,
|
||||
confirm: {
|
||||
confirm: true
|
||||
},
|
||||
buttons: {
|
||||
closer: false,
|
||||
sticker: false
|
||||
},
|
||||
history: {
|
||||
history: false
|
||||
},
|
||||
addclass: 'pnotify-center'
|
||||
}).get().on('pnotify.confirm', function() {
|
||||
$.ajax({
|
||||
type: "PUT",
|
||||
url:"/api/external/roles",
|
||||
data: JSON.stringify({'ids':ids,'action':action}),
|
||||
success: function(data)
|
||||
{
|
||||
console.log('SUCCESS')
|
||||
users_table.ajax.reload();
|
||||
groups_table.ajax.reload();
|
||||
},
|
||||
error: function(data)
|
||||
{
|
||||
alert('Something went wrong on our side...')
|
||||
}
|
||||
});
|
||||
}).on('pnotify.cancel', function() {
|
||||
|
||||
});
|
||||
$('#action_role option[value=""]').prop("selected",true);
|
||||
} );
|
||||
|
||||
//DataTable Main renderer
|
||||
var users_table = $('#users').DataTable({
|
||||
"ajax": {
|
||||
"url": "/api/external/users",
|
||||
"dataSrc": ""
|
||||
},
|
||||
"language": {
|
||||
"loadingRecords": '<i class="fa fa-spinner fa-pulse fa-3x fa-fw"></i><span class="sr-only">Loading...</span>',
|
||||
"emptyTable": "<h2>No users imported yet.</h2><br><h2>Import with the Upload button on top right of this page.</h2>"
|
||||
},
|
||||
"rowId": "id",
|
||||
"deferRender": true,
|
||||
"columns": [
|
||||
{
|
||||
"className": 'details-control',
|
||||
"orderable": false,
|
||||
"data": null,
|
||||
"width": "10px",
|
||||
"defaultContent": '<button class="btn btn-xs btn-info" type="button" data-placement="top" ><i class="fa fa-plus"></i></button>'
|
||||
},
|
||||
// { "data": "provider", "width": "10px" },
|
||||
{ "data": "id", "width": "10px" },
|
||||
{ "data": "username", "width": "10px"},
|
||||
{ "data": "first", "width": "10px"},
|
||||
{ "data": "last", "width": "10px"},
|
||||
{ "data": "email", "width": "10px"},
|
||||
{ "data": "gids", "width": "10px"},
|
||||
{ "data": "groups", "width": "10px"},
|
||||
{ "data": "roles", "width": "10px"},
|
||||
{ "data": "quota", "width": "10px"},
|
||||
{ "data": "password", "width": "10px"},
|
||||
],
|
||||
"order": [[3, 'asc']],
|
||||
"columnDefs": [ {
|
||||
"targets": 1,
|
||||
"render": function ( data, type, full, meta ) {
|
||||
return '<img src="/custom/avatars/'+full.roles+'.jpg" title="'+full.id+'" width="25" height="25" onerror="if (this.src != \'/static/img/missing.jpg\') this.src = \'/static/img/missing.jpg\';">'
|
||||
return '<img src="/avatar/'+full.id+'" title="'+full.id+'" width="25" height="25">'
|
||||
}},
|
||||
{
|
||||
"targets": 6,
|
||||
"render": function ( data, type, full, meta ) {
|
||||
return "<li>" + full.gids.join("</li><li>") + "</li>"
|
||||
}},
|
||||
{
|
||||
"targets": 7,
|
||||
"render": function ( data, type, full, meta ) {
|
||||
return "<li>" + full.groups.join("</li><li>") + "</li>"
|
||||
}}
|
||||
]
|
||||
} );
|
||||
|
||||
|
||||
var groups_table = $('#groups').DataTable({
|
||||
"ajax": {
|
||||
"url": "/api/external/groups",
|
||||
"dataSrc": ""
|
||||
},
|
||||
"language": {
|
||||
"loadingRecords": '<i class="fa fa-spinner fa-pulse fa-3x fa-fw"></i><span class="sr-only">Loading...</span>',
|
||||
"emptyTable": "<h2>No groups imported yet.</h2>"
|
||||
},
|
||||
"rowId": "id",
|
||||
"deferRender": true,
|
||||
"columns": [
|
||||
{
|
||||
"className": 'details-control',
|
||||
"orderable": false,
|
||||
"data": null,
|
||||
"width": "10px",
|
||||
"defaultContent": '<button class="btn btn-xs btn-info" type="button" data-placement="top" ><i class="fa fa-plus"></i></button>'
|
||||
},
|
||||
// { "data": "id", "width": "10px" },
|
||||
// { "data": "provider", "width": "10px" },
|
||||
{ "data": "name", "width": "10px" },
|
||||
{ "data": "description", "width": "10px"},
|
||||
],
|
||||
"order": [[2, 'asc']],
|
||||
"columnDefs": [ ]
|
||||
} );
|
||||
});
|
||||
|
||||
function readFile (evt) {
|
||||
if($('#format').val() == 'json-ga'){
|
||||
path = "";
|
||||
items = [];
|
||||
var files = evt.target.files;
|
||||
var file = files[0];
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(event) {
|
||||
filecontents=event.target.result;
|
||||
$.each(JSON.parse(filecontents), walker);
|
||||
populate_path(items)
|
||||
}
|
||||
reader.readAsText(file, 'UTF-8')
|
||||
}
|
||||
if($('#format').val() == 'csv-ug'){
|
||||
var files = evt.target.files;
|
||||
var file = files[0];
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(event) {
|
||||
filecontents=event.target.result;
|
||||
// $.each(JSON.parse(filecontents), walker);
|
||||
// populate_path(items)
|
||||
}
|
||||
reader.readAsText(file, 'UTF-8')
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function parseCSV(){
|
||||
lines=filecontents.split('\n')
|
||||
header=lines[0].split(';')
|
||||
users=[]
|
||||
$.each(lines, function(n, l){
|
||||
if(n!=0 && l.length > 10){
|
||||
usr=toObject(header,l.split(';'))
|
||||
usr['id']=usr['username']
|
||||
users.push(usr)
|
||||
}
|
||||
})
|
||||
return users;
|
||||
}
|
||||
function toObject(names, values) {
|
||||
var result = {};
|
||||
for (var i = 0; i < names.length; i++)
|
||||
result[names[i]] = values[i];
|
||||
return result;
|
||||
}
|
||||
|
||||
function walker(key, value) {
|
||||
var savepath = path;
|
||||
path = path ? (path + "/" + key) : key;
|
||||
items.push({path:path})
|
||||
|
||||
if (typeof value === "object") {
|
||||
// Recurse into children
|
||||
if(value.constructor === Array){
|
||||
value=value[0]
|
||||
}
|
||||
if(typeof value == "object"){
|
||||
$.each(value, walker);
|
||||
}
|
||||
}
|
||||
|
||||
path = savepath;
|
||||
}
|
||||
|
||||
function populate_path(){
|
||||
$.each(items, function(key, value) {
|
||||
$(".populate").append('<option value=' + value['path']+ '>' + value['path'] + '</option>');
|
||||
})
|
||||
}
|
||||
|
||||
function convertToCSV(objArray) {
|
||||
var array = typeof objArray != 'object' ? JSON.parse(objArray) : objArray;
|
||||
var str = '';
|
||||
|
||||
for (var i = 0; i < array.length; i++) {
|
||||
var line = '';
|
||||
for (var index in array[i]) {
|
||||
if (line != '') line += ','
|
||||
|
||||
if (Array.isArray(array[i][index])){
|
||||
line += '"'+array[i][index]+'"'
|
||||
}else{
|
||||
line += array[i][index];
|
||||
}
|
||||
}
|
||||
|
||||
str += line + '\r\n';
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
function exportCSVFile(csv, fileTitle) {
|
||||
var exportedFilenmae = fileTitle + '.csv' || 'export.csv';
|
||||
|
||||
var blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||
if (navigator.msSaveBlob) { // IE 10+
|
||||
navigator.msSaveBlob(blob, exportedFilenmae);
|
||||
} else {
|
||||
var link = document.createElement("a");
|
||||
if (link.download !== undefined) { // feature detection
|
||||
// Browsers that support HTML5 download attribute
|
||||
var url = URL.createObjectURL(blob);
|
||||
link.setAttribute("href", url);
|
||||
link.setAttribute("download", exportedFilenmae);
|
||||
link.style.visibility = 'hidden';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
|
||||
$(document).on('shown.bs.modal', '#modalAddDesktop', function () {
|
||||
modal_add_desktops.columns.adjust().draw();
|
||||
});
|
||||
|
||||
$(document).ready(function() {
|
||||
|
||||
$('.btn-global-resync').on('click', function () {
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
url:"api/resync",
|
||||
success: function(data)
|
||||
{
|
||||
table.ajax.reload();
|
||||
// $("#modalImport").modal('hide');
|
||||
// users_table.ajax.reload();
|
||||
// groups_table.ajax.reload();
|
||||
},
|
||||
error: function(data)
|
||||
{
|
||||
alert('Something went wrong on our side...')
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Open new group modal
|
||||
$('.btn-new').on('click', function () {
|
||||
$('#modalAddGroup').modal({
|
||||
backdrop: 'static',
|
||||
keyboard: false
|
||||
}).modal('show');
|
||||
});
|
||||
|
||||
// Send new group form
|
||||
$('#modalAddGroup #send').on('click', function () {
|
||||
var form = $('#modalAddGroupForm');
|
||||
formdata = form.serializeObject()
|
||||
console.log('NEW GROUP')
|
||||
console.log(formdata)
|
||||
// $.ajax({
|
||||
// type: "POST",
|
||||
// "url": "/groups_list",
|
||||
// success: function(data)
|
||||
// {
|
||||
// console.log('SUCCESS')
|
||||
// // $("#modalAddGroup").modal('hide');
|
||||
// },
|
||||
// error: function(data)
|
||||
// {
|
||||
// alert('Something went wrong on our side...')
|
||||
// }
|
||||
// });
|
||||
});
|
||||
|
||||
$('.btn-delete_keycloak').on('click', function () {
|
||||
$.ajax({
|
||||
type: "DELETE",
|
||||
url:"/api/groups/keycloak",
|
||||
success: function(data)
|
||||
{
|
||||
console.log('SUCCESS')
|
||||
// $("#modalImport").modal('hide');
|
||||
// users_table.ajax.reload();
|
||||
// groups_table.ajax.reload();
|
||||
},
|
||||
error: function(data)
|
||||
{
|
||||
alert('Something went wrong on our side...')
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
//DataTable Main renderer
|
||||
var table = $('#groups').DataTable({
|
||||
"ajax": {
|
||||
"url": "/api/groups",
|
||||
"dataSrc": ""
|
||||
},
|
||||
"language": {
|
||||
"loadingRecords": '<i class="fa fa-spinner fa-pulse fa-3x fa-fw"></i><span class="sr-only">Loading...</span>',
|
||||
"emptyTable": "<h1>You don't have any group created yet.</h1><br><h2>Create one using the +Add new button on top right of this page.</h2>"
|
||||
},
|
||||
"rowId": "id",
|
||||
"deferRender": true,
|
||||
"columns": [
|
||||
{
|
||||
"className": 'details-control',
|
||||
"orderable": false,
|
||||
"data": null,
|
||||
"width": "10px",
|
||||
"defaultContent": '<button class="btn btn-xs btn-info" type="button" data-placement="top" ><i class="fa fa-plus"></i></button>'
|
||||
},
|
||||
{ "data": "id", "width": "10px" },
|
||||
{ "data": "keycloak", "width": "10px" },
|
||||
{ "data": "moodle", "width": "10px" },
|
||||
{ "data": "nextcloud", "width": "10px" },
|
||||
{ "data": "name", "width": "10px" },
|
||||
{ "data": "path", "width": "10px" },
|
||||
],
|
||||
"order": [[3, 'asc']],
|
||||
"columnDefs": [ {
|
||||
"targets": 2,
|
||||
"render": function ( data, type, full, meta ) {
|
||||
if(full.keycloak){
|
||||
return '<i class="fa fa-check" style="color:lightgreen"></i>'
|
||||
}else{
|
||||
return '<i class="fa fa-close" style="color:darkred"></i>'
|
||||
};
|
||||
}},
|
||||
{
|
||||
"targets": 3,
|
||||
"render": function ( data, type, full, meta ) {
|
||||
if(full.moodle){
|
||||
return '<i class="fa fa-check" style="color:lightgreen"></i>'
|
||||
}else{
|
||||
return '<i class="fa fa-close" style="color:darkred"></i>'
|
||||
};
|
||||
}},
|
||||
{
|
||||
"targets": 4,
|
||||
"render": function ( data, type, full, meta ) {
|
||||
if(full.nextcloud){
|
||||
return '<i class="fa fa-check" style="color:lightgreen"></i>'
|
||||
}else{
|
||||
return '<i class="fa fa-close" style="color:darkred"></i>'
|
||||
};
|
||||
}},
|
||||
]
|
||||
} );
|
||||
})
|
|
@ -0,0 +1,525 @@
|
|||
|
||||
$(document).on('shown.bs.modal', '#modalAddDesktop', function () {
|
||||
modal_add_desktops.columns.adjust().draw();
|
||||
});
|
||||
|
||||
$(document).ready(function() {
|
||||
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
"url": "/api/groups",
|
||||
success: function(data)
|
||||
{
|
||||
data.forEach(element => {
|
||||
var groupOrigins = [];
|
||||
['keycloak', 'moodle', 'nextcloud'].forEach(o => {
|
||||
if (element[o]) {
|
||||
groupOrigins.push(o)
|
||||
}
|
||||
})
|
||||
$(".groups-select").append(
|
||||
'<option value=' + element.path + '>' + element.name + ' (' + groupOrigins.join(',') + ') </option>'
|
||||
)
|
||||
});
|
||||
$('.groups-select').select2();
|
||||
},
|
||||
error: function(data)
|
||||
{
|
||||
alert('Something went wrong on our side...')
|
||||
}
|
||||
});
|
||||
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
"url": "/api/roles",
|
||||
success: function(data)
|
||||
{
|
||||
console.log('ROLES')
|
||||
console.log(data)
|
||||
data.forEach(element => {
|
||||
$(".role-moodle-select, .role-nextcloud-select, .role-keycloak-select").append(
|
||||
'<option value=' + element.id + '>' + element.name + '</option>'
|
||||
)
|
||||
})
|
||||
},
|
||||
error: function(data)
|
||||
{
|
||||
alert('Something went wrong on our side...')
|
||||
}
|
||||
});
|
||||
|
||||
$('.btn-global-resync').on('click', function () {
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
url:"/api/resync",
|
||||
success: function(data)
|
||||
{
|
||||
console.log('Reloaded')
|
||||
table.ajax.reload();
|
||||
// $("#modalImport").modal('hide');
|
||||
// users_table.ajax.reload();
|
||||
// groups_table.ajax.reload();
|
||||
},
|
||||
error: function(data)
|
||||
{
|
||||
alert('Something went wrong on our side...')
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Open new user modal
|
||||
$('.btn-new-user').on('click', function () {
|
||||
$('#modalAddUser').modal({
|
||||
backdrop: 'static',
|
||||
keyboard: false
|
||||
}).modal('show');
|
||||
});
|
||||
|
||||
// Send new user form
|
||||
$('#modalAddUser #send').on('click', function () {
|
||||
var form = $('#modalAddUserForm');
|
||||
formdata = form.serializeObject()
|
||||
console.log('NEW USER')
|
||||
console.log(formdata)
|
||||
// $.ajax({
|
||||
// type: "POST",
|
||||
// "url": "/groups_list",
|
||||
// success: function(data)
|
||||
// {
|
||||
// console.log('SUCCESS')
|
||||
// // $("#modalAddUser").modal('hide');
|
||||
// },
|
||||
// error: function(data)
|
||||
// {
|
||||
// alert('Something went wrong on our side...')
|
||||
// }
|
||||
// });
|
||||
});
|
||||
|
||||
$('.btn-delete_keycloak').on('click', function () {
|
||||
new PNotify({
|
||||
title: 'Confirmation Needed',
|
||||
text: "Are you sure you want to DELETE ALL USERS IN KEYCLOAK???",
|
||||
hide: false,
|
||||
opacity: 0.9,
|
||||
confirm: {
|
||||
confirm: true
|
||||
},
|
||||
buttons: {
|
||||
closer: false,
|
||||
sticker: false
|
||||
},
|
||||
history: {
|
||||
history: false
|
||||
},
|
||||
addclass: 'pnotify-center'
|
||||
}).get().on('pnotify.confirm', function() {
|
||||
console.log('Updating user password...')
|
||||
$.ajax({
|
||||
type: "DELETE",
|
||||
url:"/api/users/keycloak",
|
||||
success: function(data)
|
||||
{
|
||||
console.log('SUCCESS')
|
||||
// $("#modalImport").modal('hide');
|
||||
// users_table.ajax.reload();
|
||||
// groups_table.ajax.reload();
|
||||
},
|
||||
error: function(data)
|
||||
{
|
||||
alert('Something went wrong on our side...')
|
||||
}
|
||||
});
|
||||
}).on('pnotify.cancel', function() {
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
$('.btn-delete_nextcloud').on('click', function () {
|
||||
new PNotify({
|
||||
title: 'Confirmation Needed',
|
||||
text: "Are you sure you want to DELETE ALL USERS IN NEXTCLOUD?",
|
||||
hide: false,
|
||||
opacity: 0.9,
|
||||
confirm: {
|
||||
confirm: true
|
||||
},
|
||||
buttons: {
|
||||
closer: false,
|
||||
sticker: false
|
||||
},
|
||||
history: {
|
||||
history: false
|
||||
},
|
||||
addclass: 'pnotify-center'
|
||||
}).get().on('pnotify.confirm', function() {
|
||||
console.log('Updating user password...')
|
||||
$.ajax({
|
||||
type: "DELETE",
|
||||
url:"/api/users/nextcloud",
|
||||
success: function(data)
|
||||
{
|
||||
console.log('SUCCESS')
|
||||
// $("#modalImport").modal('hide');
|
||||
// users_table.ajax.reload();
|
||||
// groups_table.ajax.reload();
|
||||
},
|
||||
error: function(data)
|
||||
{
|
||||
alert('Something went wrong on our side...')
|
||||
}
|
||||
});
|
||||
}).on('pnotify.cancel', function() {
|
||||
});
|
||||
});
|
||||
|
||||
$('.btn-delete_moodle').on('click', function () {
|
||||
new PNotify({
|
||||
title: 'Confirmation Needed',
|
||||
text: "Are you sure you want to DELETE ALL USERS IN MOODLE?",
|
||||
hide: false,
|
||||
opacity: 0.9,
|
||||
confirm: {
|
||||
confirm: true
|
||||
},
|
||||
buttons: {
|
||||
closer: false,
|
||||
sticker: false
|
||||
},
|
||||
history: {
|
||||
history: false
|
||||
},
|
||||
addclass: 'pnotify-center'
|
||||
}).get().on('pnotify.confirm', function() {
|
||||
console.log('Updating user password...')
|
||||
$.ajax({
|
||||
type: "DELETE",
|
||||
url:"/api/users/moodle",
|
||||
success: function(data)
|
||||
{
|
||||
console.log('SUCCESS')
|
||||
// $("#modalImport").modal('hide');
|
||||
// users_table.ajax.reload();
|
||||
// groups_table.ajax.reload();
|
||||
},
|
||||
error: function(data)
|
||||
{
|
||||
alert('Something went wrong on our side...')
|
||||
}
|
||||
});
|
||||
}).on('pnotify.cancel', function() {
|
||||
});
|
||||
});
|
||||
|
||||
$('.btn-sync_to_moodle').on('click', function () {
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url:"/api/users/moodle",
|
||||
success: function(data)
|
||||
{
|
||||
console.log('SUCCESS')
|
||||
// $("#modalImport").modal('hide');
|
||||
// users_table.ajax.reload();
|
||||
// groups_table.ajax.reload();
|
||||
},
|
||||
error: function(data)
|
||||
{
|
||||
alert('Something went wrong on our side...')
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
$('.btn-sync_from_keycloak').on('click', function () {
|
||||
$.ajax({
|
||||
type: "PUT",
|
||||
url:"/api/users",
|
||||
success: function(data)
|
||||
{
|
||||
console.log('SUCCESS')
|
||||
// $("#modalImport").modal('hide');
|
||||
// users_table.ajax.reload();
|
||||
// groups_table.ajax.reload();
|
||||
},
|
||||
error: function(data)
|
||||
{
|
||||
alert('Something went wrong on our side...')
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('.btn-sync_to_nextcloud').on('click', function () {
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url:"/api/users/nextcloud",
|
||||
success: function(data)
|
||||
{
|
||||
console.log('SUCCESS')
|
||||
// $("#modalImport").modal('hide');
|
||||
// users_table.ajax.reload();
|
||||
// groups_table.ajax.reload();
|
||||
},
|
||||
error: function(data)
|
||||
{
|
||||
alert('Something went wrong on our side...')
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
//DataTable Main renderer
|
||||
var table = $('#users').DataTable({
|
||||
"ajax": {
|
||||
"url": "/api/users",
|
||||
"dataSrc": ""
|
||||
},
|
||||
"language": {
|
||||
"loadingRecords": '<i class="fa fa-spinner fa-pulse fa-3x fa-fw"></i><span class="sr-only">Loading...</span>',
|
||||
"emptyTable": "<h1>You don't have any user created yet.</h1><br><h2>Create one using the +Add new button on top right of this page.</h2>"
|
||||
},
|
||||
"rowId": "id",
|
||||
"deferRender": true,
|
||||
"columns": [
|
||||
{
|
||||
"className": 'details-control',
|
||||
"orderable": false,
|
||||
"data": null,
|
||||
"width": "10px",
|
||||
"defaultContent": '<button class="btn btn-xs btn-info" type="button" data-placement="top" ><i class="fa fa-plus"></i></button>'
|
||||
},
|
||||
{ "data": "id", "width": "10px" },
|
||||
{ "data": "username", "width": "10px"},
|
||||
{ "data": "first", "width": "10px"},
|
||||
{ "data": "last", "width": "10px"},
|
||||
{ "data": "email", "width": "10px"},
|
||||
{ "data": "keycloak", "width": "10px" },
|
||||
{ "data": "keycloak_groups", "width": "10px" },
|
||||
{ "data": "roles", "width": "10px" },
|
||||
{ "data": "moodle", "width": "10px" },
|
||||
{ "data": "moodle_groups", "width": "10px" },
|
||||
{ "data": "nextcloud", "width": "10px" },
|
||||
{ "data": "nextcloud_groups", "width": "10px" },
|
||||
],
|
||||
"order": [[4, 'asc']],
|
||||
"columnDefs": [ {
|
||||
"targets": 1,
|
||||
"render": function ( data, type, full, meta ) {
|
||||
return '<img src="/avatar/'+full.id+'" title="'+full.id+'" width="25" height="25">'
|
||||
}},
|
||||
{
|
||||
"targets": 6,
|
||||
"render": function ( data, type, full, meta ) {
|
||||
if(full.keycloak){
|
||||
return '<i class="fa fa-check" style="color:lightgreen"></i>'
|
||||
}else{
|
||||
return '<i class="fa fa-close" style="color:darkred"></i>'
|
||||
};
|
||||
}},
|
||||
{
|
||||
"targets": 7,
|
||||
"render": function ( data, type, full, meta ) {
|
||||
return "<li>" + full.keycloak_groups.join("</li><li>") + "</li>"
|
||||
}},
|
||||
{
|
||||
"targets": 9,
|
||||
"render": function ( data, type, full, meta ) {
|
||||
if(full.moodle){
|
||||
return '<i class="fa fa-check" style="color:lightgreen"></i>'
|
||||
}else{
|
||||
return '<i class="fa fa-close" style="color:darkred"></i>'
|
||||
};
|
||||
}},
|
||||
{
|
||||
"targets": 10,
|
||||
"render": function ( data, type, full, meta ) {
|
||||
return "<li>" + full.moodle_groups.join("</li><li>") + "</li>"
|
||||
}},
|
||||
{
|
||||
"targets": 11,
|
||||
"render": function ( data, type, full, meta ) {
|
||||
if(full.nextcloud){
|
||||
return '<i class="fa fa-check" style="color:lightgreen"></i>'
|
||||
}else{
|
||||
return '<i class="fa fa-close" style="color:darkred"></i>'
|
||||
};
|
||||
}},
|
||||
{
|
||||
"targets": 12,
|
||||
"render": function ( data, type, full, meta ) {
|
||||
return "<li>" + full.nextcloud_groups.join("</li><li>") + "</li>"
|
||||
}},
|
||||
]
|
||||
} );
|
||||
|
||||
$template = $(".template-detail-users");
|
||||
|
||||
$('#users').find('tbody').on('click', 'td.details-control', function () {
|
||||
var tr = $(this).closest('tr');
|
||||
var row = table.row( tr );
|
||||
|
||||
if ( row.child.isShown() ) {
|
||||
// This row is already open - close it
|
||||
row.child.hide();
|
||||
tr.removeClass('shown');
|
||||
}
|
||||
else {
|
||||
// Close other rows
|
||||
if ( table.row( '.shown' ).length ) {
|
||||
$('.details-control', table.row( '.shown' ).node()).click();
|
||||
}
|
||||
// Open this row
|
||||
row.child( addUserDetailPannel(row.data()) ).show();
|
||||
tr.addClass('shown');
|
||||
actionsUserDetail()
|
||||
}
|
||||
} );
|
||||
|
||||
function addUserDetailPannel ( d ) {
|
||||
$newPanel = $template.clone();
|
||||
$newPanel.html(function(i, oldHtml){
|
||||
return oldHtml.replace(/d.id/g, d.id).replace(/d.username/g, d.username);
|
||||
});
|
||||
return $newPanel
|
||||
}
|
||||
|
||||
function actionsUserDetail(){
|
||||
|
||||
$('.btn-passwd').on('click', function () {
|
||||
var closest=$(this).closest("div").parent();
|
||||
var pk=closest.attr("data-pk");
|
||||
$("#modalPasswdUserForm")[0].reset();
|
||||
$('#modalPasswdUser').modal({
|
||||
backdrop: 'static',
|
||||
keyboard: false
|
||||
}).modal('show');
|
||||
$('#modalPasswdUserForm #id').val(pk);
|
||||
});
|
||||
|
||||
$("#modalPasswdUser #send").on('click', function(e){
|
||||
var form = $('#modalPasswdUserForm');
|
||||
form.parsley().validate();
|
||||
if (form.parsley().isValid()){
|
||||
data=$('#modalPasswdUserForm').serializeObject();
|
||||
data['id']=$('#modalPasswdUserForm #id').val();
|
||||
new PNotify({
|
||||
title: 'Confirmation Needed',
|
||||
text: "Are you sure you want to update password for the user "+ username+"?",
|
||||
hide: false,
|
||||
opacity: 0.9,
|
||||
confirm: {
|
||||
confirm: true
|
||||
},
|
||||
buttons: {
|
||||
closer: false,
|
||||
sticker: false
|
||||
},
|
||||
history: {
|
||||
history: false
|
||||
},
|
||||
addclass: 'pnotify-center'
|
||||
}).get().on('pnotify.confirm', function() {
|
||||
console.log('Updating user password...')
|
||||
$.ajax({
|
||||
type: "PUT",
|
||||
url:"/api/user" + id,
|
||||
success: function(data)
|
||||
{
|
||||
$(div_id + ' #id').val(data.id);
|
||||
$(div_id + ' #username').val(data.username);
|
||||
$(div_id + ' #email').val(data.email);
|
||||
$(div_id + ' #firstname').val(data.firstname);
|
||||
$(div_id + ' #lastname').val(data.lastname);
|
||||
$(div_id + ' .groups-select').val(data.groups);
|
||||
$(div_id + ' .role-moodle-select').val(data.roles);
|
||||
$(div_id + ' .role-nextcloud-select').val(data.roles);
|
||||
$(div_id + ' .role-keycloak-select').val(data.roles);
|
||||
$('.groups-select, .role-moodle-select, .role-nextcloud-select, .role-keycloak-select').trigger('change');
|
||||
}
|
||||
});
|
||||
}).on('pnotify.cancel', function() {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$('.btn-edit').on('click', function () {
|
||||
var closest=$(this).closest("div").parent();
|
||||
var pk=closest.attr("data-pk");
|
||||
$("#modalEditUserForm")[0].reset();
|
||||
$('#modalEditUser').modal({
|
||||
backdrop: 'static',
|
||||
keyboard: false
|
||||
}).modal('show');
|
||||
setUserDefault('#modalEditUser', pk);
|
||||
$('#modalEdit').parsley();
|
||||
});
|
||||
|
||||
$("#modalEditUser #send").on('click', function(e){
|
||||
var form = $('#modalEditUserForm');
|
||||
form.parsley().validate();
|
||||
if (form.parsley().isValid()){
|
||||
data=$('#modalEditUserForm').serializeObject();
|
||||
data['id']=$('#modalEditUserForm #id').val();
|
||||
console.log('Editing user...')
|
||||
console.log(data)
|
||||
}
|
||||
});
|
||||
|
||||
$('.btn-delete').on('click', function () {
|
||||
var closest=$(this).closest("div").parent();
|
||||
var pk=closest.attr("data-pk");
|
||||
var username=closest.attr("data-username");
|
||||
console.log(username)
|
||||
new PNotify({
|
||||
title: 'Confirmation Needed',
|
||||
text: "Are you sure you want to delete the user: "+ username+"?",
|
||||
hide: false,
|
||||
opacity: 0.9,
|
||||
confirm: {
|
||||
confirm: true
|
||||
},
|
||||
buttons: {
|
||||
closer: false,
|
||||
sticker: false
|
||||
},
|
||||
history: {
|
||||
history: false
|
||||
},
|
||||
addclass: 'pnotify-center'
|
||||
}).get().on('pnotify.confirm', function() {
|
||||
console.log('Deleting user...')
|
||||
}).on('pnotify.cancel', function() {
|
||||
});
|
||||
});
|
||||
}
|
||||
function setUserDefault(div_id, user_id) {
|
||||
// $.ajax({
|
||||
// type: "GET",
|
||||
// url:"/api/user/" + id,
|
||||
// success: function(data)
|
||||
// {
|
||||
// $(div_id + ' #id').val(data.id);
|
||||
// $(div_id + ' #username').val(data.username);
|
||||
// $(div_id + ' #email').val(data.email);
|
||||
// $(div_id + ' #firstname').val(data.firstname);
|
||||
// $(div_id + ' #lastname').val(data.lastname);
|
||||
// $(div_id + ' .groups-select').val(data.groups);
|
||||
// $(div_id + ' .role-moodle-select').val(data.roles);
|
||||
// $(div_id + ' .role-nextcloud-select').val(data.roles);
|
||||
// $(div_id + ' .role-keycloak-select').val(data.roles);
|
||||
// $('.groups-select, .role-moodle-select, .role-nextcloud-select, .role-keycloak-select').trigger('change');
|
||||
// }
|
||||
// });
|
||||
// MOCK
|
||||
$(div_id + ' #id').val('b57c8d3f-ee08-4a1d-9873-f40c082b9c69');
|
||||
$(div_id + ' #user-avatar').attr('src', '/static/img/usera.jpg');
|
||||
$(div_id + ' #username').val('yedcaqwvt');
|
||||
$(div_id + ' #email').val('yedcaqwvt@institutmariaespinalt.cat');
|
||||
$(div_id + ' #firstname').val('Ymisno');
|
||||
$(div_id + ' #lastname').val('Edcaqwvt tavnuoes');
|
||||
$(div_id + ' .groups-select').val(['student', 'manager']);
|
||||
$(div_id + ' .role-moodle-select').val('51cc1a95-94b7-48eb-aebb-1eba6745e09f');
|
||||
$(div_id + ' .role-nextcloud-select').val('1e21ec95-b8c7-43b8-baad-1a31ad33f388');
|
||||
$(div_id + ' .role-keycloak-select').val('13da53d5-c50b-42d9-8fbf-84f2ed7cbf9e');
|
||||
$('.groups-select, .role-moodle-select, .role-nextcloud-select, .role-keycloak-select').trigger('change');
|
||||
}
|
||||
});
|
|
@ -0,0 +1,644 @@
|
|||
|
||||
$(document).on('shown.bs.modal', '#modalAddDesktop', function () {
|
||||
modal_add_desktops.columns.adjust().draw();
|
||||
});
|
||||
|
||||
$(document).ready(function() {
|
||||
|
||||
$('#bulk_actions option[value=""]').prop("selected",true);
|
||||
|
||||
update_groups();
|
||||
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
"url": "/api/roles",
|
||||
success: function(data)
|
||||
{
|
||||
data.forEach(element => {
|
||||
$(".role-moodle-select, .role-nextcloud-select, .role-keycloak-select").append(
|
||||
'<option value="' + element.name + '">' + element.name + '</option>'
|
||||
)
|
||||
})
|
||||
},
|
||||
error: function(data)
|
||||
{
|
||||
alert('Something went wrong on our side...')
|
||||
}
|
||||
});
|
||||
|
||||
$('.btn-global-resync').on('click', function () {
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
url:"/api/resync",
|
||||
success: function(data)
|
||||
{
|
||||
table.ajax.reload();
|
||||
},
|
||||
error: function(data)
|
||||
{
|
||||
alert('Something went wrong on our side...')
|
||||
table.ajax.reload();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('#bulk_actions').on('change', function () {
|
||||
action=$(this).val();
|
||||
names=''
|
||||
ids=[]
|
||||
|
||||
if(table.rows('.active').data().length){
|
||||
$.each(table.rows('.active').data(),function(key, value){
|
||||
names+=value['username']+'\n';
|
||||
ids.push({'id':value['id'],'username':value['username']});
|
||||
});
|
||||
var text = "You are about to "+action+" these users:\n\n "+names
|
||||
}else{
|
||||
$.each(table.rows({filter: 'applied'}).data(),function(key, value){
|
||||
ids.push({'id':value['id'],'username':value['username']});
|
||||
});
|
||||
var text = "You are about to "+action+" "+table.rows({filter: 'applied'}).data().length+" users!\n To all the users in list!"
|
||||
}
|
||||
console.log(ids)
|
||||
new PNotify({
|
||||
title: 'Bulk actions on users',
|
||||
text: text,
|
||||
hide: false,
|
||||
opacity: 0.9,
|
||||
confirm: {
|
||||
confirm: true
|
||||
},
|
||||
buttons: {
|
||||
closer: false,
|
||||
sticker: false
|
||||
},
|
||||
history: {
|
||||
history: false
|
||||
},
|
||||
addclass: 'pnotify-center'
|
||||
}).get().on('pnotify.confirm', function() {
|
||||
$.ajax({
|
||||
type: "PUT",
|
||||
url:"/api/users_bulk/"+$('#bulk_actions').val(),
|
||||
data: JSON.stringify(ids),
|
||||
success: function(data)
|
||||
{
|
||||
console.log('SUCCESS')
|
||||
$('#bulk_actions option[value=""]').prop("selected",true);
|
||||
table.ajax.reload();
|
||||
},
|
||||
error: function(data)
|
||||
{
|
||||
alert('Something went wrong on our side...')
|
||||
$('#bulk_actions option[value=""]').prop("selected",true);
|
||||
table.ajax.reload();
|
||||
}
|
||||
});
|
||||
}).on('pnotify.cancel', function() {
|
||||
$('#bulk_actions option[value=""]').prop("selected",true);
|
||||
});
|
||||
} );
|
||||
|
||||
// Open new user modal
|
||||
$('.btn-new-user').on('click', function () {
|
||||
$("#modalAddUserForm")[0].reset();
|
||||
update_groups();
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
"url": "/api/user_password",
|
||||
success: function(data)
|
||||
{
|
||||
$('#modalAddUser #password').val(data)
|
||||
},
|
||||
error: function(data)
|
||||
{
|
||||
alert('Something went wrong on our side...')
|
||||
}
|
||||
});
|
||||
$('#modalAddUser #enabled').prop('checked',true).iCheck('update');
|
||||
$('#modalAddUser').modal({
|
||||
backdrop: 'static',
|
||||
keyboard: false
|
||||
}).modal('show');
|
||||
});
|
||||
|
||||
//has uppercase
|
||||
window.Parsley.addValidator('uppercase', {
|
||||
requirementType: 'number',
|
||||
validateString: function(value, requirement) {
|
||||
var uppercases = value.match(/[A-Z]/g) || [];
|
||||
return uppercases.length >= requirement;
|
||||
},
|
||||
messages: {
|
||||
en: 'Your password must contain at least (%s) uppercase letter.'
|
||||
}
|
||||
});
|
||||
|
||||
//has lowercase
|
||||
window.Parsley.addValidator('lowercase', {
|
||||
requirementType: 'number',
|
||||
validateString: function(value, requirement) {
|
||||
var lowecases = value.match(/[a-z]/g) || [];
|
||||
return lowecases.length >= requirement;
|
||||
},
|
||||
messages: {
|
||||
en: 'Your password must contain at least (%s) lowercase letter.'
|
||||
}
|
||||
});
|
||||
// Send new user form
|
||||
$('#modalAddUser #send').on('click', function () {
|
||||
var form = $('#modalAddUserForm');
|
||||
form.parsley().validate();
|
||||
if (form.parsley().isValid()){
|
||||
formdata = form.serializeObject()
|
||||
// console.log('NEW USER')
|
||||
// console.log(formdata)
|
||||
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
"url": "/api/user",
|
||||
data: JSON.stringify(formdata),
|
||||
complete: function(jqXHR, textStatus) {
|
||||
switch (jqXHR.status) {
|
||||
case 200:
|
||||
$("#modalAddUser").modal('hide');
|
||||
break;
|
||||
case 409:
|
||||
new PNotify({
|
||||
title: "Add user error",
|
||||
text: $.parseJSON(jqXHR.responseText)['msg'],
|
||||
hide: true,
|
||||
delay: 3000,
|
||||
icon: 'fa fa-alert-sign',
|
||||
opacity: 1,
|
||||
type: 'error'
|
||||
});
|
||||
break;
|
||||
case 412:
|
||||
new PNotify({
|
||||
title: "Add user error",
|
||||
text: $.parseJSON(jqXHR.responseText)['msg'],
|
||||
hide: true,
|
||||
delay: 3000,
|
||||
icon: 'fa fa-alert-sign',
|
||||
opacity: 1,
|
||||
type: 'error'
|
||||
});
|
||||
break;
|
||||
default:
|
||||
alert("Server error.");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
table.ajax.reload();
|
||||
});
|
||||
|
||||
// $("#modalEditUser #send").on('click', function(e){
|
||||
// var form = $('#modalEditUserForm');
|
||||
// form.parsley().validate();
|
||||
// if (form.parsley().isValid()){
|
||||
// data=$('#modalEditUserForm').serializeObject();
|
||||
// data['id']=$('#modalEditUserForm #id').val();
|
||||
// console.log('Editing user...')
|
||||
// console.log(data)
|
||||
// }
|
||||
// });
|
||||
|
||||
$('#modalEditUser #send').on('click', function () {
|
||||
var form = $('#modalEditUserForm');
|
||||
form.parsley().validate();
|
||||
if (form.parsley().isValid()){
|
||||
formdata = form.serializeObject()
|
||||
formdata['id']=$('#modalEditUserForm #id').val();
|
||||
formdata['username']=$('#modalEditUserForm #username').val();
|
||||
console.log('UPDATE USER')
|
||||
console.log(formdata)
|
||||
$.ajax({
|
||||
type: "PUT",
|
||||
"url": "/api/user/"+formdata['id'],
|
||||
data: JSON.stringify(formdata),
|
||||
complete: function(jqXHR, textStatus) {
|
||||
table.ajax.reload();
|
||||
switch (jqXHR.status) {
|
||||
case 200:
|
||||
$("#modalEditUser").modal('hide');
|
||||
break;
|
||||
case 404:
|
||||
new PNotify({
|
||||
title: "Update user error",
|
||||
text: $.parseJSON(jqXHR.responseText)['msg'],
|
||||
hide: true,
|
||||
delay: 3000,
|
||||
icon: 'fa fa-alert-sign',
|
||||
opacity: 1,
|
||||
type: 'error'
|
||||
});
|
||||
break;
|
||||
case 409:
|
||||
new PNotify({
|
||||
title: "Add user error",
|
||||
text: $.parseJSON(jqXHR.responseText)['msg'],
|
||||
hide: true,
|
||||
delay: 3000,
|
||||
icon: 'fa fa-alert-sign',
|
||||
opacity: 1,
|
||||
type: 'error'
|
||||
});
|
||||
break;
|
||||
case 412:
|
||||
new PNotify({
|
||||
title: "Add user error",
|
||||
text: $.parseJSON(jqXHR.responseText)['msg'],
|
||||
hide: true,
|
||||
delay: 3000,
|
||||
icon: 'fa fa-alert-sign',
|
||||
opacity: 1,
|
||||
type: 'error'
|
||||
});
|
||||
break;
|
||||
default:
|
||||
alert("Server error.");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
//DataTable Main renderer
|
||||
var table = $('#users').DataTable({
|
||||
"ajax": {
|
||||
"url": "/api/users",
|
||||
"dataSrc": ""
|
||||
},
|
||||
"language": {
|
||||
"loadingRecords": '<i class="fa fa-spinner fa-pulse fa-3x fa-fw"></i><span class="sr-only">Loading...</span>',
|
||||
"emptyTable": "<h1>You don't have any user created yet.</h1><br><h2>Create one using the +Add new button on top right of this page.</h2>"
|
||||
},
|
||||
"rowId": "id",
|
||||
"deferRender": true,
|
||||
"columns": [
|
||||
// {
|
||||
// "className": 'details-control',
|
||||
// "orderable": false,
|
||||
// "data": null,
|
||||
// "width": "10px",
|
||||
// "defaultContent": '<button class="btn btn-xs btn-info" type="button" data-placement="top" ><i class="fa fa-plus"></i></button>'
|
||||
// },
|
||||
{ "data": "enabled", "width": "1px" },
|
||||
{ "data": "id", "width": "10px" },
|
||||
{ "data": "roles", "width": "10px" },
|
||||
{
|
||||
"className": 'actions-control',
|
||||
"orderable": false,
|
||||
"data": null,
|
||||
"width": "80px",
|
||||
"defaultContent": '<button id="btn-delete" class="btn btn-xs" type="button" data-placement="top" ><i class="fa fa-times" style="color:darkred"></i></button> \
|
||||
<button id="btn-password" class="btn btn-xs" type="button" data-placement="top" ><i class="fa fa-lock" style="color:orange"></i></button> \
|
||||
<button id="btn-edit" class="btn btn-xs" type="button" data-placement="top" ><i class="fa fa-pencil" style="color:darkblue"></i></button>'
|
||||
},
|
||||
{
|
||||
"className": 'text-center',
|
||||
"data": null,
|
||||
"orderable": false,
|
||||
"defaultContent": '<input type="checkbox" class="form-check-input"></input>',
|
||||
"width": "10px"
|
||||
},
|
||||
{ "data": "username", "width": "10px"},
|
||||
{ "data": "first", "width": "10px"},
|
||||
{ "data": "last", "width": "150px"},
|
||||
{ "data": "email", "width": "10px"},
|
||||
{ "data": "keycloak_groups", "width": "50px" },
|
||||
{ "data": "quota", "width": "10px", "default": "-"},
|
||||
],
|
||||
"order": [[6, 'asc']],
|
||||
"columnDefs": [ {
|
||||
"targets": 1,
|
||||
"render": function ( data, type, full, meta ) {
|
||||
// return '<object data="/static/img/missing.jpg" type="image/jpeg" width="25" height="25"><img src="/avatar/'+full.id+'" title="'+full.id+'" width="25" height="25"></object>'
|
||||
return '<img src="/avatar/'+full.id+'" title="'+full.id+'" width="25" height="25" onerror="if (this.src != \'/static/img/missing.jpg\') this.src = \'/static/img/missing.jpg\';">'
|
||||
}},
|
||||
{
|
||||
"targets": 0,
|
||||
"render": function ( data, type, full, meta ) {
|
||||
if(full.enabled){
|
||||
return '<i class="fa fa-check" style="color:lightgreen"></i>'
|
||||
}else{
|
||||
return '<i class="fa fa-close" style="color:darkred"></i>'
|
||||
};
|
||||
}},
|
||||
{
|
||||
"targets": 2,
|
||||
"render": function ( data, type, full, meta ) {
|
||||
if(full.roles.length){
|
||||
return full.roles[0][0].toUpperCase() + full.roles[0].slice(1);
|
||||
}else{
|
||||
return '-'
|
||||
}
|
||||
}},
|
||||
{
|
||||
"targets": 5,
|
||||
"render": function ( data, type, full, meta ) {
|
||||
return '<b>'+full.username+'</b>'
|
||||
}},
|
||||
{
|
||||
"targets": 9,
|
||||
"render": function ( data, type, full, meta ) {
|
||||
grups = ''
|
||||
full.keycloak_groups.forEach(element => {
|
||||
grups += '<span class="label label-primary" style="margin: 5px;">' + element + '</span>'
|
||||
})
|
||||
return grups
|
||||
}},
|
||||
{
|
||||
"targets": 10,
|
||||
"render": function ( data, type, full, meta ) {
|
||||
if(full.quota == false){
|
||||
return 'Unlimited'
|
||||
}else{
|
||||
return full.quota
|
||||
}
|
||||
}},
|
||||
]
|
||||
} );
|
||||
|
||||
table.on( 'click', 'tr', function () {
|
||||
$(this).toggleClass('active');
|
||||
if ($(this).hasClass('active')) {
|
||||
$(this).find('input').prop('checked', true);
|
||||
} else {
|
||||
$(this).find('input').prop('checked', false);
|
||||
}
|
||||
} );
|
||||
|
||||
// $template = $(".template-detail-users");
|
||||
|
||||
// $('#users').find('tbody').on('click', 'td.details-control', function () {
|
||||
// var tr = $(this).closest('tr');
|
||||
// var row = table.row( tr );
|
||||
|
||||
// if ( row.child.isShown() ) {
|
||||
// // This row is already open - close it
|
||||
// row.child.hide();
|
||||
// tr.removeClass('shown');
|
||||
// }
|
||||
// else {
|
||||
// // Close other rows
|
||||
// if ( table.row( '.shown' ).length ) {
|
||||
// $('.details-control', table.row( '.shown' ).node()).click();
|
||||
// }
|
||||
// // Open this row
|
||||
// row.child( addUserDetailPannel(row.data()) ).show();
|
||||
// tr.addClass('shown');
|
||||
// actionsUserDetail()
|
||||
// }
|
||||
// } );
|
||||
|
||||
$('#users').find(' tbody').on( 'click', 'button', function () {
|
||||
var data = table.row( $(this).parents('tr') ).data();
|
||||
// var closest=$(this).closest("div").parent();
|
||||
// var pk=closest.attr("data-pk");
|
||||
// console.log(pk)
|
||||
switch($(this).attr('id')){
|
||||
case 'btn-edit':
|
||||
$("#modalEditUserForm")[0].reset();
|
||||
$('#modalEditUser').modal({
|
||||
backdrop: 'static',
|
||||
keyboard: false
|
||||
}).modal('show');
|
||||
$('#modalEditUser #user-avatar').attr("src","/avatar/"+data.id)
|
||||
setUserDefault('#modalEditUser', data.id);
|
||||
$('#modalEdit').parsley();
|
||||
break;
|
||||
case 'btn-delete':
|
||||
new PNotify({
|
||||
title: 'Confirmation Needed',
|
||||
text: "Are you sure you want to delete user: "+data['username']+"?",
|
||||
hide: false,
|
||||
opacity: 0.9,
|
||||
confirm: {
|
||||
confirm: true
|
||||
},
|
||||
buttons: {
|
||||
closer: false,
|
||||
sticker: false
|
||||
},
|
||||
history: {
|
||||
history: false
|
||||
},
|
||||
addclass: 'pnotify-center'
|
||||
}).get().on('pnotify.confirm', function() {
|
||||
$.ajax({
|
||||
type: "DELETE",
|
||||
url:"/api/user/"+data.id,
|
||||
success: function(data)
|
||||
{
|
||||
table.ajax.reload();
|
||||
},
|
||||
error: function(data)
|
||||
{
|
||||
alert('Something went wrong on our side...')
|
||||
table.ajax.reload();
|
||||
}
|
||||
});
|
||||
}).on('pnotify.cancel', function() {
|
||||
});
|
||||
break;
|
||||
case 'btn-password':
|
||||
$("#modalPasswdUserForm")[0].reset();
|
||||
$('#modalPasswdUser').modal({
|
||||
backdrop: 'static',
|
||||
keyboard: false
|
||||
}).modal('show');
|
||||
$('#modalPasswdUserForm #id').val(data.id);
|
||||
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
url:"/api/user_password",
|
||||
success: function(data)
|
||||
{
|
||||
$('#modalPasswdUserForm #password').val(data);
|
||||
table.ajax.reload();
|
||||
},
|
||||
error: function(data)
|
||||
{
|
||||
alert('Something went wrong on our side...')
|
||||
table.ajax.reload();
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
$("#modalPasswdUser #send").on('click', function(e){
|
||||
var form = $('#modalPasswdUserForm');
|
||||
form.parsley().validate();
|
||||
if (form.parsley().isValid()){
|
||||
formdata=$('#modalPasswdUserForm').serializeObject();
|
||||
|
||||
id=$('#modalPasswdUserForm #id').val();
|
||||
$.ajax({
|
||||
type: "PUT",
|
||||
url:"/api/user_password/" + id,
|
||||
data: JSON.stringify(formdata),
|
||||
success: function(data)
|
||||
{
|
||||
$("#modalPasswdUser").modal('hide');
|
||||
table.ajax.reload();
|
||||
// groups_table.ajax.reload();
|
||||
},
|
||||
error: function(data)
|
||||
{
|
||||
alert('Something went wrong on our side...')
|
||||
table.ajax.reload();
|
||||
}
|
||||
// statusCode: {
|
||||
// 404: function(data) {
|
||||
// // {'error': 'description}. Not able to get responseJSON from received object
|
||||
// alert('User not exists in system!')
|
||||
// },
|
||||
// 200: function() {
|
||||
// console.log("Success");
|
||||
// }
|
||||
// },
|
||||
// error: function(data)
|
||||
// {
|
||||
// alert('Something went wrong on our side...')
|
||||
// }
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// function addUserDetailPannel ( d ) {
|
||||
// $newPanel = $template.clone();
|
||||
// $newPanel.html(function(i, oldHtml){
|
||||
// return oldHtml.replace(/d.id/g, d.id).replace(/d.username/g, d.username);
|
||||
// });
|
||||
// return $newPanel
|
||||
// }
|
||||
|
||||
// function actionsUserDetail(){
|
||||
|
||||
// $('.btn-passwd').on('click', function () {
|
||||
// var closest=$(this).closest("div").parent();
|
||||
// var pk=closest.attr("data-pk");
|
||||
// $("#modalPasswdUserForm")[0].reset();
|
||||
// $('#modalPasswdUser').modal({
|
||||
// backdrop: 'static',
|
||||
// keyboard: false
|
||||
// }).modal('show');
|
||||
// $('#modalPasswdUserForm #id').val(pk);
|
||||
// });
|
||||
|
||||
|
||||
|
||||
// $('.btn-edit').on('click', function () {
|
||||
// var closest=$(this).closest("div").parent();
|
||||
// var pk=closest.attr("data-pk");
|
||||
// $("#modalEditUserForm")[0].reset();
|
||||
// $('#modalEditUser').modal({
|
||||
// backdrop: 'static',
|
||||
// keyboard: false
|
||||
// }).modal('show');
|
||||
// setUserDefault('#modalEditUser', pk);
|
||||
// $('#modalEdit').parsley();
|
||||
// });
|
||||
|
||||
// $('.btn-delete').on('click', function () {
|
||||
// var closest=$(this).closest("div").parent();
|
||||
// var pk=closest.attr("data-pk");
|
||||
// var username=closest.attr("data-username");
|
||||
// console.log(username)
|
||||
// new PNotify({
|
||||
// title: 'Confirmation Needed',
|
||||
// text: "Are you sure you want to delete the user: "+ username+"?",
|
||||
// hide: false,
|
||||
// opacity: 0.9,
|
||||
// confirm: {
|
||||
// confirm: true
|
||||
// },
|
||||
// buttons: {
|
||||
// closer: false,
|
||||
// sticker: false
|
||||
// },
|
||||
// history: {
|
||||
// history: false
|
||||
// },
|
||||
// addclass: 'pnotify-center'
|
||||
// }).get().on('pnotify.confirm', function() {
|
||||
// console.log('Deleting user...')
|
||||
// }).on('pnotify.cancel', function() {
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
||||
function setUserDefault(div_id, user_id) {
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
url:"/api/user/" + user_id,
|
||||
success: function(data)
|
||||
{
|
||||
console.log(data)
|
||||
if (data.enabled) {
|
||||
$(div_id + ' #enabled').iCheck('check')
|
||||
}
|
||||
$(div_id + ' #id').val(data.id);
|
||||
$(div_id + ' #username').val(data.username);
|
||||
$(div_id + ' #email').val(data.email);
|
||||
$(div_id + ' #firstname').val(data.first);
|
||||
$(div_id + ' #lastname').val(data.last);
|
||||
if(data.quota == false){
|
||||
$(div_id + ' #quota').val('false')
|
||||
}else{
|
||||
$(div_id + ' #quota').val(data.quota);
|
||||
}
|
||||
$(div_id + ' .groups-select').val(data.keycloak_groups);
|
||||
|
||||
// $(div_id + ' .role-moodle-select').val(data.keycloak_roles);
|
||||
// $(div_id + ' .role-nextcloud-select').val(data.roles);
|
||||
$(div_id + ' .role-keycloak-select').val(data.roles[0]);
|
||||
$('.groups-select').trigger('change');
|
||||
|
||||
// $('.groups-select, .role-moodle-select, .role-nextcloud-select, .role-keycloak-select').trigger('change');
|
||||
}
|
||||
});
|
||||
// MOCK
|
||||
// $(div_id + ' #id').val('b57c8d3f-ee08-4a1d-9873-f40c082b9c69');
|
||||
// $(div_id + ' #user-avatar').attr('src', 'static/img/usera.jpg');
|
||||
// $(div_id + ' #username').val('yedcaqwvt');
|
||||
// $(div_id + ' #email').val('yedcaqwvt@institutmariaespinalt.cat');
|
||||
// $(div_id + ' #firstname').val('Ymisno');
|
||||
// $(div_id + ' #lastname').val('Edcaqwvt tavnuoes');
|
||||
// $(div_id + ' .groups-select').val(['student', 'manager']);
|
||||
// $(div_id + ' .role-moodle-select').val('51cc1a95-94b7-48eb-aebb-1eba6745e09f');
|
||||
// $(div_id + ' .role-nextcloud-select').val('1e21ec95-b8c7-43b8-baad-1a31ad33f388');
|
||||
// $(div_id + ' .role-keycloak-select').val('13da53d5-c50b-42d9-8fbf-84f2ed7cbf9e');
|
||||
// $('.groups-select, .role-moodle-select, .role-nextcloud-select, .role-keycloak-select').trigger('change');
|
||||
}
|
||||
});
|
||||
|
||||
function update_groups(){
|
||||
$(".groups-select").empty()
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
"url": "/api/groups",
|
||||
success: function(data)
|
||||
{
|
||||
data.forEach(element => {
|
||||
var groupOrigins = [];
|
||||
['keycloak'].forEach(o => {
|
||||
if (element[o]) {
|
||||
groupOrigins.push(o)
|
||||
}
|
||||
})
|
||||
$(".groups-select").append(
|
||||
'<option value="' + element.name + '">' + element.name + '</option>'
|
||||
)
|
||||
});
|
||||
$('.groups-select').select2();
|
||||
},
|
||||
error: function(data)
|
||||
{
|
||||
alert('Something went wrong on our side...')
|
||||
}
|
||||
});
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
<div class="form-group row">
|
||||
<div id="alerts"></div>
|
||||
<div
|
||||
class="btn-toolbar editor"
|
||||
data-role="editor-toolbar_{{ type }}"
|
||||
data-target="#editor-{{ type }}"
|
||||
>
|
||||
|
||||
<div class="col-md-1 col-sm-1 ">
|
||||
<select id="{{type}}-lang" class="form-control">
|
||||
{% for lang in [{'text': 'Español', 'code': 'es'}, {'text': 'English', 'code': 'en'}, {'text': 'Català', 'code': 'ca'}, {'text': 'Français', 'code': 'fr'}] %}
|
||||
<option value="{{ lang['code'] }}">{{ lang['text'] }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<a class="btn dropdown-toggle" data-toggle="dropdown" title="Font Size"
|
||||
><i class="fa fa-text-height"></i> <b class="caret"></b
|
||||
></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a data-edit="fontSize 5">
|
||||
<p style="font-size: 17px">Huge</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a data-edit="fontSize 3">
|
||||
<p style="font-size: 14px">Normal</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a data-edit="fontSize 1">
|
||||
<p style="font-size: 11px">Small</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<a class="btn" data-edit="bold" title="Bold (Ctrl/Cmd+B)"
|
||||
><i class="fa fa-bold"></i
|
||||
></a>
|
||||
<a class="btn" data-edit="italic" title="Italic (Ctrl/Cmd+I)"
|
||||
><i class="fa fa-italic"></i
|
||||
></a>
|
||||
<a class="btn" data-edit="strikethrough" title="Strikethrough"
|
||||
><i class="fa fa-strikethrough"></i
|
||||
></a>
|
||||
<a class="btn" data-edit="underline" title="Underline (Ctrl/Cmd+U)"
|
||||
><i class="fa fa-underline"></i
|
||||
></a>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<a class="btn" data-edit="insertunorderedlist" title="Bullet list"
|
||||
><i class="fa fa-list-ul"></i
|
||||
></a>
|
||||
<a class="btn" data-edit="insertorderedlist" title="Number list"
|
||||
><i class="fa fa-list-ol"></i
|
||||
></a>
|
||||
<a class="btn" data-edit="outdent" title="Reduce indent (Shift+Tab)"
|
||||
><i class="fa fa-dedent"></i
|
||||
></a>
|
||||
<a class="btn" data-edit="indent" title="Indent (Tab)"
|
||||
><i class="fa fa-indent"></i
|
||||
></a>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<a class="btn" data-edit="justifyleft" title="Align Left (Ctrl/Cmd+L)"
|
||||
><i class="fa fa-align-left"></i
|
||||
></a>
|
||||
<a class="btn" data-edit="justifycenter" title="Center (Ctrl/Cmd+E)"
|
||||
><i class="fa fa-align-center"></i
|
||||
></a>
|
||||
<a class="btn" data-edit="justifyright" title="Align Right (Ctrl/Cmd+R)"
|
||||
><i class="fa fa-align-right"></i
|
||||
></a>
|
||||
<a class="btn" data-edit="justifyfull" title="Justify (Ctrl/Cmd+J)"
|
||||
><i class="fa fa-align-justify"></i
|
||||
></a>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<a class="btn" data-edit="undo" title="Undo (Ctrl/Cmd+Z)"
|
||||
><i class="fa fa-undo"></i
|
||||
></a>
|
||||
<a class="btn" data-edit="redo" title="Redo (Ctrl/Cmd+Y)"
|
||||
><i class="fa fa-repeat"></i
|
||||
></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="editor-{{ type }}" type="{{ type }}" class="editor-wrapper"></div>
|
||||
|
||||
<textarea name="descr" id="descr" style="display: none"></textarea>
|
||||
</div>
|
|
@ -0,0 +1,138 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<!-- Meta, title, CSS, favicons, etc. -->
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta http-equiv="no-cache">
|
||||
<meta http-equiv="Expires" content="-1">
|
||||
<meta http-equiv="Cache-Control" content="no-cache">
|
||||
|
||||
<title>{{ title }} | Digital Democratic</title>
|
||||
<link rel="icon" type="image/png" href="favicon-32x32.png" sizes="32x32" />
|
||||
<link rel="icon" type="image/png" href="favicon-16x16.png" sizes="16x16" />
|
||||
<!-- Fancytree -->
|
||||
<link href="/static/vendor/fancytree/dist/skin-win8/ui.fancytree.css" rel="stylesheet">
|
||||
<!-- Bootstrap -->
|
||||
<link href="/vendors/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Font Awesome -->
|
||||
<link href="/vendors/font-awesome/css/font-awesome.min.css" rel="stylesheet">
|
||||
<!-- ion.rangeSlider -->
|
||||
<link href="/vendors/ion.rangeSlider/css/ion.rangeSlider.css" rel="stylesheet">
|
||||
<link href="/vendors/ion.rangeSlider/css/ion.rangeSlider.skinFlat.css" rel="stylesheet">
|
||||
<!-- Datatables -->
|
||||
<link href="/vendors/datatables.net-bs/css/dataTables.bootstrap.min.css" rel="stylesheet">
|
||||
<!-- PNotify -->
|
||||
<link href="/vendors/pnotify/dist/pnotify.css" media="all" rel="stylesheet" type="text/css" />
|
||||
<link href="/vendors/pnotify/dist/pnotify.buttons.css" media="all" rel="stylesheet" type="text/css" />
|
||||
<!-- iCheck -->
|
||||
<link href="/vendors/iCheck/skins/flat/green.css" rel="stylesheet">
|
||||
<link href="/vendors/select2/dist/css/select2.min.css" rel="stylesheet">
|
||||
{% block css %}{% endblock %}
|
||||
<!-- Custom Theme Style -->
|
||||
<link href="/build/css/custom.css" rel="stylesheet">
|
||||
<!-- Isard Style Sheet-->
|
||||
<link href="/static/dd.css" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body class="nav-md">
|
||||
<div class="container body" style="margin-top: 50px;">
|
||||
<div class="main_container">
|
||||
<div class="col-md-3 left_col">
|
||||
<div class="left_col scroll-view">
|
||||
{% include 'sidebar.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<!-- top navigation -->
|
||||
{% include 'header.html' %}
|
||||
<!-- /top navigation -->
|
||||
<div class="right_col" role="main">
|
||||
<!-- page content -->
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
<!-- /page content -->
|
||||
</div>
|
||||
<!-- footer content -->
|
||||
{% include 'footer.html' %}
|
||||
<!-- /footer content -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'pages/modals/common_modals.html' %}
|
||||
|
||||
|
||||
<!-- jQuery -->
|
||||
<script src="/vendors/jquery/dist/jquery.js"></script>
|
||||
<!-- Bootstrap -->
|
||||
<script src="/vendors/bootstrap/dist/js/bootstrap.min.js"></script>
|
||||
<!-- NProgress -->
|
||||
<script src="/vendors/nprogress/nprogress.js"></script>
|
||||
<!-- Datatables -->
|
||||
<script src="/vendors/datatables.net/js/jquery.dataTables.min.js"></script>
|
||||
<script src="/vendors/datatables.net-bs/js/dataTables.bootstrap.min.js"></script>
|
||||
<!-- Ion.RangeSlider -->
|
||||
<script src="/vendors/ion.rangeSlider/js/ion.rangeSlider.min.js"></script>
|
||||
<!-- PNotify -->
|
||||
<script type="text/javascript" src="/vendors/pnotify/dist/pnotify.js"></script>
|
||||
<script type="text/javascript" src="/vendors/pnotify/dist/pnotify.confirm.js"></script>
|
||||
<script type="text/javascript" src="/vendors/pnotify/dist/pnotify.buttons.js"></script>
|
||||
<!-- validator -->
|
||||
<script src="/vendors/validator/validator.js"></script>
|
||||
<!-- Parsley -->
|
||||
<script src="/vendors/parsleyjs/dist/parsley.min.js"></script>
|
||||
<!-- moment -->
|
||||
<script src="/vendors/moment/min/moment.min.js"></script>
|
||||
<!-- validator -->
|
||||
<script src="/vendors/iCheck/icheck.min.js"></script>
|
||||
<!-- bootstrap-progressbar -->
|
||||
<script src="/vendors/bootstrap-progressbar/bootstrap-progressbar.min.js"></script>
|
||||
<!-- ECharts -->
|
||||
<script src="/vendors/echarts/dist/echarts.min.js"></script>
|
||||
<!-- Select2 -->
|
||||
<script src="/vendors/select2/dist/js/select2.full.min.js"></script>
|
||||
<!-- SocketIO -->
|
||||
<script src="/node_modules/socket.io/client-dist/socket.io.min.js"></script>
|
||||
<script src="/static/js/status_socket.js"></script>
|
||||
|
||||
<!-- isard initializers -->
|
||||
<script src="/static/dd.js"></script>
|
||||
<!-- Requirements for fancy tree -->
|
||||
<script src="/static/vendor/fancytree/src/jquery-ui-dependencies/jquery-ui.min.js"></script>
|
||||
<script src="/static/vendor/fancytree/dist/jquery.fancytree.min.js"></script>
|
||||
<script src="/static/vendor/fancytree/src/jquery.fancytree.table.js"></script>
|
||||
<!-- Header render -->
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
url: "https://api."+document.domain.split(/\.(.+)/)[1]+"/header/html/admin",
|
||||
success: function(data) {
|
||||
$('#header').html(data)
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
<!-- flashed messages with pnotify -->
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<script type="text/javascript">
|
||||
new PNotify({
|
||||
title: "{{ nav }}",
|
||||
text: "{{ message }}",
|
||||
hide: true,
|
||||
delay: 2000,
|
||||
/~ icon: 'fa fa-alert-sign',
|
||||
opacity: 1,
|
||||
type: "{{ category }}",
|
||||
addclass: "pnotify-center"
|
||||
});
|
||||
</script>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% block pagescript %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,6 @@
|
|||
<footer>
|
||||
<div class="pull-right">
|
||||
Digital Democratic - Administration | <a href="https:/gitlab.com/digitaldemocratic/digitaldemocratic">gitlab</a>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</footer>
|
|
@ -0,0 +1,2 @@
|
|||
<div id="header">
|
||||
</div>
|
|
@ -0,0 +1,88 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<!-- Meta, title, CSS, favicons, etc. -->
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<title>Login | Digital Democratic</title>
|
||||
|
||||
<!-- Bootstrap -->
|
||||
<link href="/vendors/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Font Awesome -->
|
||||
<link href="/vendors/font-awesome/css/font-awesome.min.css" rel="stylesheet">
|
||||
<!-- Animate.css -->
|
||||
<link href="/vendors/animate.css/animate.min.css" rel="stylesheet">
|
||||
<!-- PNotify -->
|
||||
<link href="/vendors/pnotify/dist/pnotify.css" media="all" rel="stylesheet" type="text/css" />
|
||||
<link href="/vendors/pnotify/dist/pnotify.buttons.css" media="all" rel="stylesheet" type="text/css" />
|
||||
<!-- Custom Theme Style -->
|
||||
<link href="/build/css/custom.min.css" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body class="login">
|
||||
<div>
|
||||
<a class="hiddenanchor" id="signup"></a>
|
||||
<a class="hiddenanchor" id="signin"></a>
|
||||
<div class="login_wrapper">
|
||||
<div class="animate form login_form">
|
||||
<section class="login_content">
|
||||
<form id="login-form" action="{{ url_for('login') }}" method="POST" novalidate>
|
||||
<img src="https://nextcloud.digitaldemocratic.net/themes/digitaldemocratic/core/img/dd.svg" height="75px">
|
||||
<h1></h1>
|
||||
<div>
|
||||
<input type="text" name="user" class="form-control" placeholder="Username" required="" autofocus />
|
||||
</div>
|
||||
<div>
|
||||
<input type="password" name="password" class="form-control" placeholder="Password" required="" />
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class="btn btn-default submit">Login</button>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
<div class="separator">
|
||||
<div class="clearfix"></div>
|
||||
<br />
|
||||
|
||||
<div>
|
||||
<!-- <h1><i class="fa fa-user"></i> Digital Democratic</h1> -->
|
||||
<p>©2022 All Rights Reserved. <a href="https://gitlab.com/digitaldemocratic/digitaldemocratic/-/blob/master/LICENSE" target="_blank">AGPLv3</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<!-- jQuery -->
|
||||
<script src="/vendors/jquery/dist/jquery.min.js"></script>
|
||||
<!-- PNotify -->
|
||||
<script type="text/javascript" src="/vendors/pnotify/dist/pnotify.js"></script>
|
||||
<script type="text/javascript" src="/vendors/pnotify/dist/pnotify.confirm.js"></script>
|
||||
<script type="text/javascript" src="/vendors/pnotify/dist/pnotify.buttons.js"></script>
|
||||
<script>PNotify.prototype.options.styling = "bootstrap3";</script>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<script type="text/javascript">
|
||||
new PNotify({
|
||||
title: "{{ nav }}",
|
||||
text: "{{ message }}",
|
||||
hide: true,
|
||||
// icon: 'fa fa-alert-sign',
|
||||
opacity: 1,
|
||||
type: "error",
|
||||
addclass: "pnotify-center"
|
||||
});
|
||||
</script>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<!-- Isard restful ajax calls -->
|
||||
<script src="/static/js/restful.js"></script>
|
||||
</html>
|
|
@ -0,0 +1,69 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<!-- Meta, title, CSS, favicons, etc. -->
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<title>Page not found! | Digital Democratic</title>
|
||||
|
||||
<!-- Bootstrap -->
|
||||
<link href="../vendors/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Font Awesome -->
|
||||
<link href="../vendors/font-awesome/css/font-awesome.min.css" rel="stylesheet">
|
||||
<!-- NProgress -->
|
||||
<link href="../vendors/nprogress/nprogress.css" rel="stylesheet">
|
||||
|
||||
<!-- Custom Theme Style -->
|
||||
<link href="../build/css/custom.min.css" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body class="nav-md">
|
||||
<div class="container body">
|
||||
<div class="main_container">
|
||||
<!-- page content -->
|
||||
<div class="col-md-12">
|
||||
<div class="col-middle">
|
||||
<div class="text-center text-center">
|
||||
<h1 class="error-number">404</h1>
|
||||
<h2>Sorry but we couldn't find this page</h2>
|
||||
<p>This page you are looking for does not exist <a href="https:/gitlab.com/digitaldemocratic/digitaldemocratic">Report this?</a>
|
||||
<a href="/login">Go back to login page</a>
|
||||
</p>
|
||||
<!--
|
||||
<div class="mid_center">
|
||||
<h3>Search</h3>
|
||||
<form>
|
||||
<div class="col-xs-12 form-group pull-right top_search">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" placeholder="Search for...">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" type="button">Go!</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /page content -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- jQuery -->
|
||||
<script src="../vendors/jquery/dist/jquery.min.js"></script>
|
||||
<!-- Bootstrap -->
|
||||
<script src="../vendors/bootstrap/dist/js/bootstrap.min.js"></script>
|
||||
<!-- FastClick -->
|
||||
<script src="../vendors/fastclick/lib/fastclick.js"></script>
|
||||
<!-- NProgress -->
|
||||
<script src="../vendors/nprogress/nprogress.js"></script>
|
||||
|
||||
<!-- Custom Theme Scripts -->
|
||||
<script src="../build/js/custom.min.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,69 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<!-- Meta, title, CSS, favicons, etc. -->
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<title>Page not allowed! | Digital Democratic</title>
|
||||
|
||||
<!-- Bootstrap -->
|
||||
<link href="../vendors/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Font Awesome -->
|
||||
<link href="../vendors/font-awesome/css/font-awesome.min.css" rel="stylesheet">
|
||||
<!-- NProgress -->
|
||||
<link href="../vendors/nprogress/nprogress.css" rel="stylesheet">
|
||||
|
||||
<!-- Custom Theme Style -->
|
||||
<link href="../build/css/custom.min.css" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body class="nav-md">
|
||||
<div class="container body">
|
||||
<div class="main_container">
|
||||
<!-- page content -->
|
||||
<div class="col-md-12">
|
||||
<div class="col-middle">
|
||||
<div class="text-center">
|
||||
<h1 class="error-number">500</h1>
|
||||
<h2>Internal Server Error</h2>
|
||||
<p>We track these errors automatically, but if the problem persists feel free to contact us. In the meantime, try refreshing. <a href="https:/gitlab.com/digitaldemocratic/digitaldemocratic">Report this?</a>
|
||||
<a href="/login">Go back to login page</a>
|
||||
</p>
|
||||
<!--
|
||||
<div class="mid_center">
|
||||
<h3>Search</h3>
|
||||
<form>
|
||||
<div class="col-xs-12 form-group pull-right top_search">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" placeholder="Search for...">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" type="button">Go!</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /page content -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- jQuery -->
|
||||
<script src="../vendors/jquery/dist/jquery.min.js"></script>
|
||||
<!-- Bootstrap -->
|
||||
<script src="../vendors/bootstrap/dist/js/bootstrap.min.js"></script>
|
||||
<!-- FastClick -->
|
||||
<script src="../vendors/fastclick/lib/fastclick.js"></script>
|
||||
<!-- NProgress -->
|
||||
<script src="../vendors/nprogress/nprogress.js"></script>
|
||||
|
||||
<!-- Custom Theme Scripts -->
|
||||
<script src="../build/js/custom.min.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,57 @@
|
|||
<!-- extend base layout -->
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-12 col-sm-12 col-xs-12">
|
||||
<div class="x_panel">
|
||||
<div class="container for-about text-center" style="margin-top: 15px;">
|
||||
<img src="/static/img/dd.svg" width="250px" height="250px">
|
||||
<h1>Digital Democratic</h1>
|
||||
<h1><small>Schools apps integrations</small></h1>
|
||||
<div class="row" style="margin-top: 40px;">
|
||||
<div class="col-lg-2 col-md-12 col-sm-12 col-xs-12"></div>
|
||||
<div class="col-lg-2 col-md-6 col-sm-6 col-xs-12">
|
||||
<a href="https:/gitlab.com/digitaldemocratic/digitaldemocratic" target="_blank" style="color: deepskyblue">
|
||||
<i class="fa fa-globe" style="font-size: 125px;" aria-hidden="true"></i>
|
||||
<h1><small>Visit website</small></h1>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-lg-2 col-md-6 col-sm-6 col-xs-12">
|
||||
<a href="https:/gitlab.com/digitaldemocratic/digitaldemocratic/-/issues" target="_blank" style="color: orange">
|
||||
<i class="fa fa-gitlab fa-5x" style="font-size: 125px;" aria-hidden="true"></i>
|
||||
<h1><small>Open an issue</small></h1>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-lg-2 col-md-12 col-sm-12 col-xs-12"></div>
|
||||
</div>
|
||||
<div class="row" style="margin-top: 25px;">
|
||||
<div class="col-md-4 col-sm-4 col-xs-4"></div>
|
||||
<div class="col-md-2 col-sm-2 col-xs-12">
|
||||
<i class="fa fa-envelope-o fa-5x" style="font-size: 125px;" aria-hidden="true"></i>
|
||||
<h1>
|
||||
<small>
|
||||
Contact us at:
|
||||
<br/>
|
||||
info@digitaldemocratic.net
|
||||
</small>
|
||||
</h1>
|
||||
</div>
|
||||
<div class="col-md-2 col-sm-6 col-xs-12">
|
||||
<p><img src="/static/img/agplv3-155x51.png" style="margin-top: 60px;"></p>
|
||||
<h1 style="margin-top: 25px;"><small>License</small></h1>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-4 col-xs-4"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block pagescript %}
|
||||
<script src="/static/js/restful.js"></script>
|
||||
|
||||
<script src="/static/js/status_socket.js"></script>
|
||||
{% endblock %}
|
|
@ -0,0 +1,436 @@
|
|||
<!-- extend base layout -->
|
||||
{% extends "base.html" %}
|
||||
{% block css %}
|
||||
<!-- Ion.RangeSlider -->
|
||||
<link href="/vendors/normalize-css/normalize.css" rel="stylesheet">
|
||||
<!-- Switchery -->
|
||||
<link href="/vendors/switchery/dist/switchery.min.css" rel="stylesheet">
|
||||
<!-- Bootstrap Colorpicker -->
|
||||
<link href="/vendors/mjolnic-bootstrap-colorpicker/dist/css/bootstrap-colorpicker.min.css" rel="stylesheet">
|
||||
<link href="/vendors/cropper/dist/cropper.min.css" rel="stylesheet">
|
||||
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
{% set icons = [ 'fa-500px', 'fa-address-book', 'fa-address-book-o', 'fa-address-card', 'fa-address-card-o', 'fa-adjust', 'fa-adn',
|
||||
'fa-align-center', 'fa-align-justify', 'fa-align-left', 'fa-align-right', 'fa-amazon', 'fa-ambulance', 'fa-american-sign-language-interpreting',
|
||||
'fa-anchor', 'fa-android', 'fa-angellist', 'fa-angle-double-down', 'fa-angle-double-left', 'fa-angle-double-right', 'fa-angle-double-up',
|
||||
'fa-angle-down', 'fa-angle-left', 'fa-angle-right', 'fa-angle-up', 'fa-apple', 'fa-archive', 'fa-area-chart', 'fa-arrow-circle-down',
|
||||
'fa-arrow-circle-left', 'fa-arrow-circle-o-down', 'fa-arrow-circle-o-left', 'fa-arrow-circle-o-right', 'fa-arrow-circle-o-up', 'fa-arrow-circle-right',
|
||||
'fa-arrow-circle-up', 'fa-arrow-down', 'fa-arrow-left', 'fa-arrow-right', 'fa-arrow-up', 'fa-arrows', 'fa-arrows-alt', 'fa-arrows-h',
|
||||
'fa-arrows-v', 'fa-asl-interpreting', 'fa-assistive-listening-systems', 'fa-asterisk', 'fa-at', 'fa-audio-description', 'fa-automobile',
|
||||
'fa-backward', 'fa-balance-scale', 'fa-ban', 'fa-bandcamp', 'fa-bank', 'fa-bar-chart', 'fa-bar-chart-o', 'fa-barcode', 'fa-bars', 'fa-bath',
|
||||
'fa-bathtub', 'fa-battery', 'fa-battery-0', 'fa-battery-1', 'fa-battery-2', 'fa-battery-3', 'fa-battery-4', 'fa-battery-empty',
|
||||
'fa-battery-full', 'fa-battery-half', 'fa-battery-quarter', 'fa-battery-three-quarters', 'fa-bed', 'fa-beer', 'fa-behance', 'fa-behance-square', 'fa-bell', 'fa-bell-o', 'fa-bell-slash', 'fa-bell-slash-o', 'fa-bicycle', 'fa-binoculars', 'fa-birthday-cake', 'fa-bitbucket', 'fa-bitbucket-square', 'fa-bitcoin', 'fa-black-tie', 'fa-blind', 'fa-bluetooth', 'fa-bold', 'fa-bolt', 'fa-bomb', 'fa-book', 'fa-bookmark', 'fa-bookmark-o', 'fa-braille', 'fa-briefcase', 'fa-btc', 'fa-bug', 'fa-building', 'fa-building-o', 'fa-bullhorn', 'fa-bullseye', 'fa-bus', 'fa-buysellads', 'fa-cab', 'fa-calculator', 'fa-calendar', 'fa-calendar-check-o', 'fa-calendar-minus-o', 'fa-calendar-o', 'fa-calendar-plus-o', 'fa-calendar-times-o', 'fa-camera', 'fa-camera-retro',
|
||||
'fa-car', 'fa-caret-down', 'fa-caret-left', 'fa-caret-right', 'fa-caret-square-o-down', 'fa-caret-square-o-left', 'fa-caret-square-o-right',
|
||||
'fa-caret-square-o-up', 'fa-caret-up', 'fa-cart-arrow-down', 'fa-cart-plus', 'fa-cc', 'fa-cc-amex', 'fa-cc-diners-club', 'fa-cc-discover',
|
||||
'fa-cc-jcb', 'fa-cc-mastercard', 'fa-cc-paypal', 'fa-cc-stripe', 'fa-cc-visa', 'fa-certificate', 'fa-chain', 'fa-chain-broken', 'fa-check',
|
||||
'fa-check-circle', 'fa-check-circle-o', 'fa-check-square', 'fa-check-square-o', 'fa-chevron-circle-down', 'fa-chevron-circle-left',
|
||||
'fa-chevron-circle-right', 'fa-chevron-circle-up', 'fa-chevron-down', 'fa-chevron-left', 'fa-chevron-right', 'fa-chevron-up', 'fa-child',
|
||||
'fa-chrome', 'fa-circle', 'fa-circle-o', 'fa-circle-o-notch', 'fa-circle-thin', 'fa-clipboard', 'fa-clock-o', 'fa-clone', 'fa-close',
|
||||
'fa-cloud', 'fa-cloud-download', 'fa-cloud-upload', 'fa-cny', 'fa-code', 'fa-code-fork', 'fa-codepen', 'fa-codiepie', 'fa-coffee', 'fa-cog',
|
||||
'fa-cogs', 'fa-columns', 'fa-comment', 'fa-comment-o', 'fa-commenting', 'fa-commenting-o', 'fa-comments', 'fa-comments-o', 'fa-compass',
|
||||
'fa-compress', 'fa-connectdevelop', 'fa-contao', 'fa-copy', 'fa-copyright', 'fa-creative-commons', 'fa-credit-card', 'fa-credit-card-alt',
|
||||
'fa-crop', 'fa-crosshairs', 'fa-css3', 'fa-cube', 'fa-cubes', 'fa-cut', 'fa-cutlery', 'fa-dashboard', 'fa-dashcube', 'fa-database', 'fa-deaf', 'fa-deafness', 'fa-dedent', 'fa-delicious', 'fa-desktop', 'fa-deviantart', 'fa-diamond', 'fa-digg', 'fa-dollar', 'fa-dot-circle-o', 'fa-download', 'fa-dribbble', 'fa-drivers-license', 'fa-drivers-license-o', 'fa-dropbox', 'fa-drupal', 'fa-edge', 'fa-edit', 'fa-eercast', 'fa-eject', 'fa-ellipsis-h', 'fa-ellipsis-v', 'fa-empire', 'fa-envelope', 'fa-envelope-o', 'fa-envelope-open',
|
||||
'fa-envelope-open-o', 'fa-envelope-square', 'fa-envira', 'fa-eraser', 'fa-etsy', 'fa-eur', 'fa-euro', 'fa-exchange', 'fa-exclamation',
|
||||
'fa-exclamation-circle', 'fa-exclamation-triangle', 'fa-expand', 'fa-expeditedssl', 'fa-external-link', 'fa-external-link-square', 'fa-eye',
|
||||
'fa-eye-slash', 'fa-eyedropper', 'fa-fa', 'fa-facebook', 'fa-facebook-f', 'fa-facebook-official', 'fa-facebook-square', 'fa-fast-backward',
|
||||
'fa-fast-forward', 'fa-fax', 'fa-feed', 'fa-female', 'fa-fighter-jet', 'fa-file', 'fa-file-archive-o', 'fa-file-audio-o', 'fa-file-code-o',
|
||||
'fa-file-excel-o', 'fa-file-image-o', 'fa-file-movie-o', 'fa-file-o', 'fa-file-pdf-o', 'fa-file-photo-o', 'fa-file-picture-o',
|
||||
'fa-file-powerpoint-o', 'fa-file-sound-o', 'fa-file-text', 'fa-file-text-o', 'fa-file-video-o', 'fa-file-word-o', 'fa-file-zip-o',
|
||||
'fa-files-o', 'fa-film', 'fa-filter', 'fa-fire', 'fa-fire-extinguisher', 'fa-firefox', 'fa-first-order', 'fa-flag', 'fa-flag-checkered',
|
||||
'fa-flag-o', 'fa-flash', 'fa-flask', 'fa-flickr', 'fa-floppy-o', 'fa-folder', 'fa-folder-o', 'fa-folder-open', 'fa-folder-open-o', 'fa-font',
|
||||
'fa-font-awesome', 'fa-fonticons', 'fa-fort-awesome', 'fa-forumbee', 'fa-forward', 'fa-foursquare', 'fa-free-code-camp', 'fa-frown-o',
|
||||
'fa-futbol-o', 'fa-gamepad', 'fa-gavel', 'fa-gbp', 'fa-ge', 'fa-gear', 'fa-gears', 'fa-genderless', 'fa-get-pocket', 'fa-gg', 'fa-gg-circle',
|
||||
'fa-gift', 'fa-git', 'fa-git-square', 'fa-github', 'fa-github-alt', 'fa-github-square', 'fa-gitlab', 'fa-gittip', 'fa-glass', 'fa-glide',
|
||||
'fa-glide-g', 'fa-globe', 'fa-google', 'fa-google-plus', 'fa-google-plus-circle', 'fa-google-plus-official', 'fa-google-plus-square',
|
||||
'fa-google-wallet', 'fa-graduation-cap', 'fa-gratipay', 'fa-grav', 'fa-group', 'fa-h-square', 'fa-hacker-news', 'fa-hand-grab-o',
|
||||
'fa-hand-lizard-o', 'fa-hand-o-down', 'fa-hand-o-left', 'fa-hand-o-right', 'fa-hand-o-up', 'fa-hand-paper-o', 'fa-hand-peace-o',
|
||||
'fa-hand-pointer-o', 'fa-hand-rock-o', 'fa-hand-scissors-o', 'fa-hand-spock-o', 'fa-hand-stop-o', 'fa-handshake-o', 'fa-hard-of-hearing',
|
||||
'fa-hashtag', 'fa-hdd-o', 'fa-header', 'fa-headphones', 'fa-heart', 'fa-heart-o', 'fa-heartbeat', 'fa-history', 'fa-home', 'fa-hospital-o',
|
||||
'fa-hotel', 'fa-hourglass', 'fa-hourglass-1', 'fa-hourglass-2', 'fa-hourglass-3', 'fa-hourglass-end', 'fa-hourglass-half', 'fa-hourglass-o',
|
||||
'fa-hourglass-start', 'fa-houzz', 'fa-html5', 'fa-i-cursor', 'fa-id-badge', 'fa-id-card', 'fa-id-card-o', 'fa-ils', 'fa-image', 'fa-imdb',
|
||||
'fa-inbox', 'fa-indent', 'fa-industry', 'fa-info', 'fa-info-circle', 'fa-inr', 'fa-instagram', 'fa-institution', 'fa-internet-explorer',
|
||||
'fa-intersex', 'fa-ioxhost', 'fa-italic', 'fa-joomla', 'fa-jpy', 'fa-jsfiddle', 'fa-key', 'fa-keyboard-o', 'fa-krw', 'fa-language',
|
||||
'fa-laptop', 'fa-lastfm', 'fa-lastfm-square', 'fa-leaf', 'fa-leanpub', 'fa-legal', 'fa-lemon-o', 'fa-level-down', 'fa-level-up',
|
||||
'fa-life-bouy', 'fa-life-buoy', 'fa-life-ring', 'fa-life-saver', 'fa-lightbulb-o', 'fa-line-chart', 'fa-link', 'fa-linkedin',
|
||||
'fa-linkedin-square', 'fa-linode', 'fa-linux', 'fa-list', 'fa-list-alt', 'fa-list-ol', 'fa-list-ul', 'fa-location-arrow', 'fa-lock',
|
||||
'fa-long-arrow-down', 'fa-long-arrow-left', 'fa-long-arrow-right', 'fa-long-arrow-up', 'fa-low-vision', 'fa-magic', 'fa-magnet',
|
||||
'fa-mail-forward', 'fa-mail-reply', 'fa-mail-reply-all', 'fa-male', 'fa-map', 'fa-map-marker', 'fa-map-o', 'fa-map-pin', 'fa-map-signs',
|
||||
'fa-mars', 'fa-mars-double', 'fa-mars-stroke', 'fa-mars-stroke-h', 'fa-mars-stroke-v', 'fa-maxcdn', 'fa-meanpath', 'fa-medium', 'fa-medkit',
|
||||
'fa-meetup', 'fa-meh-o', 'fa-mercury', 'fa-microchip', 'fa-microphone', 'fa-microphone-slash', 'fa-minus', 'fa-minus-circle', 'fa-minus-square',
|
||||
'fa-minus-square-o', 'fa-mixcloud', 'fa-mobile', 'fa-mobile-phone', 'fa-modx', 'fa-money', 'fa-moon-o', 'fa-mortar-board', 'fa-motorcycle', 'fa-mouse-pointer', 'fa-music', 'fa-navicon', 'fa-neuter', 'fa-newspaper-o', 'fa-object-group', 'fa-object-ungroup',
|
||||
'fa-odnoklassniki', 'fa-odnoklassniki-square', 'fa-opencart', 'fa-openid', 'fa-opera', 'fa-optin-monster', 'fa-outdent', 'fa-pagelines',
|
||||
'fa-paint-brush', 'fa-paper-plane', 'fa-paper-plane-o', 'fa-paperclip', 'fa-paragraph', 'fa-paste', 'fa-pause', 'fa-pause-circle',
|
||||
'fa-pause-circle-o', 'fa-paw', 'fa-paypal', 'fa-pencil', 'fa-pencil-square', 'fa-pencil-square-o', 'fa-percent', 'fa-phone', 'fa-phone-square',
|
||||
'fa-photo', 'fa-picture-o', 'fa-pie-chart', 'fa-pied-piper', 'fa-pied-piper-alt', 'fa-pied-piper-pp', 'fa-pinterest', 'fa-pinterest-p',
|
||||
'fa-pinterest-square', 'fa-plane', 'fa-play', 'fa-play-circle', 'fa-play-circle-o', 'fa-plug', 'fa-plus', 'fa-plus-circle', 'fa-plus-square',
|
||||
'fa-plus-square-o', 'fa-podcast', 'fa-power-off', 'fa-print', 'fa-product-hunt', 'fa-puzzle-piece', 'fa-qq', 'fa-qrcode', 'fa-question',
|
||||
'fa-question-circle', 'fa-question-circle-o', 'fa-quora', 'fa-quote-left', 'fa-quote-right', 'fa-ra', 'fa-random', 'fa-ravelry', 'fa-rebel',
|
||||
'fa-recycle', 'fa-reddit', 'fa-reddit-alien', 'fa-reddit-square', 'fa-refresh', 'fa-registered', 'fa-remove', 'fa-renren', 'fa-reorder',
|
||||
'fa-repeat', 'fa-reply', 'fa-reply-all', 'fa-resistance', 'fa-retweet', 'fa-rmb', 'fa-road', 'fa-rocket', 'fa-rotate-left', 'fa-rotate-right',
|
||||
'fa-rouble', 'fa-rss', 'fa-rss-square', 'fa-rub', 'fa-ruble', 'fa-rupee', 'fa-s15', 'fa-safari', 'fa-save', 'fa-scissors', 'fa-scribd',
|
||||
'fa-search', 'fa-search-minus', 'fa-search-plus', 'fa-sellsy', 'fa-send', 'fa-send-o', 'fa-server', 'fa-share', 'fa-share-alt',
|
||||
'fa-share-alt-square', 'fa-share-square', 'fa-share-square-o', 'fa-shekel', 'fa-sheqel', 'fa-shield', 'fa-ship', 'fa-shirtsinbulk',
|
||||
'fa-shopping-bag', 'fa-shopping-basket', 'fa-shopping-cart', 'fa-shower', 'fa-sign-in', 'fa-sign-language', 'fa-sign-out', 'fa-signal',
|
||||
'fa-signing', 'fa-simplybuilt', 'fa-sitemap', 'fa-skyatlas', 'fa-skype', 'fa-slack', 'fa-sliders', 'fa-slideshare', 'fa-smile-o', 'fa-snapchat',
|
||||
'fa-snapchat-ghost', 'fa-snapchat-square', 'fa-snowflake-o', 'fa-soccer-ball-o', 'fa-sort', 'fa-sort-alpha-asc', 'fa-sort-alpha-desc',
|
||||
'fa-sort-amount-asc', 'fa-sort-amount-desc', 'fa-sort-asc', 'fa-sort-desc', 'fa-sort-down', 'fa-sort-numeric-asc', 'fa-sort-numeric-desc',
|
||||
'fa-sort-up', 'fa-soundcloud', 'fa-space-shuttle', 'fa-spinner', 'fa-spoon', 'fa-spotify', 'fa-square', 'fa-square-o', 'fa-stack-exchange',
|
||||
'fa-stack-overflow', 'fa-star', 'fa-star-half', 'fa-star-half-empty', 'fa-star-half-full', 'fa-star-half-o', 'fa-star-o', 'fa-steam',
|
||||
'fa-steam-square', 'fa-step-backward', 'fa-step-forward', 'fa-stethoscope', 'fa-sticky-note', 'fa-sticky-note-o', 'fa-stop', 'fa-stop-circle',
|
||||
'fa-stop-circle-o', 'fa-street-view', 'fa-strikethrough', 'fa-stumbleupon', 'fa-stumbleupon-circle', 'fa-subscript', 'fa-subway', 'fa-suitcase',
|
||||
'fa-sun-o', 'fa-superpowers', 'fa-superscript', 'fa-support', 'fa-table', 'fa-tablet', 'fa-tachometer', 'fa-tag', 'fa-tags', 'fa-tasks',
|
||||
'fa-taxi', 'fa-telegram', 'fa-television', 'fa-tencent-weibo', 'fa-terminal', 'fa-text-height', 'fa-text-width', 'fa-th', 'fa-th-large',
|
||||
'fa-th-list', 'fa-themeisle', 'fa-thermometer', 'fa-thermometer-0', 'fa-thermometer-1', 'fa-thermometer-2', 'fa-thermometer-3', 'fa-thermometer-4',
|
||||
'fa-thermometer-empty', 'fa-thermometer-full', 'fa-thermometer-half', 'fa-thermometer-quarter', 'fa-thermometer-three-quarters', 'fa-thumb-tack',
|
||||
'fa-thumbs-down', 'fa-thumbs-o-down', 'fa-thumbs-o-up', 'fa-thumbs-up', 'fa-ticket', 'fa-times', 'fa-times-circle', 'fa-times-circle-o',
|
||||
'fa-times-rectangle', 'fa-times-rectangle-o', 'fa-tint', 'fa-toggle-down', 'fa-toggle-left', 'fa-toggle-off', 'fa-toggle-on', 'fa-toggle-right',
|
||||
'fa-toggle-up', 'fa-trademark', 'fa-train', 'fa-transgender', 'fa-transgender-alt', 'fa-trash', 'fa-trash-o', 'fa-tree', 'fa-trello', 'fa-tripadvisor',
|
||||
'fa-trophy', 'fa-truck', 'fa-try', 'fa-tty', 'fa-tumblr', 'fa-tumblr-square', 'fa-turkish-lira', 'fa-tv', 'fa-twitch', 'fa-twitter', 'fa-twitter-square',
|
||||
'fa-umbrella', 'fa-underline', 'fa-undo', 'fa-universal-access', 'fa-university', 'fa-unlink', 'fa-unlock', 'fa-unlock-alt', 'fa-unsorted',
|
||||
'fa-upload', 'fa-usb', 'fa-usd', 'fa-user', 'fa-user-circle', 'fa-user-circle-o', 'fa-user-md', 'fa-user-o', 'fa-user-plus', 'fa-user-secret',
|
||||
'fa-user-times', 'fa-users', 'fa-vcard', 'fa-vcard-o', 'fa-venus', 'fa-venus-double', 'fa-venus-mars', 'fa-viacoin', 'fa-viadeo',
|
||||
'fa-viadeo-square', 'fa-video-camera', 'fa-vimeo', 'fa-vimeo-square', 'fa-vine', 'fa-vk', 'fa-volume-control-phone', 'fa-volume-down',
|
||||
'fa-volume-off', 'fa-volume-up', 'fa-warning', 'fa-wechat', 'fa-weibo', 'fa-weixin', 'fa-whatsapp', 'fa-wheelchair', 'fa-wheelchair-alt',
|
||||
'fa-wifi', 'fa-wikipedia-w', 'fa-window-close', 'fa-window-close-o', 'fa-window-maximize', 'fa-window-minimize', 'fa-window-restore',
|
||||
'fa-windows', 'fa-won', 'fa-wordpress', 'fa-wpbeginner', 'fa-wpexplorer', 'fa-wpforms', 'fa-wrench', 'fa-xing', 'fa-xing-square',
|
||||
'fa-y-combinator', 'fa-y-combinator-square', 'fa-yahoo', 'fa-yc', 'fa-yc-square', 'fa-yelp', 'fa-yen', 'fa-yoast', 'fa-youtube',
|
||||
'fa-youtube-play', 'fa-youtube-square' ]
|
||||
%}
|
||||
|
||||
<style>
|
||||
#select_fa{
|
||||
font-family:"FontAwesome","Liberation Sans";
|
||||
font-size:14px;
|
||||
}
|
||||
#select_fa::before{
|
||||
vertical-align:middle;
|
||||
}
|
||||
</style>
|
||||
<div class="row">
|
||||
<!-- form color picker -->
|
||||
<div class="col-lg-3 col-md-12 col-sm-12 col-xs-12">
|
||||
<div class="x_panel">
|
||||
<div class="x_title">
|
||||
<h2>System colors</h2>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="x_content">
|
||||
<div class="form-group row text-center">
|
||||
<div class="col-md-12 col-sm-4 col-xs-6">
|
||||
<label class="control-label col-xs-12">
|
||||
Background
|
||||
</label>
|
||||
<p>It will appear in Moodle's (Aules/Aulas) background. We recomend using a grey color.</p>
|
||||
<div id="colorpicker-background" class="demo demo-auto inl-bl colorpicker-background" data-container="#colorpicker-background" data-color="{{data.colours.background}}" data-inline="true"></div>
|
||||
<input id="colorpicker-background-input" type="text" value="{{data.colours.background}}" class="form-control" />
|
||||
</div>
|
||||
<div class="col-md-12 col-sm-4 col-xs-6">
|
||||
<label class="control-label col-xs-12">Primary</label>
|
||||
<p>It will appear in main top bar menú and Moodle's (Aules/Aulas) buttons. We recomend using a corporate color.</p>
|
||||
<div id="colorpicker-primary" class="demo demo-auto inl-bl colorpicker-primary" data-container="#colorpicker-primary" data-color="{{data.colours.primary}}" data-inline="true"></div>
|
||||
<input id="colorpicker-primary-input" type="text" value="{{data.colours.primary}}" class="form-control" />
|
||||
</div>
|
||||
<div class="col-md-12 col-sm-4 col-xs-6">
|
||||
<label class="control-label col-xs-12">Secondary</label>
|
||||
<p>It will appear in some secondary buttons in Moodle. We recommend white, unless corporate color is clear.</p>
|
||||
<div id="colorpicker-secondary" class="demo demo-auto inl-bl colorpicker-secondary" data-container="#colorpicker-secondary" data-color="{{data.colours.secondary}}" data-inline="true"></div>
|
||||
<input id="colorpicker-secondary-input" type="text" value="{{data.colours.secondary}}" class="form-control" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<ul class="nav navbar-right panel_toolbox">
|
||||
<li><button id="save-colors" type="button" class="btn btn-primary">Save changes</button></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /form color picker -->
|
||||
<!-- custom menu -->
|
||||
<div class="col-lg-9 col-md-12 col-sm-12 col-xs-12">
|
||||
<div class="x_panel">
|
||||
<div class="x_title">
|
||||
<h2>Custom menu</h2>
|
||||
<!-- <button class="btn btn-info pull-right">
|
||||
<i class='fa fa-plus'></i>
|
||||
Add entry
|
||||
</button> -->
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="x_content">
|
||||
{% for menu_item in data.apps_external %}
|
||||
<div class="form-group row ml-4">
|
||||
<div class="col-md-12 col-xs-12">
|
||||
<!-- <button class="btn btn-danger pull-right">
|
||||
<i class='fa fa-trash'></i>
|
||||
</button> -->
|
||||
<h3>
|
||||
<i class='fa {{ menu_item.icon }}'></i>
|
||||
- {{ menu_item.name }}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div hidden>
|
||||
<label class="control-label" for="apps_external-{{ menu_item.shortname }}-shortname">Shortname
|
||||
</label>
|
||||
<input readonly="readonly" id="apps_external-{{ menu_item.shortname }}-shortname" value="{{ menu_item.shortname }}" class="roundbox form-control" name="apps_external-{{ menu_item.shortname }}-shortname" placeholder="" type="text" required style="width: 100%;">
|
||||
</div>
|
||||
<div class="col-md-4 col-xs-12">
|
||||
<label class="control-label" for="apps_external-{{ menu_item.shortname }}-name">Name <span class="required">*</span>
|
||||
</label>
|
||||
<input id="apps_external-{{ menu_item.shortname }}-name" value="{{ menu_item.name }}" class="roundbox" name="apps_external-{{ menu_item.shortname }}-name" placeholder="" type="text" required style="width: 100%;">
|
||||
</div>
|
||||
<div class="col-md-4 col-xs-12">
|
||||
<label class="control-label" for="apps_external-{{ menu_item.shortname }}-href">Url <span class="required">*</span>
|
||||
</label>
|
||||
<input id="apps_external-{{ menu_item.shortname }}-href" value="{{ menu_item.href }}" class="roundbox" name="apps_external-{{ menu_item.href }}-href" placeholder="" type="text" required style="width: 100%;">
|
||||
</div>
|
||||
<div class="col-md-4 col-xs-12">
|
||||
<label class="control-label" for="apps_external-{{ menu_item.shortname }}-icon">Icon <span class="required">*</span>
|
||||
</label>
|
||||
<select class="icon-dropdown" id="apps_external-{{ menu_item.shortname }}-icon" name="apps_external-{{ menu_item.shortname }}-icon">
|
||||
{% for icon in icons %}
|
||||
<option value='fa {{ icon }}' data-icon="{{ icon }}" {% if 'fa ' + icon == menu_item.icon %} selected='selected' {% endif %}>
|
||||
{{ icon }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="form-group row">
|
||||
<ul class="nav navbar-right panel_toolbox">
|
||||
<li><button id="save-menu" type="button" class="btn btn-primary">Save changes</button></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /custom menu -->
|
||||
</div>
|
||||
<!-- Logo crop -->
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<div class="x_panel">
|
||||
<div class="x_title">
|
||||
<h2>Logo Image</h2>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="x_content">
|
||||
<div class="form-group row">
|
||||
<!-- Image cropping -->
|
||||
<div class="container cropper">
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
<div class="img-container">
|
||||
<img id="image_logo" src="{{ data.logo }}" alt="Background login">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="docs-preview clearfix">
|
||||
<div class="img-preview img-preview-logo preview-lg"></div>
|
||||
</div>
|
||||
<div class="alert alert-info" role="alert">
|
||||
Your organization logo will appear in the top bar of all Digital Democratic Work Environments and in the login page.<br/>
|
||||
Recommended Format:
|
||||
<strong>
|
||||
<ul>
|
||||
<li>Size: 80 x 45 píxels</li>
|
||||
<li>Weight: 30 KB</li>
|
||||
<li>Format: .png or .jpg</li>
|
||||
</ul>
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-9 docs-buttons-logo">
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-primary" data-method="setDragMode" data-option="move" title="Move">
|
||||
<span class="docs-tooltip" data-toggle="tooltip" title="Move">
|
||||
<span class="fa fa-arrows"></span>
|
||||
</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" data-method="setDragMode" data-option="crop" title="Crop">
|
||||
<span class="docs-tooltip" data-toggle="tooltip" title="Crop">
|
||||
<span class="fa fa-crop"></span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-primary" data-method="zoom" data-option="0.1" title="Zoom In">
|
||||
<span class="docs-tooltip" data-toggle="tooltip" title="Zoom In">
|
||||
<span class="fa fa-search-plus"></span>
|
||||
</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" data-method="zoom" data-option="-0.1" title="Zoom Out">
|
||||
<span class="docs-tooltip" data-toggle="tooltip" title="Zoom Out">
|
||||
<span class="fa fa-search-minus"></span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-primary" data-method="rotate" data-option="-45" title="Rotate Left">
|
||||
<span class="docs-tooltip" data-toggle="tooltip" title="$().cropper("rotate", -45)">
|
||||
<span class="fa fa-rotate-left"></span>
|
||||
</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" data-method="rotate" data-option="45" title="Rotate Right">
|
||||
<span class="docs-tooltip" data-toggle="tooltip" title="$().cropper("rotate", 45)">
|
||||
<span class="fa fa-rotate-right"></span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-primary" data-method="reset" title="Reset">
|
||||
<span class="docs-tooltip" data-toggle="tooltip" title="Reset">
|
||||
<span class="fa fa-refresh"></span>
|
||||
</span>
|
||||
</button>
|
||||
<label class="btn btn-primary btn-upload" for="inputImageLogo" title="Upload image file">
|
||||
<input type="file" class="sr-only" id="inputImageLogo" name="file" accept="image/*">
|
||||
<span class="docs-tooltip" data-toggle="tooltip" title="Import image with Blob URLs">
|
||||
<span class="fa fa-upload"></span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /image cropping -->
|
||||
<div class="form-group row docs-buttons">
|
||||
<ul class="nav navbar-right panel_toolbox">
|
||||
<li><button id="save-logo-crop" type="button" class="btn btn-primary">Save changes</button></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Background crop -->
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<div class="x_panel">
|
||||
<div class="x_title">
|
||||
<h2>Background Image</h2>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="x_content">
|
||||
<div class="form-group row">
|
||||
<!-- Image cropping -->
|
||||
<div class="container cropper">
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
<div class="img-container">
|
||||
<img id="image_background" src="{{ data.background_login }}" alt="Background login">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="docs-preview clearfix">
|
||||
<div class="img-preview img-preview-background preview-lg"></div>
|
||||
</div>
|
||||
<div class="alert alert-info" role="alert">
|
||||
The background image of your organization will appear in the Digital Democratic Login Page.<br/>
|
||||
Recommended Format:
|
||||
<strong>
|
||||
<ul>
|
||||
<li>Size: 1920 x 1080 píxels</li>
|
||||
<li>Weight: 1.75 MB</li>
|
||||
<li>Format: .png or .jpg</li>
|
||||
</ul>
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-9 docs-buttons-background">
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-primary" data-method="setDragMode" data-option="move" title="Move">
|
||||
<span class="docs-tooltip" data-toggle="tooltip" title="Move">
|
||||
<span class="fa fa-arrows"></span>
|
||||
</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" data-method="setDragMode" data-option="crop" title="Crop">
|
||||
<span class="docs-tooltip" data-toggle="tooltip" title="Crop">
|
||||
<span class="fa fa-crop"></span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-primary" data-method="zoom" data-option="0.1" title="Zoom In">
|
||||
<span class="docs-tooltip" data-toggle="tooltip" title="Zoom In">
|
||||
<span class="fa fa-search-plus"></span>
|
||||
</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" data-method="zoom" data-option="-0.1" title="Zoom Out">
|
||||
<span class="docs-tooltip" data-toggle="tooltip" title="Zoom Out">
|
||||
<span class="fa fa-search-minus"></span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-primary" data-method="rotate" data-option="-45" title="Rotate Left">
|
||||
<span class="docs-tooltip" data-toggle="tooltip" title="$().cropper("rotate", -45)">
|
||||
<span class="fa fa-rotate-left"></span>
|
||||
</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" data-method="rotate" data-option="45" title="Rotate Right">
|
||||
<span class="docs-tooltip" data-toggle="tooltip" title="$().cropper("rotate", 45)">
|
||||
<span class="fa fa-rotate-right"></span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-primary" data-method="reset" title="Reset">
|
||||
<span class="docs-tooltip" data-toggle="tooltip" title="Reset">
|
||||
<span class="fa fa-refresh"></span>
|
||||
</span>
|
||||
</button>
|
||||
<label class="btn btn-primary btn-upload" for="inputImageBackground" title="Upload image file">
|
||||
<input type="file" class="sr-only" id="inputImageBackground" name="file" accept="image/*">
|
||||
<span class="docs-tooltip" data-toggle="tooltip" title="Import image with Blob URLs">
|
||||
<span class="fa fa-upload"></span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /image cropping -->
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<ul class="nav navbar-right panel_toolbox">
|
||||
<li><button id="save-background-crop" type="button" class="btn btn-primary">Save changes</button></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block pagescript %}
|
||||
<!-- Ion.RangeSlider -->
|
||||
<script src="/vendors/ion.rangeSlider/js/ion.rangeSlider.min.js"></script>
|
||||
<!-- iCheck -->
|
||||
<script src="/vendors/iCheck/icheck.min.js"></script>
|
||||
<!-- Switchery -->
|
||||
<script src="/vendors/switchery/dist/switchery.min.js"></script>
|
||||
<!-- Desktops sse & modals -->
|
||||
<!-- Bootstrap Colorpicker -->
|
||||
<script src="/vendors/mjolnic-bootstrap-colorpicker/dist/js/bootstrap-colorpicker.min.js"></script>
|
||||
<!-- Cropper -->
|
||||
<script src="/vendors/cropper/dist/cropper.min.js"></script>
|
||||
<script src="/static/js/dashboard.js"></script>
|
||||
{% endblock %}
|
|
@ -0,0 +1,55 @@
|
|||
<!-- extend base layout -->
|
||||
{% extends "base.html" %}
|
||||
{% block css %}
|
||||
<!-- Ion.RangeSlider -->
|
||||
<link href="/vendors/normalize-css/normalize.css" rel="stylesheet">
|
||||
<!-- Switchery -->
|
||||
<link href="/vendors/switchery/dist/switchery.min.css" rel="stylesheet">
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 col-sm-12 col-xs-12">
|
||||
<div class="x_panel">
|
||||
<div class="x_title">
|
||||
<h3><i class="fa fa-users"></i> Groups</h3>
|
||||
<ul class="nav navbar-right panel_toolbox">
|
||||
<li>
|
||||
<a class="btn-new"><span style="color: #5499c7; "><i class="fa fa-plus"></i> Add new</span></a>
|
||||
<!-- <a class="btn-delete_keycloak"><span style="color: #c75454; "><i class="fa fa-cross"></i> Delete all keycloak</span></a> -->
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="x_content">
|
||||
<p class="text-muted font-13 m-b-30"></p>
|
||||
<table id="groups" class="table" width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Name</th>
|
||||
<th>Path</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'pages/modals/groups_modals.html' %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block pagescript %}
|
||||
<!-- Ion.RangeSlider -->
|
||||
<script src="/vendors/ion.rangeSlider/js/ion.rangeSlider.min.js"></script>
|
||||
<!-- iCheck -->
|
||||
<script src="/vendors/iCheck/icheck.min.js"></script>
|
||||
<!-- Switchery -->
|
||||
<script src="/vendors/switchery/dist/switchery.min.js"></script>
|
||||
<!-- Desktops sse & modals -->
|
||||
<script src="/static/js/groups.js"></script>
|
||||
{% endblock %}
|
|
@ -0,0 +1,81 @@
|
|||
<!-- extend base layout -->
|
||||
{% extends "base.html" %} {% block css %}
|
||||
<!-- Ion.RangeSlider -->
|
||||
<link href="/vendors/normalize-css/normalize.css" rel="stylesheet" />
|
||||
<!-- Switchery -->
|
||||
<link href="/vendors/switchery/dist/switchery.min.css" rel="stylesheet" />
|
||||
<!-- Bootstrap Colorpicker -->
|
||||
<link
|
||||
href="/vendors/mjolnic-bootstrap-colorpicker/dist/css/bootstrap-colorpicker.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
{% endblock %} {% block content %}
|
||||
|
||||
<style>
|
||||
#select_fa {
|
||||
font-family: "FontAwesome", "Liberation Sans";
|
||||
font-size: 14px;
|
||||
}
|
||||
#select_fa::before {
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
<div class="row">
|
||||
<!-- form color picker -->
|
||||
<div class="col-md-12 col-sm-12">
|
||||
<div class="x_panel">
|
||||
<div class="x_title">
|
||||
<h2>Legal<small>Company/School</small></h2>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="x_content">
|
||||
{% with type = 'legal' %}
|
||||
{% include 'aux/text-editor.html' %}
|
||||
{% endwith %}
|
||||
<div class="form-group row docs-buttons">
|
||||
<ul class="nav navbar-right panel_toolbox">
|
||||
<li>
|
||||
<button id="save-legal" type="button" class="btn btn-primary">
|
||||
Save changes
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="row">
|
||||
<div class="col-md-12 col-sm-12">
|
||||
<div class="x_panel">
|
||||
<div class="x_title">
|
||||
<h2>Privacy policy<small></small></h2>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="x_content">
|
||||
{% with type = 'privacy' %}
|
||||
{% include 'aux/text-editor.html' %}
|
||||
{% endwith %}
|
||||
<div class="form-group row docs-buttons">
|
||||
<ul class="nav navbar-right panel_toolbox">
|
||||
<li>
|
||||
<button id="save-privacy" type="button" class="btn btn-primary">
|
||||
Save changes
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
{% endblock %} {% block pagescript %}
|
||||
<script src="/static/js/legal.js"></script>
|
||||
<!-- bootstrap-wysiwyg -->
|
||||
<script src="/vendors/bootstrap-wysiwyg/js/bootstrap-wysiwyg.min.js"></script>
|
||||
<script src="/vendors/jquery.hotkeys/jquery.hotkeys.js"></script>
|
||||
<script src="/vendors/google-code-prettify/src/prettify.js"></script>
|
||||
{% endblock %}
|
||||
</div>
|
|
@ -0,0 +1,44 @@
|
|||
<div id="modal-lostconnection" class="modal fade" role="dialog" style="width:50%;margin-left:30%;margin-top:10%;z-index: 100000;">
|
||||
<div class="modal-admin">
|
||||
<div class="modal-content">
|
||||
<div class="row text-center"><h2 style="margin-bottom:5px">Connection lost</h2></div>
|
||||
<hr>
|
||||
<div class="row">
|
||||
<div class="col-md-1 col-sm-1 col-xs-12"></div>
|
||||
<div class="col-md-10 col-sm-10 col-xs-12">
|
||||
<div class="row text-center">Unable to contact server. There should be a problem with network or a heavy load.</div>
|
||||
<br>
|
||||
<div class="row text-center">
|
||||
<i class="fa fa-circle-o-notch fa-spin fa-fw"></i> Trying to reconnect...
|
||||
</div>
|
||||
<br>
|
||||
<div class="col-md-1 col-sm-1 col-xs-12">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="modal-info" class="modal fade" role="dialog" style="width:100%;margin-left:10%;margin-top:10%;z-index: 100000;">
|
||||
<div class="modal-admin">
|
||||
<div class="modal-content">
|
||||
<div class="row text-center"><h2 style="margin-bottom:5px">Connection lost</h2></div>
|
||||
<hr>
|
||||
<div class="row">
|
||||
<div class="col-md-10 col-sm-10 col-xs-12">
|
||||
<div class="row text-center">Unable to contact server. There should be a problem with network or a heavy load.</div>
|
||||
<br>
|
||||
<div class="row text-center">
|
||||
<i class="fa fa-circle-o-notch fa-spin fa-fw"></i> Trying to reconnect...
|
||||
</div>
|
||||
<br>
|
||||
<div class="col-md-1 col-sm-1 col-xs-12">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,68 @@
|
|||
<div class="modal fade" id="modalAddGroup" tabindex="-1" role="dialog" aria-labelledby="modalAddGroup" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<!-- Modal Header -->
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal">
|
||||
<span aria-hidden="true">×</span>
|
||||
<span class="sr-only">Close</span>
|
||||
</button>
|
||||
<h4 class="modal-title">
|
||||
<i class="fa fa-plus fa-1x"> </i> <i class="fa fa-users"> </i> Add new group
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<!-- Modal Body -->
|
||||
<div class="modal-body">
|
||||
|
||||
<form id="modalAddGroupForm" class="form-inline form-label-left">
|
||||
<div class="x_panel">
|
||||
<div class="x_title">
|
||||
<h4><i class="fa fa-info-circle" aria-hidden="true"></i> Group info</h4>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="x_content" style="padding: 0px;">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-xs-12">
|
||||
<label class="control-label" for="name">Name <span class="required">*</span>
|
||||
</label>
|
||||
<input id="name" class="roundbox" maxlength="40" pattern="^[-_àèìòùáéíóúñçÀÈÌÒÙÁÉÍÓÚÑÇ .a-zA-Z0-9]+$" data-parsley-length="[4, 40]" name="name" placeholder="Name" data-parsley-trigger="change" required type="text" style="width:100%">
|
||||
</div>
|
||||
<div class="col-md-6 col-xs-12">
|
||||
<label class="control-label" for="name">Description <span class="required">*</span>
|
||||
</label>
|
||||
<input id="description" class="roundbox" name="description" placeholder="Description" data-parsley-trigger="change" required type="text" style="width:100%">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="x_panel">
|
||||
<div class="x_title">
|
||||
<h4><i class="fa fa-users" aria-hidden="true"></i> Parent group</h4>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="x_content" style="padding: 0px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12 col-xs-12">
|
||||
<label class="control-label" for="id">Select parent group
|
||||
</label>
|
||||
<select class="groups-select roundbox" id="parent" name="parent" style="width:100%" required>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<!-- Modal Footer -->
|
||||
<div class="modal-footer">
|
||||
<ul class="nav navbar-left panel_toolbox">
|
||||
<li><button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button></li>
|
||||
</ul>
|
||||
<ul class="nav navbar-right panel_toolbox">
|
||||
<li><button id="send" type="button" class="btn btn-success">Create group</button></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,342 @@
|
|||
<div class="modal fade" id="modalAddUser" tabindex="-1" role="dialog" aria-labelledby="modalAddUser" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<!-- Modal Header -->
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal">
|
||||
<span aria-hidden="true">×</span>
|
||||
<span class="sr-only">Close</span>
|
||||
</button>
|
||||
<h4 class="modal-title" id="myModalLabel">
|
||||
<i class="fa fa-plus fa-1x"> </i> <i class="fa fa-user"> </i> Add new user
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<!-- Modal Body -->
|
||||
<div class="modal-body">
|
||||
|
||||
<form id="modalAddUserForm" class="form-inline form-label-left">
|
||||
<div class="x_panel">
|
||||
<div class="x_content">
|
||||
<div class="row">
|
||||
<div class="col-md-12 col-sm-12 col-xs-12">
|
||||
<label>Enabled</label>
|
||||
<div class="checkbox">
|
||||
<label class="">
|
||||
<div class="icheckbox_flat-green" style="position: relative;">
|
||||
<input type="checkbox" id="enabled" name="enabled" class="flat" style="position: absolute; opacity: 0;">
|
||||
<ins class="iCheck-helper" style="position: absolute; top: 0%; left: 0%; display: block; width: 100%; height: 100%; margin: 0px; padding: 0px; background: rgb(255, 255, 255); border: 0px; opacity: 0;">
|
||||
</ins>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="x_title">
|
||||
<h4><i class="fa fa-info-circle" aria-hidden="true"></i> User info</h4>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="x_content" style="padding: 0px;">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-xs-12">
|
||||
<label class="control-label" for="name">Username <span class="required">*</span>
|
||||
</label>
|
||||
<input id="username" class="roundbox" maxlength="40" pattern="^[-_.a-z0-9]+$" data-parsley-length="[4, 40]" name="username" placeholder="Username" data-parsley-trigger="change" required type="text" style="width:100%">
|
||||
</div>
|
||||
<div class="col-md-6 col-xs-12">
|
||||
<label class="control-label" for="email">Email <span class="required">*</span>
|
||||
</label>
|
||||
<input id="email" class="roundbox" data-validate-length-range="4,40" name="email" placeholder="Email" type="email" data-parsley-trigger="change" style="width:100%" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-xs-12">
|
||||
<label class="control-label" for="first">First name <span class="required">*</span>
|
||||
</label>
|
||||
<input id="first" class="roundbox" name="first" placeholder="First name" type="text" style="width:100%" required>
|
||||
</div>
|
||||
<div class="col-md-6 col-xs-12">
|
||||
<label class="control-label" for="last">Last name <span class="required">*</span>
|
||||
</label>
|
||||
<input id="last" class="roundbox" name="last" placeholder="Last name" type="text" style="width:100%" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-xs-12">
|
||||
<label class="control-label" for="quota">Quota <span class="required">*</span>
|
||||
</label>
|
||||
<select class="roundbox" name="quota" style="width:100%" required>
|
||||
<option value="500 MB">500 MB</option>
|
||||
<option value="1 GB">1 GB</option>
|
||||
<option value="3 GB">3 GB</option>
|
||||
<option value="5 GB">5 GB</option>
|
||||
<option value=false>Unlimited</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6 col-xs-12">
|
||||
<label class="control-label" for="password">Password <span class="required">*</span>
|
||||
</label>
|
||||
<input id="password" class="roundbox" name="password" placeholder="Password" type="text" style="width:100%"
|
||||
data-parsley-minlength="10"
|
||||
data-parsley-uppercase="2"
|
||||
data-parsley-lowercase="2"
|
||||
required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="x_panel">
|
||||
<div class="x_title">
|
||||
<h4><i class="fa fa-users" aria-hidden="true"></i> Groups</h4>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="x_content" style="padding: 0px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12 col-xs-12">
|
||||
<label class="control-label" for="id">Select group(s)
|
||||
</label>
|
||||
<select class="groups-select roundbox" name="groups[]" multiple="multiple" style="width:100%" required>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="x_panel">
|
||||
<div class="x_title">
|
||||
<h4><i class="fa fa-user-secret" aria-hidden="true"></i> Role</h4>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="x_content" style="padding: 0px;">
|
||||
<div class="row">
|
||||
<!-- <div class="col-md-4 col-xs-12">
|
||||
<label class="control-label" for="id">Moodle
|
||||
</label>
|
||||
<select class="role-moodle-select" name="moodle" style="width:100%">
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4 col-xs-12">
|
||||
<label class="control-label" for="id">Nextcloud
|
||||
</label>
|
||||
<select class="role-nextcloud-select" name="nextcloud" style="width:100%">
|
||||
</select>
|
||||
</div> -->
|
||||
<div class="col-md-12 col-xs-12">
|
||||
<label class="control-label" for="id">Select role
|
||||
</label>
|
||||
<select class="role-keycloak-select" name="role" style="width:100%" required>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<!-- Modal Footer -->
|
||||
<div class="modal-footer">
|
||||
<ul class="nav navbar-left panel_toolbox">
|
||||
<li><button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button></li>
|
||||
</ul>
|
||||
<ul class="nav navbar-right panel_toolbox">
|
||||
<li><button id="send" type="button" class="btn btn-success">Create user</button></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="modalPasswdUser" tabindex="-1" role="dialog" aria-labelledby="modalPasswdUser" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<!-- Modal Header -->
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal">
|
||||
<span aria-hidden="true">×</span>
|
||||
<span class="sr-only">Close</span>
|
||||
</button>
|
||||
<h4 class="modal-title" id="myModalLabel">
|
||||
<i class="fa fa-plus fa-1x"> </i> <i class="fa fa-key"> </i> Change user password
|
||||
</h4>
|
||||
</div>
|
||||
<!-- Modal Body -->
|
||||
<div class="modal-body">
|
||||
<form id="modalPasswdUserForm" class="form-inline form-label-left">
|
||||
<div class="x_panel">
|
||||
<div class="x_title">
|
||||
<h4>REMEMBER TO COPY THIS PASSWORD AS IT IS THE ONLY TIME YOU WILL SEE IT</h4>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="x_content" style="padding: 0px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12 col-xs-12">
|
||||
This is a proposed password. Change it for the desired one for this user. <br>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12 col-xs-12">
|
||||
<input id="id" hidden/>
|
||||
<label class="control-label" for="password">Password <span class="required">*</span>
|
||||
</label>
|
||||
<input id="password" class="roundbox" name="password" placeholder="Password" data-parsley-trigger="change" required type="text" style="width:100%">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<!-- Modal Footer -->
|
||||
<div class="modal-footer">
|
||||
<ul class="nav navbar-left panel_toolbox">
|
||||
<li><button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button></li>
|
||||
</ul>
|
||||
<ul class="nav navbar-right panel_toolbox">
|
||||
<li><button id="send" type="button" class="btn btn-success">Change user password</button></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="modalEditUser" tabindex="-1" role="dialog" aria-labelledby="modalEditUser" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<!-- Modal Header -->
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal">
|
||||
<span aria-hidden="true">×</span>
|
||||
<span class="sr-only">Close</span>
|
||||
</button>
|
||||
<h4 class="modal-title" id="myModalLabel">
|
||||
<i class="fa fa-plus fa-1x"> </i> <i class="fa fa-user"> </i> Edit user
|
||||
</h4>
|
||||
</div>
|
||||
<!-- Modal Body -->
|
||||
<div class="modal-body">
|
||||
<form id="modalEditUserForm" class="form-inline form-label-left">
|
||||
<div class="x_panel">
|
||||
<div class="x_content">
|
||||
<div class="row">
|
||||
<div class="col-md-12 col-sm-12 col-xs-12">
|
||||
<label>Enabled</label>
|
||||
<div class="checkbox">
|
||||
<label class="">
|
||||
<div class="icheckbox_flat-green" style="position: relative;">
|
||||
<input type="checkbox" id="enabled" name="enabled" class="flat" style="position: absolute; opacity: 0;">
|
||||
<ins class="iCheck-helper" style="position: absolute; top: 0%; left: 0%; display: block; width: 100%; height: 100%; margin: 0px; padding: 0px; background: rgb(255, 255, 255); border: 0px; opacity: 0;">
|
||||
</ins>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="x_title">
|
||||
<h4><i class="fa fa-info-circle" aria-hidden="true"></i> User info</h4>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="x_content" style="padding: 0px;">
|
||||
<input id="id" hidden/>
|
||||
<div class="row">
|
||||
<div class="col-md-2 col-sm-2 col-xs-12 text-center">
|
||||
<img id="user-avatar" src="" width="100" height="100"/>
|
||||
</div>
|
||||
<div class="col-md-10 col-sm-10 col-xs-12">
|
||||
<div class="row">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-xs-12">
|
||||
<label class="control-label" for="name">Username <span class="required">*</span>
|
||||
</label>
|
||||
<input disabled id="username" class="roundbox" maxlength="40" pattern="^[-_àèìòùáéíóúñçÀÈÌÒÙÁÉÍÓÚÑÇ .a-zA-Z0-9]+$" data-parsley-length="[4, 40]" name="name" placeholder="Username" data-parsley-trigger="change" required type="text" style="width:100%">
|
||||
</div>
|
||||
<div class="col-md-6 col-xs-12">
|
||||
<label class="control-label" for="email">Email <span class="required">*</span>
|
||||
</label>
|
||||
<input id="email" class="roundbox" data-validate-length-range="4,40" name="email" placeholder="Email" type="email" data-parsley-trigger="change" style="width:100%">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-xs-12">
|
||||
<label class="control-label" for="firstname">First name <span class="required">*</span>
|
||||
</label>
|
||||
<input id="firstname" class="roundbox" name="firstname" placeholder="First name" type="text" style="width:100%">
|
||||
</div>
|
||||
<div class="col-md-6 col-xs-12">
|
||||
<label class="control-label" for="lastname">Last name <span class="required">*</span>
|
||||
</label>
|
||||
<input id="lastname" class="roundbox" name="lastname" placeholder="First name" type="text" style="width:100%">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-xs-12">
|
||||
<label class="control-label" for="quota">Quota <span class="required">*</span>
|
||||
</label>
|
||||
<select class="roundbox" id="quota" name="quota" style="width:100%" required>
|
||||
<option value="500 MB">500 MB</option>
|
||||
<option value="1 GB">1 GB</option>
|
||||
<option value="3 GB">3 GB</option>
|
||||
<option value="5 GB">5 GB</option>
|
||||
<option value=false>Unlimited</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="x_panel">
|
||||
<div class="x_title">
|
||||
<h4><i class="fa fa-users" aria-hidden="true"></i> Groups</h4>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="x_content" style="padding: 0px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12 col-xs-12">
|
||||
<label class="control-label" for="id">Select group(s)
|
||||
</label>
|
||||
<select class="groups-select roundbox" name="groups[]" multiple="multiple" style="width:100%">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="x_panel">
|
||||
<div class="x_title">
|
||||
<h4><i class="fa fa-user-secret" aria-hidden="true"></i> Roles</h4>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="x_content" style="padding: 0px;">
|
||||
<div class="row">
|
||||
<!-- <div class="col-md-4 col-xs-12">
|
||||
<label class="control-label" for="id">Moodle</label>
|
||||
<select class="role-moodle-select" name="moodle" style="width:100%">
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4 col-xs-12">
|
||||
<label class="control-label" for="id">Nextcloud</label>
|
||||
<select class="role-nextcloud-select" name="nextcloud" style="width:100%">
|
||||
</select>
|
||||
</div> -->
|
||||
<div class="col-md-12 col-xs-12">
|
||||
<label class="control-label" for="id">Role</label>
|
||||
<select class="role-keycloak-select roundbox" name="role-keycloak" style="width:100%">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<!-- Modal Footer -->
|
||||
<div class="modal-footer">
|
||||
<ul class="nav navbar-left panel_toolbox">
|
||||
<li><button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button></li>
|
||||
</ul>
|
||||
<ul class="nav navbar-right panel_toolbox">
|
||||
<li><button id="send" type="button" class="btn btn-success">Edit user</button></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,51 @@
|
|||
<!-- extend base layout -->
|
||||
{% extends "base.html" %}
|
||||
{% block css %}
|
||||
<!-- Ion.RangeSlider -->
|
||||
<link href="/vendors/normalize-css/normalize.css" rel="stylesheet">
|
||||
<!-- Switchery -->
|
||||
<link href="/vendors/switchery/dist/switchery.min.css" rel="stylesheet">
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 col-sm-12 col-xs-12">
|
||||
<div class="x_panel">
|
||||
<div class="x_title">
|
||||
<h3><i class="fa fa-user-secret"></i> Roles</h3>
|
||||
<ul class="nav navbar-right panel_toolbox">
|
||||
<li>
|
||||
<!-- <a class="btn-new"><span style="color: #5499c7; "><i class="fa fa-plus"></i> Add new</span></a> -->
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="x_content">
|
||||
<p class="text-muted font-13 m-b-30"></p>
|
||||
<table id="roles" class="table" width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>Name</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block pagescript %}
|
||||
<!-- Ion.RangeSlider -->
|
||||
<script src="/vendors/ion.rangeSlider/js/ion.rangeSlider.min.js"></script>
|
||||
<!-- iCheck -->
|
||||
<script src="/vendors/iCheck/icheck.min.js"></script>
|
||||
<!-- Switchery -->
|
||||
<script src="/vendors/switchery/dist/switchery.min.js"></script>
|
||||
<!-- Desktops sse & modals -->
|
||||
<script src="/static/js/roles.js"></script>
|
||||
{% endblock %}
|
|
@ -0,0 +1,92 @@
|
|||
<!-- extend base layout -->
|
||||
{% extends "base.html" %}
|
||||
{% block css %}
|
||||
<!-- Ion.RangeSlider -->
|
||||
<link href="/vendors/normalize-css/normalize.css" rel="stylesheet">
|
||||
<!-- Switchery -->
|
||||
<link href="/vendors/switchery/dist/switchery.min.css" rel="stylesheet">
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 col-sm-12 col-xs-12">
|
||||
<div class="x_panel">
|
||||
<div class="x_title">
|
||||
<h3><i class="fa fa-desktop"></i> External</h3>
|
||||
<ul class="nav navbar-right panel_toolbox">
|
||||
<li>
|
||||
<div class="item form-group">
|
||||
<label class="control-label col-md-3 col-sm-3 col-xs-12" for="action_role">Assign role: <span class="required">*</span></label>
|
||||
<div class="col-md-6 col-sm-6 col-xs-12">
|
||||
<select id="action_role" name="action_role" class="form-control action" required>
|
||||
<option value=''>Select role</option>
|
||||
<option value='manager'>Manager</option>
|
||||
<option value='teacher'>Teacher</option>
|
||||
<option value='student'>Student</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<a class="btn-sync"><span style="color: #5499c7; "><i class="fa fa-rocket"></i> Sync to system</span></a>
|
||||
<a class="btn-upload"><span style="color: #5499c7; "><i class="fa fa-upload"></i> Upload</span></a>
|
||||
<a class="btn-sample"><span style="color: #a9ddff; "><i class="fa fa-download"></i> (Sample upload)</span></a>
|
||||
<a class="btn-download"><span style="color: #5499c7; "><i class="fa fa-download"></i> Download</span></a>
|
||||
<a class="btn-clear-upload"><span style="color: #5499c7; "><i class="fa fa-cross"></i> Clear upload</span></a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="x_content">
|
||||
<p class="text-muted font-13 m-b-30"></p>
|
||||
<table id="users" class="table" width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Avatar</th>
|
||||
<th>Username</th>
|
||||
<th>First</th>
|
||||
<th>Last</th>
|
||||
<th>email</th>
|
||||
<th>groups</th>
|
||||
<th>paths</th>
|
||||
<th>roles</th>
|
||||
<th>quota</th>
|
||||
<th>password</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="x_content">
|
||||
<p class="text-muted font-13 m-b-30"></p>
|
||||
<table id="groups" class="table" width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'pages/sysadmin/modals/external_modals.html' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block pagescript %}
|
||||
<!-- Ion.RangeSlider -->
|
||||
<script src="/vendors/ion.rangeSlider/js/ion.rangeSlider.min.js"></script>
|
||||
<!-- iCheck -->
|
||||
<script src="/vendors/iCheck/icheck.min.js"></script>
|
||||
<!-- Switchery -->
|
||||
<script src="/vendors/switchery/dist/switchery.min.js"></script>
|
||||
<!-- Desktops sse & modals -->
|
||||
<script src="/static/js/sysadmin/external.js"></script>
|
||||
{% endblock %}
|
|
@ -0,0 +1,59 @@
|
|||
<!-- extend base layout -->
|
||||
{% extends "base.html" %}
|
||||
{% block css %}
|
||||
<!-- Ion.RangeSlider -->
|
||||
<link href="/vendors/normalize-css/normalize.css" rel="stylesheet">
|
||||
<!-- Switchery -->
|
||||
<link href="/vendors/switchery/dist/switchery.min.css" rel="stylesheet">
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 col-sm-12 col-xs-12">
|
||||
<div class="x_panel">
|
||||
<div class="x_title">
|
||||
<h3><i class="fa fa-users"></i> Groups</h3>
|
||||
<ul class="nav navbar-right panel_toolbox">
|
||||
<li>
|
||||
<a class="btn-new"><span style="color: #5499c7; "><i class="fa fa-plus"></i> Add new</span></a>
|
||||
<!-- <a class="btn-delete_keycloak"><span style="color: #c75454; "><i class="fa fa-cross"></i> Delete all keycloak</span></a> -->
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="x_content">
|
||||
<p class="text-muted font-13 m-b-30"></p>
|
||||
<table id="groups" class="table" width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Id</th>
|
||||
<th>Keycloak</th>
|
||||
<th>Moodle</th>
|
||||
<th>Nextcloud</th>
|
||||
<th>Name</th>
|
||||
<th>Path</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'pages/modals/groups_modals.html' %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block pagescript %}
|
||||
<!-- Ion.RangeSlider -->
|
||||
<script src="/vendors/ion.rangeSlider/js/ion.rangeSlider.min.js"></script>
|
||||
<!-- iCheck -->
|
||||
<script src="/vendors/iCheck/icheck.min.js"></script>
|
||||
<!-- Switchery -->
|
||||
<script src="/vendors/switchery/dist/switchery.min.js"></script>
|
||||
<!-- Desktops sse & modals -->
|
||||
<script src="/static/js/sysadmin/groups.js"></script>
|
||||
{% endblock %}
|
|
@ -0,0 +1,134 @@
|
|||
<div class="modal fade" id="modalImport" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<!-- Modal Header -->
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal">
|
||||
<span aria-hidden="true">×</span>
|
||||
<span class="sr-only">Close</span>
|
||||
</button>
|
||||
<h4 class="modal-title">
|
||||
<i class="fa fa-plus fa-1x"> </i> <i class="fa fa-users"> </i> Import
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<!-- Modal Body -->
|
||||
<div class="modal-body">
|
||||
|
||||
<form id="modalImportForm" class="form-horizontal form-label-left">
|
||||
<div class="x_panel">
|
||||
<div class="x_content">
|
||||
<!--
|
||||
<input id="id" hidden/>
|
||||
-->
|
||||
|
||||
<!-- <div class="item form-group">
|
||||
<label class="control-label col-md-3 col-sm-3 col-xs-12" for="provider">Provider name: <span class="required">*</span></label>
|
||||
<div class="col-md-6 col-sm-6 col-xs-12">
|
||||
<input id="provider" name="provider" placeholder="" type="text" style="width:100%">
|
||||
</div>
|
||||
</div> -->
|
||||
<div class="item form-group">
|
||||
<label class="control-label col-md-3 col-sm-3 col-xs-12" for="format">Format: <span class="required">*</span></label>
|
||||
<div class="col-md-6 col-sm-6 col-xs-12">
|
||||
<select id="format" name="format" class="form-control format" required>
|
||||
<option value="json-ga">GAdminconsole JSON</option>
|
||||
<option value="csv-ug">CSV user with groups</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item form-group">
|
||||
<label class="control-label col-md-3 col-sm-3 col-xs-12" for="name">Import JSON <span class="required">*</span>
|
||||
</label>
|
||||
<div class="col-md-6 col-sm-6 col-xs-12">
|
||||
<input type="file" id="file-upload" name="file-upload" enctype="multipart/form-data" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- <div class="x_panela" id="bulkusers-quota" style="padding: 5px;">
|
||||
<p style="font-size: 18px;margin-bottom:0px;">Map User keys</p>
|
||||
|
||||
<div class="item form-group">
|
||||
<div class="item form-group">
|
||||
<label class="control-label col-md-3 col-sm-3 col-xs-12" for="userid">id: <span class="required">*</span></label>
|
||||
<div class="col-md-6 col-sm-6 col-xs-12">
|
||||
<select id="userid" name="userid" class="form-control userid populate" required>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item form-group">
|
||||
<label class="control-label col-md-3 col-sm-3 col-xs-12" for="username">username: <span class="required">*</span></label>
|
||||
<div class="col-md-6 col-sm-6 col-xs-12">
|
||||
<select id="username" name="username" class="form-control username populate" required>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item form-group">
|
||||
<label class="control-label col-md-3 col-sm-3 col-xs-12" for="firstname">first name: <span class="required">*</span></label>
|
||||
<div class="col-md-6 col-sm-6 col-xs-12">
|
||||
<select id="firstname" name="firstname" class="form-control firstname populate" required>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item form-group">
|
||||
<label class="control-label col-md-3 col-sm-3 col-xs-12" for="lastname">last name: <span class="required">*</span></label>
|
||||
<div class="col-md-6 col-sm-6 col-xs-12">
|
||||
<select id="lastname" name="lastname" class="form-control lastname populate" required>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item form-group">
|
||||
<label class="control-label col-md-3 col-sm-3 col-xs-12" for="email">email: <span class="required">*</span></label>
|
||||
<div class="col-md-6 col-sm-6 col-xs-12">
|
||||
<select id="email" name="email" class="form-control email populate" required>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item form-group">
|
||||
<label class="control-label col-md-3 col-sm-3 col-xs-12" for="usergroup">group:<span class="required">*</span></label>
|
||||
<div class="col-md-6 col-sm-6 col-xs-12">
|
||||
<select id="usergroup" name="usergroup" class="form-control usergroup populate" required>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="x_panela" id="bulkusers-quota" style="padding: 5px;">
|
||||
<p style="font-size: 18px;margin-bottom:0px;">Map Group keys</p>
|
||||
|
||||
<div class="item form-group">
|
||||
<div class="item form-group">
|
||||
<label class="control-label col-md-3 col-sm-3 col-xs-12" for="groupid">id:<span class="required">*</span></label>
|
||||
<div class="col-md-6 col-sm-6 col-xs-12">
|
||||
<select id="groupid" name="groupid" class="form-control groupid populate" required>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item form-group">
|
||||
<label class="control-label col-md-3 col-sm-3 col-xs-12" for="groupname">name:<span class="required">*</span></label>
|
||||
<div class="col-md-6 col-sm-6 col-xs-12">
|
||||
<select id="groupname" name="groupname" class="form-control groupname populate" required>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<!-- Modal Footer -->
|
||||
<div class="modal-footer">
|
||||
<div class="form-group">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
|
||||
<button id="send" type="button" class="btn btn-success">Process</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,84 @@
|
|||
<!-- extend base layout -->
|
||||
{% extends "base.html" %}
|
||||
{% block css %}
|
||||
<!-- Ion.RangeSlider -->
|
||||
<link href="/vendors/normalize-css/normalize.css" rel="stylesheet">
|
||||
<!-- Switchery -->
|
||||
<link href="/vendors/switchery/dist/switchery.min.css" rel="stylesheet">
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 col-sm-12 col-xs-12">
|
||||
<div class="x_panel">
|
||||
<div class="x_title">
|
||||
<h3><i class="fa fa-user"></i> Users</h3>
|
||||
<ul class="nav navbar-right panel_toolbox">
|
||||
<li>
|
||||
<a class="btn-new-user"><span style="color: #5499c7; "><i class="fa fa-plus"></i> Add new</span></a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="x_content">
|
||||
<button class="btn btn-success btn-xs btn-sync_from_keycloak">
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i> Sync all from keycloak
|
||||
</button>
|
||||
<button class="btn btn-primary btn-xs btn-sync_to_nextcloud">
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i> Sync to Nextcloud
|
||||
</button>
|
||||
<button class="btn btn-primary btn-xs btn-sync_to_moodle">
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i> Sync to Moodle
|
||||
</button>
|
||||
{% if current_user.role =='admin' %}
|
||||
<button class="btn btn-danger btn-xs btn-delete_keycloak">
|
||||
<i class="fa fa-trash"></i> Delete all keycloak
|
||||
</button>
|
||||
<button class="btn btn-danger btn-xs btn-delete_nextcloud">
|
||||
<i class="fa fa-trash"></i> Delete missing keycloak in nextcloud
|
||||
</button>
|
||||
<button class="btn btn-danger btn-xs btn-delete_moodle">
|
||||
<i class="fa fa-trash"></i> Delete missing keycloak in moodle
|
||||
</button>
|
||||
{% endif %}
|
||||
<table id="users" class="table" width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Avatar</th>
|
||||
<th>Username</th>
|
||||
<th>First</th>
|
||||
<th>Last</th>
|
||||
<th>email</th>
|
||||
<th>Keycloak</th>
|
||||
<th>K.Groups</th>
|
||||
<th>K.Roles</th>
|
||||
<th>Moodle</th>
|
||||
<th>M.Groups</th>
|
||||
<th>Nextcloud</th>
|
||||
<th>N.Groups</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'pages/modals/users_modals.html' %}
|
||||
{% include 'pages/users_detail.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block pagescript %}
|
||||
<!-- Ion.RangeSlider -->
|
||||
<script src="/vendors/ion.rangeSlider/js/ion.rangeSlider.min.js"></script>
|
||||
<!-- iCheck -->
|
||||
<script src="/vendors/iCheck/icheck.min.js"></script>
|
||||
<!-- Switchery -->
|
||||
<script src="/vendors/switchery/dist/switchery.min.js"></script>
|
||||
<!-- Desktops sse & modals -->
|
||||
<script src="/static/js/sysadmin/users.js"></script>
|
||||
{% endblock %}
|
|
@ -0,0 +1,90 @@
|
|||
<!-- extend base layout -->
|
||||
{% extends "base.html" %}
|
||||
{% block css %}
|
||||
<!-- Ion.RangeSlider -->
|
||||
<link href="/vendors/normalize-css/normalize.css" rel="stylesheet">
|
||||
<!-- Switchery -->
|
||||
<link href="/vendors/switchery/dist/switchery.min.css" rel="stylesheet">
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 col-sm-12 col-xs-12">
|
||||
<div class="x_panel">
|
||||
<div class="x_title">
|
||||
<h3><i class="fa fa-user"></i> Users</h3>
|
||||
<ul class="nav navbar-right panel_toolbox">
|
||||
<li>
|
||||
<div class="item form-group">
|
||||
<label class="control-label col-md-3 col-sm-3 col-xs-12" for="bulk_actions">Bulk actions: <span class="required">*</span></label>
|
||||
<div class="col-md-6 col-sm-6 col-xs-12">
|
||||
<select id="bulk_actions" name="bulk_actions" class="form-control action" required>
|
||||
<option value=''>Select action</option>
|
||||
<option value='enable'>Enable</option>
|
||||
<option value='disable'>Disable</option>
|
||||
<option value='delete'>Delete</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<a class="btn-new-user"><span style="color: #5499c7; "><i class="fa fa-plus"></i> Add new</span></a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="x_content">
|
||||
<table id="users" class="table" width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Enabled</th>
|
||||
<th>Avatar</th>
|
||||
<th>Role</th>
|
||||
<th>Actions</th>
|
||||
<th>Selected</th>
|
||||
<th>Username</th>
|
||||
<th>First</th>
|
||||
<th>Last</th>
|
||||
<th>Email</th>
|
||||
<th>Groups</th>
|
||||
<th>Quota</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<th>Enabled</th>
|
||||
<th>Avatar</th>
|
||||
<th>Role</th>
|
||||
<th>Actions</th>
|
||||
<th>Selected</th>
|
||||
<th>Username</th>
|
||||
<th>First</th>
|
||||
<th>Last</th>
|
||||
<th>Email</th>
|
||||
<th>Groups</th>
|
||||
<th>Quota</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'pages/modals/users_modals.html' %}
|
||||
{% include 'pages/users_detail.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block pagescript %}
|
||||
<!-- Ion.RangeSlider -->
|
||||
<script src="/vendors/ion.rangeSlider/js/ion.rangeSlider.min.js"></script>
|
||||
<!-- iCheck -->
|
||||
<script src="/vendors/iCheck/icheck.min.js"></script>
|
||||
<!-- Switchery -->
|
||||
<script src="/vendors/switchery/dist/switchery.min.js"></script>
|
||||
<!-- Desktops sse & modals -->
|
||||
<script src="/static/js/users.js"></script>
|
||||
{% endblock %}
|
|
@ -0,0 +1,15 @@
|
|||
<div style="display:none">
|
||||
<div class="row template-detail-users">
|
||||
<div class="col-md-12 col-sm-12 col-xs-12">
|
||||
<div class="row">
|
||||
<div class="col-md-12 col-md-12 col-xs-12" id="actions-d.id" data-pk="d.id" data-username="d.username">
|
||||
<div class="row">
|
||||
<button class="btn btn-info btn-xs btn-passwd" type="button" data-placement="top" ><i class="fa fa-key m-right-xs"></i>Reset password</button>
|
||||
<button class="btn btn-info btn-xs btn-edit" type="button" data-placement="top" ><i class="fa fa-pencil m-right-xs"></i>Edit</button>
|
||||
<button class="btn btn-danger btn-xs btn-delete" type="button" data-placement="top" ><i class="fa fa-remove m-right-xs"></i>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,32 @@
|
|||
<div class="navbar nav_title" style="border: 0;">
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
<!-- sidebar menu -->
|
||||
<div id="sidebar-menu" class="main_menu_side hidden-print main_menu mt-4">
|
||||
<div class="menu_section">
|
||||
<h3>Administration</h3>
|
||||
<div class="clearfix"></div>
|
||||
<ul class="nav side-menu">
|
||||
<li><a class="btn-global-resync"><span style="color: #c75454; "><i class="fa fa-refresh"></i> Resync</span></a></li>
|
||||
<li><a href="/users"><i class="fa fa-user"></i> Users</a></li>
|
||||
<li><a href="/groups"><i class="fa fa-users"></i> Groups</a></li>
|
||||
<li><a href="/roles"><i class="fa fa-user-secret"></i> Roles</a></li>
|
||||
<li><a href="/sysadmin/external"><i class="fa fa-external-link"></i> Import</a></li>
|
||||
<li><a href="/dashboard"><i class="fa fa-paint-brush"></i> Customization</a></li>
|
||||
<li><a href="/legal"><i class="fa fa-legal"></i> Legal</a></li>
|
||||
|
||||
|
||||
</ul>
|
||||
|
||||
{% if current_user.role == 'admin' %}
|
||||
<h3>System Admin</h3>
|
||||
<div class="clearfix"></div>
|
||||
<ul class="nav side-menu">
|
||||
<li><a href="/sysadmin/users"><i class="fa fa-user"></i> SysAdminUsers</span></a></li>
|
||||
<li><a href="/sysadmin/groups"><i class="fa fa-user"></i> SysAdminGroups</span></a></li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,32 @@
|
|||
# This file is for unifying the coding style for different editors and IDEs
|
||||
# editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.{json,html,css}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.js]
|
||||
# indent_style = space
|
||||
# indent_size = 2
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
|
||||
[*.coffee]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.md]
|
||||
indent_style = space
|
||||
|
||||
[*.py]
|
||||
indent_style = space
|
|
@ -0,0 +1,10 @@
|
|||
bin
|
||||
doc
|
||||
lib
|
||||
node_modules
|
||||
test/**
|
||||
!test/unit/**/*.js
|
||||
*.min.js
|
||||
build/**
|
||||
dist/**
|
||||
!dist/jquery.js
|
|
@ -0,0 +1,113 @@
|
|||
|
||||
extends:
|
||||
- "eslint:recommended"
|
||||
- "jquery"
|
||||
- "plugin:prettier/recommended"
|
||||
|
||||
# Accept `window`, etc.
|
||||
env:
|
||||
browser: true
|
||||
|
||||
# Known globals (`false`: read-only)
|
||||
globals:
|
||||
define: false
|
||||
require: false
|
||||
module: false
|
||||
jQuery: false
|
||||
|
||||
# Custom rules (see https://eslint.org/docs/rules/ )
|
||||
# 0:off, 1:warn, 2:error
|
||||
rules:
|
||||
# --- Best Practices ---------------------------------------------------------
|
||||
block-scoped-var: warn
|
||||
# complexity: [warn, 20]
|
||||
# consistent-return: warn
|
||||
no-alert: error
|
||||
no-caller: error
|
||||
guard-for-in: warn
|
||||
linebreak-style: warn
|
||||
no-else-return: warn
|
||||
# no-empty-function: warn
|
||||
no-extend-native: error
|
||||
no-eval: error
|
||||
no-floating-decimal: error
|
||||
no-implied-eval: error
|
||||
# no-invalid-this: warn
|
||||
no-labels: warn
|
||||
no-lone-blocks: warn
|
||||
no-loop-func: warn
|
||||
no-new: error
|
||||
no-new-func: warn
|
||||
no-new-wrappers: warn
|
||||
no-octal-escape: warn
|
||||
no-return-assign: warn
|
||||
no-script-url: warn
|
||||
no-self-compare: warn
|
||||
no-sequences: warn
|
||||
no-throw-literal: error
|
||||
no-unmodified-loop-condition: warn
|
||||
no-unused-expressions: error
|
||||
# Not enabled because we want to allow `self._superApply(self, args)`:
|
||||
# no-useless-call: warn
|
||||
no-useless-catch: warn
|
||||
no-useless-return: warn
|
||||
no-with: warn
|
||||
prefer-promise-reject-errors: warn
|
||||
radix: error
|
||||
# vars-on-top: warn
|
||||
wrap-iife:
|
||||
- error
|
||||
- any
|
||||
yoda: warn
|
||||
|
||||
# --- Strict Mode ------------------------------------------------------------
|
||||
# strict: error
|
||||
|
||||
# --- Variables --------------------------------------------------------------
|
||||
# init-declarations: ["warn", "always"]
|
||||
no-label-var: error
|
||||
# no-shadow: warn
|
||||
no-shadow-restricted-names: error
|
||||
no-undef: error
|
||||
no-undef-init: warn
|
||||
# no-undefined: warn
|
||||
no-use-before-define: error
|
||||
# - error
|
||||
# - functions: false
|
||||
|
||||
# --- Stylistic Issues -------------------------------------------------------
|
||||
camelcase: error
|
||||
# Not enabled because sometimes we set `node = this`:
|
||||
# consistent-this: [warn, self] # use `self = this`
|
||||
func-name-matching: warn
|
||||
new-cap:
|
||||
- error
|
||||
- { "capIsNewExceptionPattern": "^\\$\\.." } # Allow `d = $.Deferred()`
|
||||
no-bitwise: error
|
||||
# no-multi-assign: warn
|
||||
no-negated-condition: warn
|
||||
no-unneeded-ternary: warn
|
||||
no-new-object: error
|
||||
one-var: # see also no-use-before-define
|
||||
- warn
|
||||
- consecutive
|
||||
# one-var-declaration-per-line: warn
|
||||
|
||||
# --- Possible Errors --------------------------------------------------------
|
||||
curly: error
|
||||
eqeqeq: ["error", "always", {"null": "ignore"}]
|
||||
no-cond-assign:
|
||||
- error
|
||||
- except-parens
|
||||
no-constant-condition:
|
||||
- error
|
||||
- { "checkLoops": false }
|
||||
no-empty:
|
||||
- error
|
||||
- {allowEmptyCatch: true}
|
||||
# no-extra-parens: [warn, all, {conditionalAssign: false }]
|
||||
no-nested-ternary: warn
|
||||
no-unused-vars:
|
||||
- error
|
||||
# Allow unused vars in catch() and if start with '_'
|
||||
- {args: none, caughtErrors: none, varsIgnorePattern: "^_" }
|
|
@ -0,0 +1,27 @@
|
|||
# Style guide rationale:
|
||||
# Width 80 is default (and explicitly recommended) by prettier
|
||||
# - 2 space indentation and trailing semicolons seem to be most popular
|
||||
# https://hackernoon.com/what-javascript-code-style-is-the-most-popular-5a3f5bec1f6f
|
||||
# It is also the prettier's default
|
||||
# - Double quotes are default in prettier and mandatory in Black
|
||||
# - Trailing comma produces smaller diffs
|
||||
# BUT:
|
||||
# As a first step, we keep the current whitespace setting:
|
||||
# - use tabs
|
||||
# - tabWitdh 4
|
||||
|
||||
printWidth: 80
|
||||
useTabs: true
|
||||
tabWidth: 4
|
||||
# useTabs: false
|
||||
# tabWidth: 2
|
||||
semi: true
|
||||
singleQuote: false
|
||||
trailingComma: "es5"
|
||||
bracketSpacing: true # because it's prettier's default
|
||||
#requirePragma: true
|
||||
|
||||
#overrides:
|
||||
# - files: "*.test.js"
|
||||
# options:
|
||||
# semi: true
|
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>fancytree</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
<project>fancytree_wiki</project>
|
||||
<project>fancytree.wiki</project>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
</natures>
|
||||
<filteredResources>
|
||||
<filter>
|
||||
<id>1373147473034</id>
|
||||
<name></name>
|
||||
<type>10</type>
|
||||
<matcher>
|
||||
<id>org.eclipse.ui.ide.multiFilter</id>
|
||||
<arguments>1.0-name-matches-false-false-node_modules</arguments>
|
||||
</matcher>
|
||||
</filter>
|
||||
</filteredResources>
|
||||
</projectDescription>
|
|
@ -0,0 +1,31 @@
|
|||
# Configuration for `pyftpsync run ...` command line tool.
|
||||
# See https://pyftpsync.readthedocs.io/ for details.
|
||||
|
||||
# This task is used by `pyftpsync run` if no task is passed as argument:
|
||||
default_task: deploy
|
||||
|
||||
# Default settings shared by all tasks:
|
||||
common_config:
|
||||
exclude: archive,build,node_modules,.*,_*
|
||||
progress: true
|
||||
remote: ftp://wwwendt.de/tech/fancytree
|
||||
# Make --root default (can be overridden by --here):
|
||||
root: true
|
||||
# verbose: 4
|
||||
|
||||
# List of task definitions:
|
||||
tasks:
|
||||
# sync_all:
|
||||
# command: sync
|
||||
# remote: ftp://wwwendt.de/tech/fancytree
|
||||
|
||||
deploy:
|
||||
command: upload
|
||||
delete_unmatched: true
|
||||
|
||||
deploy_force:
|
||||
command: upload
|
||||
delete_unmatched: true
|
||||
delete: true
|
||||
resolve: local
|
||||
force: true
|
|
@ -0,0 +1,26 @@
|
|||
language: node_js
|
||||
sudo: false
|
||||
node_js:
|
||||
- "10"
|
||||
|
||||
before_script:
|
||||
- npm install -g grunt-cli
|
||||
|
||||
addons:
|
||||
hosts:
|
||||
- travis.dev
|
||||
- localhost
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- env: GRUNT_TASK=travis
|
||||
# - env: GRUNT_TASK=travis-optional
|
||||
# allow_failures:
|
||||
# - env: GRUNT_TASK=travis-optional
|
||||
|
||||
script: grunt $GRUNT_TASK --verbose
|
||||
|
||||
env:
|
||||
global:
|
||||
- secure: VmlzKmxE+V+QZpvDuj5W41u2HTu2uTvW0aUi2p+2yhCHd7J5TFdOoECwIhTa/4VDEpnZwjLJXPd2q9kEn3+G0HpEqRMtKVTP/sM8y0JKUkprSCWV/y+pVX+0B9jQBAhEcjtkLDEGI3xVI8n+WV0Fig4kWecSCcSSUN5Mlbq5glQ=
|
||||
- secure: ITp8qeoTyowtRqqFKPSjKq1tenmjt5ezNG/8ybEJQzxAMVGJ8bnyRPV1Aep0HB0ULP+GcYzDzGj5UeKM3hfWAJEfx+z1/HiHIMpJjEuGBz1JPfHx0lKcB7QfbhsRY3r8DVYptxdK9SyMisdIfCKApNMDW90RGDuALUdkLNPPXhI=
|
128
dd-sso/admin/src/admin/static/vendor/fancytree/3rd-party/extensions/contextmenu/contextmenu.html
vendored
Normal file
|
@ -0,0 +1,128 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
|
||||
<title>Fancytree - 3rd Party Example: Context menu</title>
|
||||
|
||||
<script src="../../../lib/jquery.js"></script>
|
||||
<script src="../../../lib/jquery-ui.custom.js"></script>
|
||||
|
||||
<link href="../../../src/skin-win8/ui.fancytree.css" rel="stylesheet">
|
||||
<script src="../../../src/jquery.fancytree.js"></script>
|
||||
|
||||
<!-- jquery-contextmenu (https://github.com/swisnl/jQuery-contextMenu) -->
|
||||
<link rel="stylesheet"
|
||||
href="//cdn.jsdelivr.net/npm/jquery-contextmenu@2.9.2/dist/jquery.contextMenu.min.css" />
|
||||
<script src="//cdn.jsdelivr.net/npm/jquery-contextmenu@2.9.2/dist/jquery.contextMenu.min.js">
|
||||
</script>
|
||||
|
||||
<script src="js/jquery.fancytree.contextMenu.js"></script>
|
||||
|
||||
<!-- Start_Exclude: This block is not part of the sample code -->
|
||||
<link href="../../../lib/prettify.css" rel="stylesheet">
|
||||
<script src="../../../lib/prettify.js"></script>
|
||||
<link href="../../../demo/sample.css" rel="stylesheet">
|
||||
<script src="../../../demo/sample.js"></script>
|
||||
<!-- End_Exclude -->
|
||||
|
||||
<!-- Add code to initialize the tree when the document is loaded: -->
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
$("#tree").fancytree({
|
||||
extensions: ["contextMenu"],
|
||||
source: {
|
||||
url: "../../../demo/ajax-tree-local.json"
|
||||
},
|
||||
contextMenu: {
|
||||
menu: {
|
||||
"edit": { "name": "Edit", "icon": "edit" },
|
||||
"cut": { "name": "Cut", "icon": "cut" },
|
||||
"copy": { "name": "Copy", "icon": "copy" },
|
||||
"paste": { "name": "Paste", "icon": "paste" },
|
||||
"delete": { "name": "Delete", "icon": "delete", "disabled": true },
|
||||
"sep1": "---------",
|
||||
"quit": { "name": "Quit", "icon": "quit" },
|
||||
"sep2": "---------",
|
||||
"fold1": {
|
||||
"name": "Sub group",
|
||||
"items": {
|
||||
"fold1-key1": { "name": "Foo bar" },
|
||||
"fold2": {
|
||||
"name": "Sub group 2",
|
||||
"items": {
|
||||
"fold2-key1": { "name": "alpha" },
|
||||
"fold2-key2": { "name": "bravo" },
|
||||
"fold2-key3": { "name": "charlie" }
|
||||
}
|
||||
},
|
||||
"fold1-key3": { "name": "delta" }
|
||||
}
|
||||
},
|
||||
"fold1a": {
|
||||
"name": "Other group",
|
||||
"items": {
|
||||
"fold1a-key1": { "name": "echo" },
|
||||
"fold1a-key2": { "name": "foxtrot" },
|
||||
"fold1a-key3": { "name": "golf" }
|
||||
}
|
||||
}
|
||||
},
|
||||
actions: function(node, action, options) {
|
||||
$("#selected-action")
|
||||
.text("Selected action '" + action + "' on node " + node + ".");
|
||||
}
|
||||
},
|
||||
lazyLoad: function(event, data) {
|
||||
data.result = { url: "../../ajax-sub2.json" }
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body class="example">
|
||||
|
||||
<h1>Example: 'contextMenu' extension</h1>
|
||||
|
||||
<div class="description">
|
||||
<p>
|
||||
Integrate the external
|
||||
<a href="https://github.com/swisnl/jQuery-contextMenu/" target="_blank" class="external">jQuery contextMenu plugin</a>
|
||||
as Fancytree extension.
|
||||
(<a href="https://github.com/mar10/fancytree/pull/3">Contributed by Tomas Norkūnas</a>.)
|
||||
</p>
|
||||
<p>
|
||||
This is only one of more options. See the
|
||||
<a href="../../../demo/index.html#sample-ext-menu.html">menu overview</a> for details.
|
||||
</p>
|
||||
<p>
|
||||
Please click right mouse button on a node.
|
||||
</p>
|
||||
</div>
|
||||
<!--
|
||||
<div>
|
||||
<label for="skinswitcher">Skin:</label> <select id="skinswitcher"></select>
|
||||
</div>
|
||||
-->
|
||||
<!-- Tree wrapper -->
|
||||
<div id="tree"></div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div id="selected-action">Click right mouse button on a node.</div>
|
||||
|
||||
<!-- Start_Exclude: This block is not part of the sample code -->
|
||||
<hr>
|
||||
|
||||
<p class="sample-links no_code">
|
||||
<a class="hideInsideFS" href="https://github.com/mar10/fancytree">jquery.fancytree.js project home</a>
|
||||
<a class="hideOutsideFS" href="#">Link to this page</a>
|
||||
<a class="hideInsideFS" href="index.html">Example Browser</a>
|
||||
<a href="#" id="codeExample">View source code</a>
|
||||
</p>
|
||||
|
||||
<pre id="sourceCode" class="prettyprint" style="display:none"></pre>
|
||||
<!-- End_Exclude -->
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,85 @@
|
|||
/**!
|
||||
* jquery.fancytree.contextmenu.js
|
||||
*
|
||||
* Integrate the 'jQuery contextMenu' plugin as Fancytree extension:
|
||||
* https://github.com/swisnl/jQuery-contextMenu
|
||||
*
|
||||
* Copyright (c) 2008-2018, Martin Wendt (https://wwWendt.de)
|
||||
* Released under the MIT license
|
||||
* https://github.com/mar10/fancytree/wiki/LicenseInfo
|
||||
*/
|
||||
(function($, document) {
|
||||
"use strict";
|
||||
|
||||
var initContextMenu = function(tree, selector, menu, actions) {
|
||||
tree.$container.on("mousedown.contextMenu", function(event) {
|
||||
var node = $.ui.fancytree.getNode(event);
|
||||
|
||||
if (node) {
|
||||
$.contextMenu("destroy", "." + selector);
|
||||
|
||||
// node.setFocus(true);
|
||||
node.setActive(true);
|
||||
|
||||
$.contextMenu({
|
||||
selector: "." + selector,
|
||||
events: {
|
||||
show: function(options) {
|
||||
options.prevKeyboard = tree.options.keyboard;
|
||||
tree.options.keyboard = false;
|
||||
},
|
||||
hide: function(options) {
|
||||
tree.options.keyboard = options.prevKeyboard;
|
||||
node.setFocus(true);
|
||||
},
|
||||
},
|
||||
build: function($trigger, e) {
|
||||
node = $.ui.fancytree.getNode($trigger);
|
||||
|
||||
var menuItems = {};
|
||||
if ($.isFunction(menu)) {
|
||||
menuItems = menu(node);
|
||||
} else if ($.isPlainObject(menu)) {
|
||||
menuItems = menu;
|
||||
}
|
||||
|
||||
return {
|
||||
callback: function(action, options) {
|
||||
if ($.isFunction(actions)) {
|
||||
actions(node, action, options);
|
||||
} else if ($.isPlainObject(actions)) {
|
||||
if (
|
||||
actions.hasOwnProperty(action) &&
|
||||
$.isFunction(actions[action])
|
||||
) {
|
||||
actions[action](node, options);
|
||||
}
|
||||
}
|
||||
},
|
||||
items: menuItems,
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$.ui.fancytree.registerExtension({
|
||||
name: "contextMenu",
|
||||
version: "@VERSION",
|
||||
contextMenu: {
|
||||
selector: "fancytree-title",
|
||||
menu: {},
|
||||
actions: {},
|
||||
},
|
||||
treeInit: function(ctx) {
|
||||
this._superApply(arguments);
|
||||
initContextMenu(
|
||||
ctx.tree,
|
||||
ctx.options.contextMenu.selector || "fancytree-title",
|
||||
ctx.options.contextMenu.menu,
|
||||
ctx.options.contextMenu.actions
|
||||
);
|
||||
},
|
||||
});
|
||||
})(jQuery, document);
|
104
dd-sso/admin/src/admin/static/vendor/fancytree/3rd-party/extensions/hotkeys/hotkeys.html
vendored
Normal file
|
@ -0,0 +1,104 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
|
||||
<title>Fancytree - 3rd Party Example: Hotkeys</title>
|
||||
|
||||
<script src="../../../lib/jquery.js"></script>
|
||||
<script src="../../../lib/jquery-ui.custom.js"></script>
|
||||
|
||||
<link href="../../../src/skin-win8/ui.fancytree.css" rel="stylesheet">
|
||||
<script src="../../../src/jquery.fancytree.js"></script>
|
||||
|
||||
<script src="js/jquery.hotkeys.js"></script>
|
||||
<script src="js/jquery.fancytree.hotkeys.js"></script>
|
||||
|
||||
<!-- Start_Exclude: This block is not part of the sample code -->
|
||||
<link href="../../../lib/prettify.css" rel="stylesheet">
|
||||
<script src="../../../lib/prettify.js"></script>
|
||||
<link href="../../../demo/sample.css" rel="stylesheet">
|
||||
<script src="../../../demo/sample.js"></script>
|
||||
<!-- End_Exclude -->
|
||||
|
||||
<!-- Add code to initialize the tree when the document is loaded: -->
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
$("#tree").fancytree({
|
||||
extensions: ["hotkeys"],
|
||||
source: {
|
||||
url: "../../../demo/ajax-tree-local.json"
|
||||
},
|
||||
hotkeys: {
|
||||
keyup: {
|
||||
"shift+a": function(node) {
|
||||
$("#selected-action").append(document.createTextNode("Key up 'Shift + a' on node " + node)).append("<br />");
|
||||
}
|
||||
},
|
||||
keydown: {
|
||||
"shift+a": function(node) {
|
||||
$("#selected-action").append(document.createTextNode("Key down 'Shift + a' on node " + node)).append("<br />");
|
||||
},
|
||||
'ctrl+d': function(node, evt) {
|
||||
$('#selected-action').append(document.createTextNode('Key down "Ctrl + d" on node ' + node)).append('<br />');
|
||||
var new_node = $.extend(node.toDict(), {key: new Date().getTime().toString()}); // timestamp for dummy key
|
||||
node.appendSibling(new_node);
|
||||
evt.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
|
||||
},
|
||||
keypress: {
|
||||
"shift+a": function(node) {
|
||||
$("#selected-action").append(document.createTextNode("Key press 'Shift + a' on node " + node)).append("<br />");
|
||||
}
|
||||
}
|
||||
},
|
||||
lazyLoad: function(event, data) {
|
||||
data.result = { url: "../../ajax-sub2.json" }
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body class="example">
|
||||
|
||||
<h1>Example: 'hotkeys' extension</h1>
|
||||
|
||||
<div class="description">
|
||||
<p>
|
||||
Integrate John Resig's
|
||||
<a href="https://github.com/jeresig/jquery.hotkeys" target="_blank" class="external">'jQuery.Hotkeys' plugin</a>
|
||||
as Fancytree extension.
|
||||
</p>
|
||||
<p>
|
||||
Please activate one node and click "Shift + a".
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="skinswitcher">Skin:</label> <select id="skinswitcher"></select>
|
||||
</div>
|
||||
|
||||
<!-- Tree wrapper -->
|
||||
<div id="tree"></div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div id="selected-action">Activate one node and click "Shift + a" or "Ctrl + d":<br /></div>
|
||||
|
||||
<!-- Start_Exclude: This block is not part of the sample code -->
|
||||
<hr>
|
||||
|
||||
<p class="sample-links no_code">
|
||||
<a class="hideInsideFS" href="https://github.com/mar10/fancytree">jquery.fancytree.js project home</a>
|
||||
<a class="hideOutsideFS" href="#">Link to this page</a>
|
||||
<a class="hideInsideFS" href="index.html">Example Browser</a>
|
||||
<a href="#" id="codeExample">View source code</a>
|
||||
</p>
|
||||
|
||||
<pre id="sourceCode" class="prettyprint" style="display:none"></pre>
|
||||
<!-- End_Exclude -->
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,35 @@
|
|||
/**!
|
||||
* jquery.fancytree.hotkeys.js
|
||||
*
|
||||
* Integrate the 'jQuery.Hotkeys' plugin as Fancytree extension:
|
||||
* https://github.com/jeresig/jquery.hotkeys/
|
||||
*
|
||||
* Copyright (c) 2008-2018, Martin Wendt (https://wwWendt.de)
|
||||
* Released under the MIT license
|
||||
* https://github.com/mar10/fancytree/wiki/LicenseInfo
|
||||
*/
|
||||
(function($, document) {
|
||||
"use strict";
|
||||
|
||||
var initHotkeys = function(tree, data) {
|
||||
$.each(data, function(event, keys) {
|
||||
$.each(keys, function(key, handler) {
|
||||
$(tree.$container).on(event, null, key, function(evt) {
|
||||
var node = tree.getActiveNode();
|
||||
return handler(node, evt);
|
||||
// return false from the handler will stop default handling.
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$.ui.fancytree.registerExtension({
|
||||
name: "hotkeys",
|
||||
version: "@VERSION",
|
||||
hotkeys: {},
|
||||
treeInit: function(ctx) {
|
||||
this._superApply(arguments);
|
||||
initHotkeys(this, ctx.options.hotkeys);
|
||||
},
|
||||
});
|
||||
})(jQuery, document);
|
196
dd-sso/admin/src/admin/static/vendor/fancytree/3rd-party/extensions/hotkeys/js/jquery.hotkeys.js
vendored
Normal file
|
@ -0,0 +1,196 @@
|
|||
/*jslint browser: true*/
|
||||
/*jslint jquery: true*/
|
||||
|
||||
/*
|
||||
* jQuery Hotkeys Plugin
|
||||
* Copyright 2010, John Resig
|
||||
* Dual licensed under the MIT or GPL Version 2 licenses.
|
||||
*
|
||||
* Based upon the plugin by Tzury Bar Yochay:
|
||||
* http://github.com/tzuryby/hotkeys
|
||||
*
|
||||
* Original idea by:
|
||||
* Binny V A, http://www.openjs.com/scripts/events/keyboard_shortcuts/
|
||||
*/
|
||||
|
||||
/*
|
||||
* One small change is: now keys are passed by object { keys: '...' }
|
||||
* Might be useful, when you want to pass some other data to your handler
|
||||
*/
|
||||
|
||||
(function(jQuery) {
|
||||
|
||||
jQuery.hotkeys = {
|
||||
version: "0.8",
|
||||
|
||||
specialKeys: {
|
||||
8: "backspace",
|
||||
9: "tab",
|
||||
10: "return",
|
||||
13: "return",
|
||||
16: "shift",
|
||||
17: "ctrl",
|
||||
18: "alt",
|
||||
19: "pause",
|
||||
20: "capslock",
|
||||
27: "esc",
|
||||
32: "space",
|
||||
33: "pageup",
|
||||
34: "pagedown",
|
||||
35: "end",
|
||||
36: "home",
|
||||
37: "left",
|
||||
38: "up",
|
||||
39: "right",
|
||||
40: "down",
|
||||
45: "insert",
|
||||
46: "del",
|
||||
59: ";",
|
||||
61: "=",
|
||||
96: "0",
|
||||
97: "1",
|
||||
98: "2",
|
||||
99: "3",
|
||||
100: "4",
|
||||
101: "5",
|
||||
102: "6",
|
||||
103: "7",
|
||||
104: "8",
|
||||
105: "9",
|
||||
106: "*",
|
||||
107: "+",
|
||||
109: "-",
|
||||
110: ".",
|
||||
111: "/",
|
||||
112: "f1",
|
||||
113: "f2",
|
||||
114: "f3",
|
||||
115: "f4",
|
||||
116: "f5",
|
||||
117: "f6",
|
||||
118: "f7",
|
||||
119: "f8",
|
||||
120: "f9",
|
||||
121: "f10",
|
||||
122: "f11",
|
||||
123: "f12",
|
||||
144: "numlock",
|
||||
145: "scroll",
|
||||
173: "-",
|
||||
186: ";",
|
||||
187: "=",
|
||||
188: ",",
|
||||
189: "-",
|
||||
190: ".",
|
||||
191: "/",
|
||||
192: "`",
|
||||
219: "[",
|
||||
220: "\\",
|
||||
221: "]",
|
||||
222: "'"
|
||||
},
|
||||
|
||||
shiftNums: {
|
||||
"`": "~",
|
||||
"1": "!",
|
||||
"2": "@",
|
||||
"3": "#",
|
||||
"4": "$",
|
||||
"5": "%",
|
||||
"6": "^",
|
||||
"7": "&",
|
||||
"8": "*",
|
||||
"9": "(",
|
||||
"0": ")",
|
||||
"-": "_",
|
||||
"=": "+",
|
||||
";": ": ",
|
||||
"'": "\"",
|
||||
",": "<",
|
||||
".": ">",
|
||||
"/": "?",
|
||||
"\\": "|"
|
||||
},
|
||||
|
||||
// excludes: button, checkbox, file, hidden, image, password, radio, reset, search, submit, url
|
||||
textAcceptingInputTypes: [
|
||||
"text", "password", "number", "email", "url", "range", "date", "month", "week", "time", "datetime",
|
||||
"datetime-local", "search", "color", "tel"],
|
||||
|
||||
options: {
|
||||
filterTextInputs: true
|
||||
}
|
||||
};
|
||||
|
||||
function keyHandler(handleObj) {
|
||||
if (typeof handleObj.data === "string") {
|
||||
handleObj.data = {
|
||||
keys: handleObj.data
|
||||
};
|
||||
}
|
||||
|
||||
// Only care when a possible input has been specified
|
||||
if (!handleObj.data || !handleObj.data.keys || typeof handleObj.data.keys !== "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
var origHandler = handleObj.handler,
|
||||
keys = handleObj.data.keys.toLowerCase().split(" ");
|
||||
|
||||
handleObj.handler = function(event) {
|
||||
// Don't fire in text-accepting inputs that we didn't directly bind to
|
||||
if (this !== event.target && (/textarea|select/i.test(event.target.nodeName) ||
|
||||
(jQuery.hotkeys.options.filterTextInputs &&
|
||||
jQuery.inArray(event.target.type, jQuery.hotkeys.textAcceptingInputTypes) > -1))) {
|
||||
return;
|
||||
}
|
||||
|
||||
var special = event.type !== "keypress" && jQuery.hotkeys.specialKeys[event.which],
|
||||
character = String.fromCharCode(event.which).toLowerCase(),
|
||||
modif = "",
|
||||
possible = {};
|
||||
|
||||
jQuery.each(["alt", "ctrl", "shift"], function(index, specialKey) {
|
||||
|
||||
if (event[specialKey + 'Key'] && special !== specialKey) {
|
||||
modif += specialKey + '+';
|
||||
}
|
||||
});
|
||||
|
||||
// metaKey is triggered off ctrlKey erronously
|
||||
if (event.metaKey && !event.ctrlKey && special !== "meta") {
|
||||
modif += "meta+";
|
||||
}
|
||||
|
||||
if (event.metaKey && special !== "meta" && modif.indexOf("alt+ctrl+shift+") > -1) {
|
||||
modif = modif.replace("alt+ctrl+shift+", "hyper+");
|
||||
}
|
||||
|
||||
if (special) {
|
||||
possible[modif + special] = true;
|
||||
}
|
||||
else {
|
||||
possible[modif + character] = true;
|
||||
possible[modif + jQuery.hotkeys.shiftNums[character]] = true;
|
||||
|
||||
// "$" can be triggered as "Shift+4" or "Shift+$" or just "$"
|
||||
if (modif === "shift+") {
|
||||
possible[jQuery.hotkeys.shiftNums[character]] = true;
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0, l = keys.length; i < l; i++) {
|
||||
if (possible[keys[i]]) {
|
||||
return origHandler.apply(this, arguments);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
jQuery.each(["keydown", "keyup", "keypress"], function() {
|
||||
jQuery.event.special[this] = {
|
||||
add: keyHandler
|
||||
};
|
||||
});
|
||||
|
||||
})(jQuery || this.jQuery || window.jQuery);
|
|
@ -0,0 +1,686 @@
|
|||
# 2.36.1 / Unreleased
|
||||
|
||||
# 2.36.0 / 2020-07-15
|
||||
* [Changed] #1005 Cast key to string in getNodeByKey()
|
||||
* [Changed] #1013 ext-dnd5: log warning when jQuery is too old
|
||||
* [Added] #1012 `dnd5.dropMarkerParent` allows usage in Webcomponents (i.e. shadow DOM)
|
||||
* [Added] #1017 `copyFunctionsToData` allows also copying functions to the data property of the node
|
||||
* [Fixed] #921 ext-edit / focus handling: Internet Explorer scrolls briefly
|
||||
to the top/left after editing if the tree container is partially outside the viewport
|
||||
* [Fixed] #1001 Invalid urls in skin-xp CSS
|
||||
* [Fixed] ext-dnd5: dropEffectCallback=none was not reset in some cases
|
||||
* [Fixed] #1018 ContextMenu extension always focuses the first node in the tree
|
||||
|
||||
# 2.35.0 / 2020-03-27
|
||||
* [Changed] The `enableAspx` option will default to 'false' in the future.
|
||||
For now, a warning is emitted, to explicitly set it or use the `postProcess`
|
||||
event instead.
|
||||
* [Added] #988 New option `dnd5.preventLazyParents` prevents dropping items on
|
||||
unloaded lazy nodes (defaults to true)
|
||||
* [Fixed] #983 lazyLoad with promise not calling postProcess
|
||||
* [Fixed] #984 ext-edit: Exception when cancelling addSibling() or addChildren()
|
||||
* [Fixed] #987 Lazy load puts "Load error" for content outside tree div
|
||||
if parent folder is removed before loads ends
|
||||
* [Fixed] #989 `node.toDict()` keeps empty `children` array
|
||||
* [Fixed] #998 dnd5 triggering multiple loads of lazy nodes on hover
|
||||
|
||||
# 2.34.0 / 2019-12-26
|
||||
* [DEPRECATED] jQuery UI widget methods:
|
||||
Use `tree.METHOD()` instead of `$().fancytree("METHOD")`.
|
||||
* [Added] `tree.debugTime()`, `tree.debugTimeEnd()` for debugging.
|
||||
* [Added] `tree.destroy()` as alternative for `tree.widget.destroy()`.
|
||||
* [Fixed] `$.ui.fancytree.getTree()` for Element arg.
|
||||
* [Fixed] #973 when use ext-grid in one tree, other tree not use ext-grid has error on click.
|
||||
* [Fixed] #974 ext-grid: too much output in production mode.
|
||||
* [Fixed] #975 ext-grid: fix `tree.visitRows()` for empty tree.
|
||||
* [Fixed] #978 ext-grid: addChildren() throws error when grid is hidden.
|
||||
|
||||
# 2.33.0 / 2019-10-29
|
||||
* [Added] event `preInit` (fired before nodes are loaded).
|
||||
* [Changed] jQuery is now a peerDependency (>=1.9), so users can install or re-use their own version.
|
||||
* [Changed] ext-grid: `updateViewport` event is now also triggered for 'renumber' (i.e. expand, collapse)
|
||||
* [Fixed] #963: tree.setExpanded() fails when autoScroll is enabled
|
||||
* [Fixed] #964: handle case when `source` is not passed and no `<ul>` is embedded.
|
||||
* [Fixed] #966: ext-dnd5: bug in function onDropEvent (case 'dragover')
|
||||
* [Fixed] ext-filter: sub-match counter is one too high.
|
||||
|
||||
# 2.32.0 / 2019-09-10
|
||||
* [Added] `node.hasClass()`
|
||||
* [Added] `tree.applyCommand()` and `node.applyCommand()` (experimental!)
|
||||
* [Added] `tree.isLoading()`
|
||||
* [Added] `tree.toDict(includeRoot, callback)` and `node.toDict(recursive, callback)`:
|
||||
callback can now return `false` or `"skip"` to skip nodes.
|
||||
* [Fixed] #951 Hover issue in unselectable radio
|
||||
* ext-dnd5: allow autoExpand even if dropping is prevented
|
||||
* [Fixed] ext-filter: tree.rootNode.subMatchCount is now set correctly
|
||||
* [Fixed] #955 node.navigate($.ui.keyCode.DOWN, false) does not return promise
|
||||
* Stop testing with jQuery UI 1.10 and 1.11 (only jQuery UI 1.12 remains)
|
||||
|
||||
# 2.31.0 / 2019-05-30
|
||||
* New extension **ext-grid** (experimental)<br>
|
||||
This is a variant of `ext-table` that introduces viewport support, which
|
||||
allows to maintain *huge* data models while only rendering as many DOM elements as necessary.<br>
|
||||
Main changes:
|
||||
- A viewport is defined by the number of visible rows (`tree.viewport.count`) and the index of the first visible row (`.start`)
|
||||
- When scrolling, rows are not hidden, but removed and replaced. (This implies that the contents of embedded input fields should be written into the model immediately.)
|
||||
* Refactored **ext-dnd5**<br>
|
||||
Some **breaking changes** were made, mainly to improve handling of the dropEffect
|
||||
(note that ext-dnd5 was and still is experimental and in progress).
|
||||
- Remove `dnd5.dropEffect` callback option (set `data.dropEffect` instead)
|
||||
- Remove `dnd5.dragImage` callback option (call `data.dataTransfer.setDragImage()`
|
||||
- and set `data.useDefaultImage = false` instead)
|
||||
- Rename `dnd5.preventRecursiveMoves` to `dnd5.preventRecursion`
|
||||
- `dnd5.preventVoidMoves` now only aplies to 'move' operations, so we can *copy* before self
|
||||
- [Added] `dnd5.preventSameParent` option
|
||||
* [Added] hook `treeStructureChanged`
|
||||
* [Added] methods `tree.findRelatedNode()`, `node.findRelatedNode()`
|
||||
* [Added] method `node.getPath()`
|
||||
* [Added] methods `$.ui.fancytree.getDragNode()`, `$.ui.fancytree.getDragNodeList()`
|
||||
* [Added] event `updateViewport`
|
||||
* [Added] tree option `.checkboxAutoHide` to hide checkboxes unless selected or hovered.
|
||||
* [Added] tree option `.treeId` to prevent generation of a new sequence if the tree is re-initialized on a page.
|
||||
* [Changed] `.getTree()` now also accepts the tree id string
|
||||
* [Changed] #939: Keep a `partsel` flag that was explicitly set on a lazy node
|
||||
* [Changed] ext-clones: make default key generation more robust against collisions
|
||||
* [DEPRECATED] loaderror and lazyload options now throw an error instead of falling back to the correct loadError and lazyLoad
|
||||
* [DEPRECATED] `tree.applyFilter` was removed
|
||||
* [Fixed] #918 SVG font awesome 5 glyphs remove badge counter when parent node is collapsed
|
||||
* [Fixed] #921 ext-edit respectively focus handling: Internet Explorer scrolls briefly
|
||||
to the top/left of the tree container element after editing a node title if the
|
||||
tree container is partially outside the viewport
|
||||
* [Fixed] #931 Selecting grandparent selects all nodes of radiogroup in selectMode=3
|
||||
* [Fixed] #946 dnd5 - Counter badge shows up, although the drag was cancelled from dragStart callback
|
||||
* [Fixed] #947 dnd5 - dragEnd is fired only when re-ordering nodes within the same parent
|
||||
* [Fixed] missing tree.error() and broken node.error()
|
||||
* [Fixed] a bug in ext-logger
|
||||
* Optimized performance of `expandAll()` and `ext-filter`
|
||||
* Replace jshint/jscs with eslint
|
||||
* Now testing on Puppeteer/Chromium instead of PhantonJS
|
||||
* Use LF on Windows when checking out from git (added .gitattributes)
|
||||
* Update to jQuery 3.4
|
||||
|
||||
# 2.30.2 / 2019-01-13
|
||||
* Stop testing on IE 8 (no longer available on Saucelabs)
|
||||
* [Fixed] #910 ext-dnd5 throws error for draggable column headers
|
||||
* [Fixed] overrideMethod()'s calling context
|
||||
* [Fixed] #912 ext-dnd5 + ext-glyph awesome5 does not show the icons when dragging an item
|
||||
* [Fixed] #919 ext-multi: JavaScript error (event is not defined) in nodeKeydown
|
||||
* [Fixed] #922 scrollIntoView for plain trees that don't have a scrollbar
|
||||
* [Fixed] #924 ext-edit: Fix caret position for mouse-click in input
|
||||
* [Fixed] #928 ext-dnd5: Fix `preventNonNodes` option
|
||||
* [Fixed] #929 Fix `.getTree()` for jQuery 3
|
||||
* [Fixed] #930 ext-dnd5: If drag does not start, no drag data should be stored
|
||||
|
||||
# 2.30.1 / 2018-11-13
|
||||
* [Changed] Apply and enforce 'prettier' codestyle
|
||||
* [Changed] #897 Set font for table extension
|
||||
* [Fixed] #883: Font Awesome 4 animation spinner stays visible
|
||||
* [Fixed] #894: Fancytree assertion failed: scrollParent should be a simple element or `window`, not document or body.
|
||||
* [Fixed] #896 _requireExtension: order managment
|
||||
* [Fixed] #899 Creating duplicate icon when removing node using extension columnview
|
||||
* [Fixed] #900 ColumnView Extension - Toggle between parent and children not working
|
||||
* [Fixed] #909 With quicksearch enabled, does not search for non-Latin character
|
||||
|
||||
# 2.30.0 / 2018-09-02
|
||||
* [Changed] ext-edit trigger 'clickActive' now only triggers if no modifier keys
|
||||
(shift, meta, control, ...) are pressed.<br>
|
||||
Trigger 'shift+click' now only triggers if no other modifier key (control, ...)
|
||||
is pressed.
|
||||
* [Changed] #879 Rename ext-debug to ext-logger
|
||||
(jquery.fancytree.debug.js => jquery.fancytree.logger.js)
|
||||
* [Added] ext-multi is now deployed with jquery.fancytree-all.js (still experimental)
|
||||
* [Added] tree.activateKey(key, opts) now has an `opts` argument
|
||||
* [Added] `nodata` option (bool, string, or callback)
|
||||
* [Added] ext-table `mergeStatusColumns` option
|
||||
* [Added] new method `tree.enable(flag)`
|
||||
* [Added] new method `tree.expandAll(flag, opts)`
|
||||
* [Added] new methods `tree.setOption(name, value)` and `tree.getOption(name)`
|
||||
* [Fixed] ES6 import dependency on jquery for jquery.fancytree.ui-deps.js
|
||||
* [Fixed] #863 setActive() sometimes does not scroll node into view
|
||||
* [Fixed] #877 postProcess may now also return the object form `{..., children: []}`
|
||||
* [Fixed] #884 ReferenceError: jQuery is not defined at _simpleDeepMerge
|
||||
* [Fixed] autoScroll, node.scrollIntoView(), and .makeVisible() now work for tables as well.
|
||||
|
||||
# 2.29.1 / 2018-06-27
|
||||
* [Fixed] #848 Drag End Error with dnd5 extension (again):
|
||||
fancytree-drag-remove class not removed on drop/dragend
|
||||
* [Fixed] #875 ext-dnd5: Unwanted expanding of folder node when a node is dragged
|
||||
before/after it
|
||||
* [Fixed] #876 `triggerStart: []` does not override the default settings.<br>
|
||||
**NOTE:** Options of type `Array` will now override the default option.
|
||||
Before, arrays were merged with the default.
|
||||
* [Fixed] ext-ariagrid default actions
|
||||
|
||||
# 2.29.0 / 2018-06-16
|
||||
* [Changed]
|
||||
`toggleEffect` now also accepts "toggle" or "slideToggle" to use jQuery effects instead of jQueryUI.<br>
|
||||
`toggleEffect: { effect: "slideToggle", duration: 200 }` is now the default.<br>
|
||||
'effects' component was removed from the bundled jquery.fancytree.ui-deps.js
|
||||
* [Fixed] #746 Animation bug when expanding/collapsing nodes
|
||||
* [Fixed] #848 Drag End Error with dnd5 extension
|
||||
* [Fixed] #850 ext-childcounter doesn't work with custom icons
|
||||
* [Fixed] #859 Fix log level configuration problem
|
||||
* [Fixed] #865 toggleEffect animation (effect: blind) sometimes got stuck.
|
||||
* Stop testing jQuery UI 1.9
|
||||
* Update to jQuery 3.3.1
|
||||
|
||||
# 2.28.1 / 2018-03-19
|
||||
* [Fixed] #844 Fix RTL for ext-table
|
||||
* [Fixed] #845 Fix RTL for ext-dnd/ext-dnd5
|
||||
* [Fixed] #764 Fix clicks on embedded <a> tags when filter is on
|
||||
|
||||
# 2.28.0 / 2018-03-02
|
||||
* [Added] New extension ext-multi (experimental).
|
||||
* [Added] ext-dnd5 support for dragging multiple selected nodes.
|
||||
* [Added] #830 support for Font Awesome 5 (ext-glyph preset).
|
||||
* [Added] ext-glyph supports SVG icons.
|
||||
* [Added] `icon` option supports `{html: "..."}` content (also available for glyph-ext mapping).
|
||||
* [Added] New method tree.visitRows()
|
||||
* [Added] New method tree.selectAll()
|
||||
* [Added] New method node.isBelowOf()
|
||||
* [Added] New extension ext-fixed (experimental).
|
||||
* [Changed] Re-rename clearData() to clearPersistData()
|
||||
* [Changed] #828 Re-scale debugLevel from 0:quiet to 4:verbose, allowing to suppress warnings
|
||||
and even errors.
|
||||
* [Added] CSS helper classes:<br>
|
||||
`.fancytree-helper-disabled`<br>
|
||||
`.fancytree-helper-hidden` (replaces `ui-helper-hidden`)<br>
|
||||
`.fancytree-helper-indeterminate-cb`<br>
|
||||
`fancytree-helper-spin` for icon animations (replaces `glyphicon-spin`)
|
||||
* [Fixed] #819: ext-filter: Handle nodes without title.
|
||||
* [Fixed] #835: ext-dnd5: Accept drop externals after drag.
|
||||
|
||||
# 2.27.0 / 2017-12-16
|
||||
* **BREAKING CHANGES:**
|
||||
- `node.type` is now a first-class property of FancytreeNode.
|
||||
Node data `{..., type: "foo"}` is now available as `node.type` (before: `node.data.type`).
|
||||
- The properties `tree.types` and `tree.columns` have been added to Fancytree.
|
||||
If passed with source data, they are now available directly instead of
|
||||
`tree.data.types` or `tree.data.columns`.
|
||||
* **Support patterns for node types:**
|
||||
- The properties `node.type` and `tree.types` are recommended to implement node-type
|
||||
specific configuration ([details](https://github.com/mar10/fancytree/wiki/TutorialNodeTypes)).
|
||||
- Event `data` argument contains `typeInfo == tree.types[node.type]`.
|
||||
* **Improved ext-glyph:**
|
||||
- [Added] support for ligature icons (e.g. [material icons](https://material.io/icons/)).
|
||||
- [Added] `icon` option can now return a dict to create a ligature icon.
|
||||
* **Improved tree.loadKeyPath():**
|
||||
- [Added] support for a custom path segment matcher.
|
||||
This allows to have key paths with segments other than `node.key`.
|
||||
- [Improved] the returned deferred promise now triggers `progress()` events which can
|
||||
be used instead of the callback.
|
||||
* The property `tree.columns` was added to Fancytree. Currently only reserved as
|
||||
recommended pattern to pass global meta-data for ext-table.
|
||||
* [Added] ext-edit: new trigger mode `clickActive` for option `triggerStart: [...]`.
|
||||
* [Added] #798 Tooltip support for icons (dynamic option `iconTooltip`).
|
||||
* [Added] #808 Pass custom storage providers to ext-persist.
|
||||
* [Improved] ext-table no longer needs empty tbody/tr if thead is present.
|
||||
* [Fixed] #796 UMD requirements for node/CommonJS
|
||||
* [Fixed] #803 jquery.fancytree.ui-deps.js does not override existing widgets.
|
||||
* [Fixed] #815 `<mark>` element missing in filtered nodes (minified bundle, IE 11).
|
||||
* [Fixed] #816 findNextNode() doesn't set default for 'startNode' argument.
|
||||
* [Added] Material Design demo
|
||||
* [Added] Demo for Fancytree inside a jquery-confirm popup
|
||||
* [Changed] String representation is now `"FancytreeNode@_4[title='My name']"`
|
||||
* [DEPRECATED] `tree.clearCookies()`. Use <del>`tree.clearData()`</del> `tree.clearPersistData()` instead.
|
||||
|
||||
# 2.26.0 / 2017-11-04
|
||||
* **BREAKING CHANGES:**
|
||||
- [Fixed] #792 postProcess is now also called for non-Ajax sources.
|
||||
* [Improved] LESS now compiles with webpack
|
||||
* [Added] #791 ext-glyph support for radio buttons
|
||||
* [Added] Color definitions for skin-awesome (taken from skin-lion)
|
||||
* [Fixed] `$.ui.fancytree.getNode()` for ES6 environments
|
||||
* [Fixed] #789 Wrong node is activated in IE, when clicking in unfocused container
|
||||
|
||||
# 2.25.0 / 2017-10-31
|
||||
* **BREAKING CHANGES:**
|
||||
- The `dist/src/` folder was renamed to `dist/modules`.
|
||||
- Some directories like `demo/` are no longer part of the npm install.
|
||||
* **Improved Module Support and Distribution**<br>
|
||||
- The `dist/` folder now includes a `modules/` directory with fancytree core
|
||||
and all extensions.
|
||||
- All modules have UMD wrappers with defined dependencies.
|
||||
- Internal jQuery UI dependencies are deployed as module and implicitly loaded.
|
||||
- `jquery.fancytree/dist/modules/jquery.fancytree` is defined as
|
||||
package main module, so Fancytree can be included using a simple<br>
|
||||
`fancytree = require('jquery.fancytree')`.<br>
|
||||
See [the docs](https://github.com/mar10/fancytree/wiki/TutorialIntegration)
|
||||
for details.
|
||||
- All modules now return the
|
||||
[$.ui.fancytree object](https://wwWendt.de/tech/fancytree/doc/jsdoc/Fancytree_Static.html).
|
||||
- [Added] new static method `$.ui.fancytree.createTree(elem, opts)`
|
||||
* [Added] Source map files for `jquery.fancytree-all-deps.min.js`
|
||||
* [Added] New extension ext-fixed (work-in-progress, experimental)
|
||||
* [Fixed] #767: Input inside table head not working
|
||||
* [Fixed] #768: Can't use keyboard to select nodes when `checkbox` option is false
|
||||
* [Fixed] #782: wide extension - padding is off when checkbox option is changed
|
||||
* [Fixed] #787: Fix getEventTarget() for custom icons
|
||||
|
||||
# 2.24.0 / 2017-08-26
|
||||
* [Added] ext-glyph option `preset` (making the `map` option optional)
|
||||
* [Fixed] Drop marker for ext-glyph + ext-dnd5
|
||||
* [Fixed] #695: List AMD dependency on jQuery UI
|
||||
* [Fixed] #735: Trying to set root node selected throws an error
|
||||
* [Fixed] #740: Filtering must not consider escaped html entities
|
||||
* [Fixed] #741: Passing an empty string ("") as filter calls clearFilter()
|
||||
* [Fixed] #748: Drag start should not activate a node
|
||||
* [Fixed] #761: ext-dnd5 throws exception when tree is empty
|
||||
* [Fixed] #764: ext-filter breaks links
|
||||
* Updated jsdoc to 3.5
|
||||
|
||||
# 2.23.0 / 2017-05-27
|
||||
* **The external dependency on jQuery UI was removed**.<br>
|
||||
A new library `jquery.fancytree-all-deps.min.js` is now added to the
|
||||
distribution. It includes all dependencies on jQuery UI, so the only
|
||||
remaining external dependency is jQuery.<br>
|
||||
Continue to use `jquery.fancytree-all.min.js` if jQuery UI is already
|
||||
included anyway.
|
||||
|
||||
* **Refactored the select behavior**<br>
|
||||
[details](https://github.com/mar10/fancytree/wiki/SpecSelect):
|
||||
<!-- [details](https://github.com/mar10/fancytree/wiki#selection-and-checkboxes) -->
|
||||
* [Added] Allow control of selection status propagation with new options:
|
||||
`unselectable`, `unselectableIgnore`, `unselectableStatus`.
|
||||
* [Added] node option `radiogroup` to enable single-select for child nodes
|
||||
* [Added] option `opts.noEvents` to `setSelected(flag, opts)`
|
||||
* [Improved] Option 'checkbox' can have the string value "radio" (only has
|
||||
the visual effect of replacing the icon)
|
||||
|
||||
* **BREAKING CHANGES:**
|
||||
* The `hideCheckbox` option was removed. Use `checkbox: false` instead.<br>
|
||||
Note that the `<li class='hideCheckbox'>` is still parsed from input
|
||||
HTML and converted accordingly.
|
||||
* The optional modifier class `<div class='fancytree-radio'>` was removed.
|
||||
This class was used on the *container* to turn all checkbox items into
|
||||
radio buttons.<br>
|
||||
Instead, this class is now added to `<span class="fancytree-checkbox fancytree-radio">`.
|
||||
Use the `tree.checkox: "radio"` option to activate this for the whole tree.
|
||||
* The callback signature for the `tree.tooltip` option has changed to
|
||||
`tooltip(event, data)`
|
||||
|
||||
* [Improved] `aria` option is now on by default
|
||||
* Use the new dynamic options pattern for
|
||||
`checkbox`, `icon`, `tooltip`, `unselectable`, `unselectableIgnore`,
|
||||
`unselectableStatus`.<br>
|
||||
See also <a href="https://github.com/mar10/fancytree/wiki#dynamic-options">dynamic options</a>.
|
||||
* [Added] New method `node.visitSiblings()`
|
||||
* [Added] #730 ext-persist option `expandOpts` is passed to setExpanded()
|
||||
Allows to suppress animation or event generation.
|
||||
|
||||
# 2.22.5 / 2017-05-11
|
||||
* [Improved] #709 experimental ext-ariagrid
|
||||
|
||||
# 2.22.4 / 2017-05-06
|
||||
* [Improved] #709 experimental ext-ariagrid
|
||||
|
||||
# 2.22.3 / 2017-05-05
|
||||
* [Improved] #709 experimental ext-ariagrid
|
||||
|
||||
# 2.22.2 / 2017-04-29
|
||||
* [Fixed] #729 Fix regression with addChild performance improvements (#708)
|
||||
|
||||
# 2.22.1 / 2017-04-21
|
||||
* [Fixed] #722 Fix regression with addChild performance improvements (#708)
|
||||
|
||||
# 2.22.0 / 2017-04-11
|
||||
* [Added] ext-dnd5 now part of standard distribution
|
||||
* [Added] #693 ext-dnd/dnd5: configurable drop marker offset
|
||||
* [Added] #616 ext-wide: configurable left padding
|
||||
* [Added] New method $.ui.fancytree.evalOption()
|
||||
* [Improved] #601 ext-filter: improve performance (don't render hidden nodes)
|
||||
* [Improved] ext-contextMenu: disable keyboard while popup is open and restore focus
|
||||
* [Improved] #701 ext-hotkeys: Prevent default behavior on hot key combination
|
||||
* [Improved] #708 speedup improvement for addChildren
|
||||
* [Fixed] #680 ext-dnd5: top level nodes not draggable
|
||||
* [Fixed] #681 ext-table: exception when a lazy node has `children: []`
|
||||
* [Fixed] #699 ext-dnd5: Icon remains after dnd is cancelled
|
||||
* [Fixed] #702 $.ui.fancytree.getNode(jQuery)' for jQuery v3.x
|
||||
* [Fixed] #706 Fix DND where fancytree-title span is not a direct child due to custom layouts
|
||||
* [Fixed] #712 When clicking in a scrolled tree for the first time, focus is not set properly
|
||||
* [Fixed] #716 ext-wide: animation 'jumps' (jQuery UI 1.12)
|
||||
* [Fixed] #717, #719 expand/collapse shows displaced child nodes when scrolled (jQuery UI 1.12)
|
||||
* Update demos to jQuery 3.2.1 / jQuery UI 1.12.1
|
||||
|
||||
# 2.21.0 / 2017-01-15
|
||||
* [Added] New extension 'ext-dnd5' (beta) for native HTML5 drag'n'drop support
|
||||
* [Added] `rtl` option for right-to-left script support
|
||||
* [Added] Add $.ui.fancytree.overrideMethod()
|
||||
* [Added] hook `treeSetOption` allows extensions to update on option changes
|
||||
* [Changed] standard CSS no longer defines `overflow: auto` for the container.
|
||||
If the tree container has a fixed height, `overflow: auto` or `overflow: scroll`
|
||||
should be added to make it scrollable.
|
||||
(Otherwise this always would be the scroll parent for ext-dnd5.)
|
||||
* [Improved] better support for initializing from embedded JSON using the
|
||||
`data-type="json"` attribute
|
||||
* [Fixed] corner case of #658 when ext-edit is loaded, but inactive
|
||||
* [Fixed] #396 Don't load 'loading.gif' for glyph skins
|
||||
* [Fixed] #675 ext-table: node.render(false) puts first node at end
|
||||
|
||||
# 2.20.0 / 2016-11-13
|
||||
* [Added] #419 `modifyChild` event. This event is also a good place to
|
||||
implement auto sorting (#559)
|
||||
* [Added] #419 node.triggerModifyChild() and node.triggerModify()
|
||||
* [Added] #595 add custom node filter to `generateFormElements()`
|
||||
* [Added] #610 `tree.tooltip` option allows automatic or custom tooltips
|
||||
* [Added] #620 improved tooltip escaping to allow newlines
|
||||
* [DEPRECATED] `removeNode` event. Listen for `modifyChild` with operation
|
||||
'remove' instead (which is fired on the parent)
|
||||
* [Improved] ThemeRoller theme
|
||||
* [Improved] ext-filter
|
||||
- #297 add filter option 'hideExpanders' to remove expanders if all child
|
||||
nodes are hidden by filter
|
||||
- Filter options and the `opts` argument of `filterNodes()` / `filterBranches()`
|
||||
have been unified
|
||||
- [Fixed] #230 themeroller theme compatible with ext-filter
|
||||
- [Fixed] #528 autoCollapse option blocks filter's autoExpand option
|
||||
- [Fixed] #529 Filter: Mark matching nodes even if parent was matched in branch mode
|
||||
- [Fixed] #643 Exceptions in ext-filter if expression contains special chars
|
||||
- [Fixed] #658 ext-filter does not work with ext-edit `editCreateNode()`
|
||||
* [Improved] #656 WAI-ARIA support
|
||||
- Set focus to first node on first tab-in
|
||||
- Support [home] and [end] keys
|
||||
- Set aria-activedescendant on container to active ID
|
||||
- Set aria-multiselectable on container if selectMode != 1
|
||||
- Set aria-treeitem, -selected, -expanded, on title span instead `<li>`
|
||||
* [Fixed] #576 `loadKeyPath()` sometimes gets the root wrong
|
||||
* [Fixed] #615 Drag & drop helper icons lose indentation with table extension
|
||||
* [Fixed] #632 Tabbing is not working if there is an anchor tag in treeview
|
||||
* [Fixed] #644 New nodes created with ext-edit, are hidden in filtered trees
|
||||
* [Fixed] #647 ext-table: tree.render(true) does not discard existing markup
|
||||
* [Fixed] #659 handling of function keys, when quicksearch is on
|
||||
* Use QUnit 2.0
|
||||
|
||||
# 2.19.0 / 2016-08-11
|
||||
* [Added] #607 tree.enableUpdate() to temporarily disable rendering to improve
|
||||
performance on bulk updates
|
||||
* [Added] modifier class `.fancytree-connectors` to be set on container<br>
|
||||
Note: Experimental! Not required for skin-xp and not compatible with ext-table
|
||||
* [Added] #623 ext-edit: `data.originalEvent` is now passed to `beforeClose`
|
||||
* [Fixed] #604 Set `source` option does not update tree
|
||||
* [Fixed] #609 node.load(true); doesn't maintain expanded
|
||||
* [Fixed] #621 Cannot focus embedded input controls
|
||||
* [Improved] #611 Keyboard navigation honors autoScroll option
|
||||
* Extensions inherit main version number
|
||||
|
||||
# 2.18.0 / 2016-05-02
|
||||
* [Added] #586 node.discardMarkup() (useful in the `collapsed` event)
|
||||
* [Added] #171 new option `.escapeTitles`
|
||||
* [Added] new callback `.enhanceTitle()`
|
||||
* [Fixed] #515 Html tags included in filter results
|
||||
* [Fixed] #593 ext-dnd revert position fails for tables
|
||||
|
||||
# 2.17.0 / 2016-04-11
|
||||
* [Added] `node.addClass()`, `.removeClass()`, and `.toggleClass()`
|
||||
* [Added] ext-filter: matcher-callback for `tree.filterNodes()` may now return
|
||||
`"branch"` and `"skip"`
|
||||
* [Added] ext-filter: new option`nodata` allows to configure a status node for
|
||||
empty results
|
||||
* [Added] `digits` argument to `node.getIndexHier(separator, digits)`
|
||||
* [Added] tree option `.tabindex`, default is "0". Pass "" to resolve #577
|
||||
* [DEPRECATED] tree option `.tabbable`. Use `.tabindex` instead
|
||||
* [Added] New option `mode='firstChild'` for `node.moveTo()`
|
||||
* [Added] New option `digits=<int>` for `node.getIndexHier()`
|
||||
* [Fixed] ext-filter: branch mode honors `autoExpand: true`
|
||||
* [Fixed] #584: aria-labelledby ids not unique
|
||||
* Update to jQuery UI 1.11.4
|
||||
|
||||
# 2.16.1 / 2016-03-18
|
||||
* [Added] ext-glyph: new icon for 'nodata' status nodes
|
||||
* [Fixed] #575 missing loading icon in non-bootstrap themes.<br>
|
||||
Glyph themes now display status images in icon span (was expander span before)
|
||||
|
||||
# 2.16.0 / 2016-03-16
|
||||
* [Added] ext-clones: new method node.setRefKey(refKey)
|
||||
* [Added] modifier class `.fancytree-fade-expander` to be set on container
|
||||
* [Added] ext-dnd: `.dragExpand()` callback to prevent auto-expand
|
||||
* [Improved] load error reporting
|
||||
* [Improved] bootstrap theme icons and style (samples use bootstrap 3.3)
|
||||
* [Improved] status nodes don't have icons
|
||||
* [Improved] pass data argument to `source` callback
|
||||
* [Improved] Handle exceptions inside `postProcess`
|
||||
* [Improved] #568 ext-dnd: Auto-expanding of collapsed nodes should also work
|
||||
when dropping is not allowed
|
||||
* [Improved] #567 ext-dnd: fix revert position
|
||||
* [Improved] #565 ext-dnd: fix intermediate display of wrong icon (sending 'over' after 'enter')
|
||||
* [Fixed] #569 node.navigate does not return a Promise object
|
||||
* [Fixed] #563 `tree.reactivate(false)` sets fancytree-treefocus and `tree.reactivate(true)`
|
||||
doesn't set keyboard focus
|
||||
* [Fixed] #562 Node span tag leaks outside table cell
|
||||
* [Fixed] #526 tree.setFocus() does not set keyboard focus
|
||||
* Updated to jQuery 1.12.1
|
||||
* Updated grunt devDependencies
|
||||
* Add jQuery 3.0 beta to test suite
|
||||
* Added LICENSE.txt to dist
|
||||
|
||||
# 2.15.0 / 2016-01-11
|
||||
* [Changed] Renamed class `fancytree-statusnode-wait` to `fancytree-statusnode-loading`
|
||||
* [Added] new event `renderStatusColumns`
|
||||
* [DEPRECATED] ext-table option `customStatus`. Use `renderStatusColumns` instead
|
||||
* [Added] new event `clickPaging`
|
||||
* [Added] new mode `nodata` for use with node.setStatus()
|
||||
* [Added] new method `node.addPagingNode()`
|
||||
* [Added] new method `node.replaceWith()`
|
||||
* [Added] new type 'paging' for `node.statusNodeType`
|
||||
* [Added] #542 new method `node.getSelectedNodes()`
|
||||
* [Added] Helper class `glyphicon-spin` to allow rotating loading icon with bootstrap3
|
||||
* [Improved] #356: serialize load requests
|
||||
* [Improved] #538: Be more robust if site css defines custom li:before
|
||||
* [Improved] ext-table: Define table row templates in `<tbody>`
|
||||
* [Improved] ext-table: `<thead>` is now optional if `<tbody>` contains `<td>`s
|
||||
|
||||
# 2.14.0 / 2015-12-19
|
||||
* [CHANGED] #519 Refactored custom icon configuration:<br>
|
||||
(see also the [theming tutorial](https://github.com/mar10/fancytree/wiki/TutorialTheming))
|
||||
* [Added] `options.icon` option/callback.<br>
|
||||
Valid values are true, false, a string containing a class name or image
|
||||
url, or a callback returning that.
|
||||
* [Changed] `node.icon` option. Valid values are true, false, or a string
|
||||
containing a class name or image url.<br>
|
||||
This option existed before, but was stored in the `node.data.icon` namespace,
|
||||
and did not accept class names.
|
||||
* [DEPRECATED] `options.iconClass` callback: use `options.icon` instead
|
||||
* [DEPRECATED] `options.icons`: use `options.icon` instead
|
||||
* [DEPRECATED] `node.data.iconclass` option: use `node.icon` instead
|
||||
* [DEPRECATED] `node.data.icon` option: use `node.icon` instead
|
||||
* [Added] `tree.clear()` method.
|
||||
* [Added] #520 ext-persist: new event `beforeRestore`
|
||||
* [Fixed] #533 table-ext: nodeSetExpanded triggers redundant events
|
||||
|
||||
# 2.13.0 / 2015-11-16
|
||||
* [Changed] If a node is initalized as `lazy: true`, and `children: []`,
|
||||
treat it as 'loaded leaf node'.<br>
|
||||
This is consistent with a lazy node that has no children property at all (i.e.
|
||||
`undefined`). This would issue a lazyLoad event and a resopnse of `[]` would
|
||||
mark the node as leaf node.
|
||||
* [Added] new function $.ui.fancytree.getTree()
|
||||
* [Added] ext-filter methods node.isMatched() and tree.isFilterActive()
|
||||
* [Added] CSS for ext-childcounter badges is now part of the standard themes
|
||||
* [Added] ext-childcounter method node.updateCounter()`
|
||||
* [Fixed] #507 data-hideCheckbox="true"
|
||||
* [Fixed] #513 activeVisible option does not work on init
|
||||
* [Fixed] #516 ExtPersist requires cookie.js even when not using cookies
|
||||
|
||||
# 2.12.0 / 2015-09-10
|
||||
* [Changed] Documented `iconClass` callback and changed signature from
|
||||
`iconClass(node)` to `iconClass(event, data)`
|
||||
* [Added] ext-dnd events `initHelper` and `updateHelper`
|
||||
* [Added] ext-dnd option `smartRevert`
|
||||
* [Added] #146 sample for multi-node drag'n'drop
|
||||
* [Added] Sample for modifier keys to control copy/move behavior while dragging
|
||||
* [Added] `highlight` and `fuzzy` options to ext-filter
|
||||
* [Added] `fireActivate` option to ext-persist (default: true)
|
||||
* [Added] #496 new methods tree.findFirst() / .findAll()
|
||||
* [Improved] clearFilter() performance #491
|
||||
* [Improved] dnd registers global handlers to cancel on ESC and mousedown
|
||||
* [Fixed] #475 Font color while editing node title with bootstrap skin
|
||||
* [Fixed] #484 Glyph plugin: Missing margin-left for span.fancytree-custom-icon
|
||||
* [Fixed] #486 node.render(true) moves the node to the end of the list
|
||||
* [Fixed] #489 `focusOnClick` option is ignored for tables if 'dnd' is listed after 'table' extension
|
||||
* [Fixed] #495 Double clicking on expander with lazy-load causes assertion error
|
||||
|
||||
# 2.11.0 / 2015-07-26
|
||||
* [Changed] Adding `fancytree-plain` class to container (if not table), allowing for more efficient css
|
||||
* [Changed] #434: Use data-uris to inline loading.gif image
|
||||
* [Changed] #460: Use padding-left instead of margin-left for table indent
|
||||
* [Changed] #465: Add `node` argument to the `toDict()` callback
|
||||
* [Improved] Nicer bootstrap theme and added table to the example
|
||||
* [Improved] #464: ext-dnd supports ext-glyph
|
||||
* [Improved] #466: Add counter badges to ext-filter
|
||||
* [Fixed] Win8 theme jumpy hover effects
|
||||
* [Fixed] #411: ext-edit fails with ext-table, when edit was cancelled
|
||||
* [Fixed] #463: ext-table: render(deep) does not work
|
||||
* [Fixed] #470: Wide plugin not present in jquery.fancytree-all.min.js
|
||||
|
||||
# 2.10.2 / 2015-07-02
|
||||
* [Fixed] Add `dist/skin-custom-1` sample (again)
|
||||
* [Fixed] #459 Don't collapse root folder when last node is removed
|
||||
|
||||
# 2.10.1 / 2015-06-27
|
||||
* [Changed] Undo #340: Revert dist folder layout to v2.9.0, but add
|
||||
dist/skin-common.less
|
||||
|
||||
# 2.10.0 / 2015-06-26 [YANKED]
|
||||
* [Changed] #340: New dist folder layout: moved skin-* folders into src/ folder
|
||||
(**Note:** this change was reverted in v2.10.1)
|
||||
* [Improved] Update to jQuery UI 1.11.4, jQuery 1.11.3
|
||||
* [Improved] #340: add `dist/skin-common.less` to fix theme imports
|
||||
* [Improved] #443 Support js-cookie (still compatible with jquery-cookie)
|
||||
* [Fixed] #415 selected and unselectable shows unchecked checkbox
|
||||
* [Fixed] #427 table + themeroller: apply color to TR
|
||||
* [Fixed] #442 filterBranches shall use opts to allow autoExpand
|
||||
* [Fixed] #445 enter key not handled correctly
|
||||
* [Fixed] #449 After deleting last child, parent node remains expanded
|
||||
* [Fixed] #452 destroy not removing nodes with ext-table
|
||||
* [Fixed] #457 Autoscroll fails with lazyloading returning empty list
|
||||
|
||||
# 2.9.0 / 2015-04-19
|
||||
* [Changed] ext-filter: `tree.filterNodes(filter, opts)` now accept an `opts`
|
||||
object instead of `leavesOnly`
|
||||
* [Improved] #417 only raise exception about data being a string if dataType is "json"
|
||||
* [Added] #394 New option `autoExpand` for [ext-filter]
|
||||
* [Fixed] #402, #405 rare exception in dnd events
|
||||
* [Fixed] #420 nodeSetActive not returning promise
|
||||
* [Fixed] #270 Keyboard focus not working when using dnd extension
|
||||
|
||||
# 2.8.1 / 2015-03-01
|
||||
* [Improved] generateFormElements() new argument `opts`, default: `{stopOnParents: true}`
|
||||
* [Fixed] #393 ext-table: checkboxColumnIdx not working
|
||||
* [Fixed] #397 ext-edit: Creating sub category fails
|
||||
* [Fixed] #403 generateFormElements() doesn't work with string args
|
||||
|
||||
# 2.8.0 / 2015-02-08
|
||||
* [Changed] Deprecated ext-menu (was never officially supported, see http://localhost:8080/demo/index.html#sample-ext-menu.html)
|
||||
* [Improved] Bluring the widget will now blur the focused node too.
|
||||
* [Improved] Persistence will only set node focus if widget had focus (otherwise only activate the node).
|
||||
* [Improved] Set default focus on first keypress to active node (first node otherwise)
|
||||
* [Improved] #383 Accept [ECMAScript 6 Promise](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise) as source
|
||||
* [Added] `_superApply()` for hook handlers.
|
||||
* [Added] eventToString() supports mouse events
|
||||
* [Fixed] persistence for focus (when using non-cookie storage)
|
||||
* [Fixed] #391 Exception on autoscrolling filtered trees
|
||||
|
||||
# 2.7.0 / 2014-12-21
|
||||
* [CHANGED] Dropped `fx` option. Use `toggleEffect` instead.
|
||||
* [CHANGED] 'win8' and 'bootstrap' skins where modified to highlight the
|
||||
title span instead of the node span, in order to be compatible with
|
||||
[ext-wide]. The original skins are available as 'skin-win8-n' and
|
||||
'skin-bootstrap-n' respectively.
|
||||
* [Added] ext-wide extension (experimental)
|
||||
* [Added] LESS files to distribution
|
||||
* [Added] Publish on [cdnjs](https://cdnjs.com/libraries/jquery.fancytree)
|
||||
* [Improved] tree.reactivate() returns a promise
|
||||
* [Fixed] #246 Gaps when filtering in hide mode (patch by @lefunque)
|
||||
* [Fixed] #287 wrong image on hovers
|
||||
* [Fixed] #368 Standard browser behavior prevented (e.g. zoom with Ctrl+'+'/'-')
|
||||
* [Fixed] #369 Suppress warning, when dropping top- on top-node
|
||||
|
||||
# 2.6.0 / 2014-11-29
|
||||
* [Added] Option `focusOnSelect` to set focus when node is checked by a mouse
|
||||
click (default: false)
|
||||
* [Added] `restore` event, sent after ext-persist has restored the tree state
|
||||
* [Improved] #359 Better navigation performance when skipping hidden nodes
|
||||
* Publish on npm Registry
|
||||
|
||||
# 2.5.0 / 2014-11-23
|
||||
* [CHANGED] [ext-persist] overrideSource option now defaults to true
|
||||
* [Added] [ext-filter] Option `autoApply` re-applies filter on lazy loading
|
||||
(on by default)
|
||||
* [Added] quicksearch: navigate to next node by typing the first letters
|
||||
* [Improved] [ext-dnd] Make draggable helper and parent configurable
|
||||
* [Improved] #153 Add class `fancytree-unselectable` to respective nodes and
|
||||
dim unselectable checkboxes
|
||||
* [Improved] Update to jQuery 1.1.11, jQuery UI 1.11.2
|
||||
* [Improved] New mode 'firstChild' for node.addNode()
|
||||
* [Fixed] #324 Fix problem where minExpandLevel was not expanding root node
|
||||
* [Fixed] #300 dnd.focusOnClick for jQuery UI 1.11
|
||||
* [Fixed] #354 [ext-persist] with selectMode 3
|
||||
|
||||
# 2.4.1 / 2014-09-23
|
||||
* [Fixed] Regression #323
|
||||
|
||||
# 2.4.0 / 2014-09-21
|
||||
* [CHANGED] Renamed dist/jquery.fancytree-custom.min.js to jquery.fancytree-all.min.js
|
||||
* [CHANGED] ext-edit callbacks no longer pass `data.value` (use `data.input.val()` instead).
|
||||
* [Added] CDN support (http://www.jsdelivr.com/#!jquery.fancytree)
|
||||
* [Added] New method `node.visitAndLoad()`
|
||||
* [Added] New method `node.editCreateNode()` (ext-edit)
|
||||
* [Added] New method `node.isRootNode()`
|
||||
* [Added] New method `node.isTopLevel()`
|
||||
* [Added] New option `id` to override default tree id
|
||||
* [Added] New argument `stopOnParents` for tree.generateFormElements()
|
||||
* [Improved] #294 node.load() should resolve 'ok', if node is already loaded
|
||||
* [Improved] #293 minExpandLevel does not auto-expand
|
||||
* [Improved] #313 Allow HTML in tooltips
|
||||
* [Fixed] crash in scrollIntoView() when parent is `window`
|
||||
* [Fixed] #305 Checkbox doesn't show with Glyph + Table
|
||||
* [Fixed] #316 Fix hasChildren() when children = []
|
||||
* [Fixed] #237 Ajax LoadError not updated in StatusNode with Table ext
|
||||
* [Fixed] #295 loadKeyPath with multiple paths
|
||||
* [DEPRECATED] node.isRoot(). Use node.isRootNode() instead
|
||||
|
||||
# 2.3.0 / 2014-08-17
|
||||
* [CHANGED] renamed (undocumented) event 'loaderror' to 'loadError'
|
||||
* [Added] postProcess now allows to signal error conditions (so it becomes easy to handle custom Ajax response formats)
|
||||
* [Added] node.setStatus()
|
||||
* [Added] ext-clones to the standard distribution.
|
||||
* [Improved] loadError allows to return `false` to prevent default handling
|
||||
* [Fixed] #258 Fix moveTo when moving a node to same parent
|
||||
* [Fixed] #257 Glyph expander sometimes disappears
|
||||
|
||||
# 2.2.0 / 2014-06-28
|
||||
* [Added] Option dnd.focusOnClick sets focus to tree widget, even when dragging
|
||||
is enabled
|
||||
* [Added] node.info()
|
||||
* [Improved] #245 tree.generateInput() now returns data using PHPs array
|
||||
convention, i.e. by appending brackets to the name: 'ft_1[]'.
|
||||
* [Fixed] #250: Children lazy empty nodes remain checked when parent is
|
||||
unchecked with hierarchical multi-selection
|
||||
* [Fixed] #272 Navigation in filtered trees
|
||||
|
||||
# 2.1.0 / 2014-05-29
|
||||
* [Added] #210: [ext-persist] optionally store information in sessionStorage or localStorage
|
||||
* [Added] #64 [ext-filter] filterBranches() will restrict display to sub-trees
|
||||
* [Added] New options 'scrollParent' and 'scrollOfs' are evaluated by node.scrollIntoView()
|
||||
(which is also called on expand). This allows autoScroll to work with
|
||||
[ext-table]: set scrollParent to `window` or a wrapper DIV with overflow: auto
|
||||
* [Added] [ext-wide] use 100% wide selection bar (experimental)
|
||||
* [Added] $.ui.fancytree.debounce()
|
||||
* [Improved] [ext-columnview] css
|
||||
* [Improved] skin-win8 now includes the loading.gif as inline CSS for faster response
|
||||
* [Improved] Add 'fancytree-icon' class to icon IMG
|
||||
* [Improved] css v-align for checkboxes and icons
|
||||
* [Fixed] #217: persistence when node keys are numeric
|
||||
* [Fixed] #228: html in node title prevents click
|
||||
* [Fixed] #235: D'n'd helper is displaced, when window is scrolled
|
||||
* [Fixed] #241: fromDict() does not update node title
|
||||
* [Fixed] relative custom imagePath option
|
||||
* [DEPRECATED] [ext-filter] Use filterNodes() instead of applyFilter()
|
||||
* [DEPRECATED] [ext-filter] 'leavesOnly' option removed (see filterNodes())
|
||||
|
||||
# 2.0.0 / 2014-05-01
|
||||
* Released 2.0.0
|
||||
|
||||
# 1.x
|
||||
|
||||
* See [Dynatree](https://code.google.com/p/dynatree/)
|
|
@ -0,0 +1,629 @@
|
|||
###
|
||||
Build scripts for Fancytree
|
||||
###
|
||||
|
||||
"use strict"
|
||||
|
||||
module.exports = (grunt) ->
|
||||
|
||||
grunt.initConfig
|
||||
|
||||
pkg:
|
||||
grunt.file.readJSON("package.json")
|
||||
|
||||
# Project metadata, used by the <banner> directive.
|
||||
meta:
|
||||
banner: "/*! <%= pkg.title || pkg.name %> - @VERSION - @DATE\n" +
|
||||
# "<%= grunt.template.today('yyyy-mm-dd HH:mm') %>\n" +
|
||||
"<%= pkg.homepage ? ' * ' + pkg.homepage + '\\n' : '' %>" +
|
||||
" * Copyright (c) <%= grunt.template.today('yyyy') %> <%= pkg.author.name %>;" +
|
||||
" Licensed <%= _.map(pkg.licenses, 'type').join(', ') %>\n" +
|
||||
" */\n"
|
||||
# separator: "\n/*! --- Fancytree Plugin --- */\n"
|
||||
|
||||
clean:
|
||||
build:
|
||||
src: [ "build" ]
|
||||
dist:
|
||||
src: [ "dist" ]
|
||||
post_build: # Remove unwanted files from build folder
|
||||
src: [
|
||||
"build/jquery.fancytree.*.min.js"
|
||||
"build/jquery.fancytree.js"
|
||||
"build/jquery-ui-dependencies/"
|
||||
]
|
||||
|
||||
concat:
|
||||
core_to_build:
|
||||
options:
|
||||
stripBanners: true
|
||||
src: ["<banner:meta.banner>"
|
||||
"src/jquery.fancytree.js"
|
||||
]
|
||||
dest: "build/jquery.fancytree.js"
|
||||
bundle_to_build:
|
||||
options:
|
||||
stripBanners: true
|
||||
src: [
|
||||
"<%= meta.banner %>"
|
||||
"src/jquery.fancytree.js"
|
||||
# "src/jquery.fancytree.ariagrid.js"
|
||||
"src/jquery.fancytree.childcounter.js"
|
||||
"src/jquery.fancytree.clones.js"
|
||||
# "src/jquery.fancytree.columnview.js"
|
||||
"src/jquery.fancytree.dnd.js"
|
||||
"src/jquery.fancytree.dnd5.js"
|
||||
"src/jquery.fancytree.edit.js"
|
||||
"src/jquery.fancytree.filter.js"
|
||||
# "src/jquery.fancytree.fixed.js"
|
||||
"src/jquery.fancytree.glyph.js"
|
||||
# "src/jquery.fancytree.grid.js"
|
||||
"src/jquery.fancytree.gridnav.js"
|
||||
"src/jquery.fancytree.multi.js"
|
||||
"src/jquery.fancytree.persist.js"
|
||||
"src/jquery.fancytree.table.js"
|
||||
"src/jquery.fancytree.themeroller.js"
|
||||
"src/jquery.fancytree.wide.js"
|
||||
]
|
||||
dest: "build/jquery.fancytree-all.js"
|
||||
|
||||
amd_bundle_min:
|
||||
options:
|
||||
banner: "<%= meta.banner %>"
|
||||
stripBanners: true
|
||||
process: (src, fspec) ->
|
||||
# Remove all comments, including /*! ... */
|
||||
src = src.replace(/\/\*(.|\n)*\*\//g, "")
|
||||
if /fancytree..+.min.js/.test(fspec)
|
||||
# If it is an extension:
|
||||
# Prepend a one-liner instead
|
||||
fspec = fspec.substr(6) # strip 'build/'
|
||||
src = "\n/*! Extension '" + fspec + "' */" + src
|
||||
return src
|
||||
src: [
|
||||
"lib/amd-intro-require-native-ui.js"
|
||||
"build/jquery.fancytree.min.js"
|
||||
# "build/jquery.fancytree.ariagrid.min.js"
|
||||
"build/jquery.fancytree.childcounter.min.js"
|
||||
"build/jquery.fancytree.clones.min.js"
|
||||
# "build/jquery.fancytree.columnview.min.js"
|
||||
"build/jquery.fancytree.dnd.min.js"
|
||||
"build/jquery.fancytree.dnd5.min.js"
|
||||
"build/jquery.fancytree.edit.min.js"
|
||||
"build/jquery.fancytree.filter.min.js"
|
||||
# "build/jquery.fancytree.fixed.min.js"
|
||||
"build/jquery.fancytree.glyph.min.js"
|
||||
# "build/jquery.fancytree.grid.min.js"
|
||||
"build/jquery.fancytree.gridnav.min.js"
|
||||
"build/jquery.fancytree.multi.min.js"
|
||||
"build/jquery.fancytree.persist.min.js"
|
||||
"build/jquery.fancytree.table.min.js"
|
||||
"build/jquery.fancytree.themeroller.min.js"
|
||||
"build/jquery.fancytree.wide.min.js"
|
||||
"lib/amd-outro.js"
|
||||
]
|
||||
dest: "build/jquery.fancytree-all.min.js"
|
||||
|
||||
all_deps: # un-minified, so we can generate a map file
|
||||
options:
|
||||
banner: "<%= meta.banner %>"
|
||||
stripBanners: true
|
||||
process: (src, fspec) ->
|
||||
# Remove all comments, including /*! ... */
|
||||
# (but keep disclaimer for jQuery-UI)
|
||||
# if not /jquery-ui..+.js/.test(fspec)
|
||||
# # if not /jquery-ui..+.min.js/.test(fspec)
|
||||
# src = src.replace(/\/\*(.|\n)*\*\//g, "")
|
||||
# # strip out AMD related code from jQuery-UI and make it an IIFE
|
||||
# if /jquery-ui..+.min.js/.test(fspec)
|
||||
# src = src.replace(/\(function.+jQuery\)}\)\((.+\)}\)})\);/, "!$1(jQuery);")
|
||||
if /jquery.fancytree.js/.test(fspec)
|
||||
src = "\n/*! Fancytree Core */" + src
|
||||
if /fancytree..+.js/.test(fspec)
|
||||
# If it is an extension:
|
||||
# Prepend a one-liner instead
|
||||
fspec = fspec.substr(4) # strip 'src/'
|
||||
src = "\n/*! Extension '" + fspec + "' */" + src
|
||||
return src
|
||||
src: [
|
||||
"<%= meta.banner %>"
|
||||
# Inline jQuery UI custom (AMD header removed: IIFE only)
|
||||
"src/jquery-ui-dependencies/jquery-ui-iife.js"
|
||||
# Fancytree core and extensions, wrapped in UMD pattern
|
||||
"lib/amd-intro-require-jquery.js"
|
||||
"src/jquery.fancytree.js"
|
||||
# "src/jquery.fancytree.ariagrid.js"
|
||||
"src/jquery.fancytree.childcounter.js"
|
||||
"src/jquery.fancytree.clones.js"
|
||||
# "src/jquery.fancytree.columnview.js"
|
||||
# "src/jquery.fancytree.dnd.js" # Draggable widget is not part of our custom jQuery UI dependencies
|
||||
"src/jquery.fancytree.dnd5.js"
|
||||
"src/jquery.fancytree.edit.js"
|
||||
"src/jquery.fancytree.filter.js"
|
||||
# "src/jquery.fancytree.fixed.js"
|
||||
"src/jquery.fancytree.glyph.js"
|
||||
# "src/jquery.fancytree.grid.js"
|
||||
"src/jquery.fancytree.gridnav.js"
|
||||
"src/jquery.fancytree.multi.js"
|
||||
"src/jquery.fancytree.persist.js"
|
||||
"src/jquery.fancytree.table.js"
|
||||
"src/jquery.fancytree.themeroller.js"
|
||||
"src/jquery.fancytree.wide.js"
|
||||
"lib/amd-outro.js"
|
||||
]
|
||||
dest: "build/jquery.fancytree-all-deps.js"
|
||||
|
||||
connect:
|
||||
forever:
|
||||
options:
|
||||
port: 8080
|
||||
base: "./"
|
||||
keepalive: true
|
||||
dev: # pass on, so subsequent tasks (like watch) can start
|
||||
options:
|
||||
port: 8080
|
||||
base: "./"
|
||||
keepalive: false
|
||||
# middleware: (connect) ->
|
||||
# return [
|
||||
# (req, res, next) ->
|
||||
# res.setHeader('Access-Control-Allow-Origin', '*')
|
||||
# res.setHeader('Access-Control-Allow-Methods', '*')
|
||||
# next()
|
||||
# ]
|
||||
sauce: # Used by sauce tasks, see https://wiki.saucelabs.com/display/DOCS/Grunt-Saucelabs+Set+Up%2C+Configuration%2C+and+Usage
|
||||
options:
|
||||
# hostname: "localhost"
|
||||
# hostname: "127.0.0.1"
|
||||
# port: 8080
|
||||
port: 9999
|
||||
base: ""
|
||||
keepalive: false
|
||||
localhost_9999: # Start web server for Sauce live testing w/ manuallly run bin/sc.exe
|
||||
options:
|
||||
# hostname: "localhost"
|
||||
port: 9999
|
||||
base: ""
|
||||
keepalive: true
|
||||
|
||||
copy:
|
||||
build: # Copy development files to build folder
|
||||
files: [{ # src/ => build/
|
||||
expand: true # required for cwd
|
||||
cwd: "src/"
|
||||
src: [
|
||||
"skin-**/*.{css,gif,md,png,less}"
|
||||
"skin-common.less"
|
||||
"*.txt"
|
||||
]
|
||||
dest: "build/"
|
||||
}, { # src/ => build/modules/
|
||||
expand: true
|
||||
cwd: "src/"
|
||||
src: [ "jquery.*.js" ]
|
||||
dest: "build/modules/"
|
||||
}, { # Top-level => build/
|
||||
src: ["LICENSE.txt"]
|
||||
dest: "build/"
|
||||
}]
|
||||
ui_deps: #
|
||||
files: [{
|
||||
src: "src/jquery-ui-dependencies/jquery.fancytree.ui-deps.js"
|
||||
dest: "build/modules/jquery.fancytree.ui-deps.js"
|
||||
}]
|
||||
dist: # Copy build folder to dist/ (recursive)
|
||||
files: [
|
||||
{expand: true, cwd: "build/", src: ["**"], dest: "dist/"}
|
||||
]
|
||||
|
||||
cssmin:
|
||||
options:
|
||||
report: "min"
|
||||
build:
|
||||
expand: true
|
||||
cwd: "build/"
|
||||
src: ["**/*.fancytree.css", "!*.min.css"]
|
||||
dest: "build/"
|
||||
ext: ".fancytree.min.css"
|
||||
|
||||
devUpdate:
|
||||
main:
|
||||
options:
|
||||
reportUpdated: true
|
||||
updateType: 'prompt' # 'report'
|
||||
|
||||
docco:
|
||||
docs:
|
||||
src: ["src/jquery.fancytree.childcounter.js"]
|
||||
options:
|
||||
output: "doc/annotated-src"
|
||||
|
||||
eslint:
|
||||
options:
|
||||
maxWarnings: 100
|
||||
# format: "stylish"
|
||||
# options:
|
||||
# # See https://github.com/sindresorhus/grunt-eslint/issues/119
|
||||
# quiet: true
|
||||
# We have to explicitly declare "src" property otherwise "newer"
|
||||
# task wouldn't work properly :/
|
||||
build:
|
||||
options:
|
||||
ignore: false
|
||||
src: [
|
||||
"build/jquery.fancytree.js"
|
||||
"build/jquery.fancytree-all.js"
|
||||
"build/modules/*.js"
|
||||
]
|
||||
dev:
|
||||
src: [
|
||||
"src/*.js"
|
||||
"3rd-party/**/jquery.fancytree.*.js"
|
||||
# "test/**/test-*.js"
|
||||
"demo/**/*.js"
|
||||
]
|
||||
fix:
|
||||
options:
|
||||
fix: true
|
||||
src: [
|
||||
"src/*.js"
|
||||
"3rd-party/**/jquery.fancytree.*.js"
|
||||
# "test/**/test-*.js"
|
||||
"demo/**/*.js"
|
||||
]
|
||||
|
||||
exec:
|
||||
upload:
|
||||
# FTP upload the demo files (requires https://github.com/mar10/pyftpsync)
|
||||
stdin: true # Allow interactive console
|
||||
cmd: "pyftpsync upload . ftp://www.wwwendt.de/tech/fancytree --progress --exclude build,node_modules,.*,_* --delete-unmatched"
|
||||
upload_force:
|
||||
# FTP upload the demo files (requires https://github.com/mar10/pyftpsync)
|
||||
cmd: "pyftpsync upload . ftp://www.wwwendt.de/tech/fancytree --progress --exclude build,node_modules,.*,_* --delete-unmatched --resolve=local --force"
|
||||
|
||||
jsdoc:
|
||||
build:
|
||||
src: ["src/*.js", "doc/README.md"]
|
||||
options:
|
||||
destination: "doc/jsdoc"
|
||||
template: "bin/jsdoc3-moogle",
|
||||
configure: "doc/jsdoc.conf.json"
|
||||
verbose: true
|
||||
|
||||
less:
|
||||
development:
|
||||
options:
|
||||
# paths: ["src/"]
|
||||
# report: "min"
|
||||
compress: false
|
||||
yuicompress: false
|
||||
# optimization: 10
|
||||
|
||||
# webpack uses /dist/skin-common.less as root path
|
||||
# grunt-less uses /dist/skin-Xxx/ui.fancyree.less as root path
|
||||
# So we define our theme LESS files for webpack compatibility
|
||||
# and fix it for grunt-less here:
|
||||
rootpath: ".."
|
||||
files: [
|
||||
{expand: true, cwd: "src/", src: "**/ui.fancytree.less", dest: "src/", ext: ".fancytree.css"}
|
||||
]
|
||||
|
||||
qunit:
|
||||
options:
|
||||
httpBase: "http://localhost:8080"
|
||||
# httpBase: "http://127.0.0.1:8080"
|
||||
build: [
|
||||
"test/unit/test-core-build.html"
|
||||
]
|
||||
develop: [
|
||||
"test/unit/test-core.html"
|
||||
"test/unit/test-ext-filter.html"
|
||||
"test/unit/test-ext-table.html"
|
||||
"test/unit/test-ext-misc.html"
|
||||
]
|
||||
dist: [
|
||||
"test/unit/test-core-dist.html"
|
||||
]
|
||||
|
||||
replace: # grunt-text-replace
|
||||
production:
|
||||
src: ["build/**/*.{js,less,css}"]
|
||||
overwrite : true
|
||||
replacements: [ {
|
||||
from : /@DATE/g
|
||||
# https://github.com/felixge/node-dateformat
|
||||
to : "<%= grunt.template.today('isoUtcDateTime') %>"
|
||||
},{
|
||||
from : /buildType:\s*\"[a-zA-Z]+\"/g
|
||||
to : "buildType: \"production\""
|
||||
},{
|
||||
from : /debugLevel:\s*[0-9]/g
|
||||
to : "debugLevel: 3"
|
||||
} ]
|
||||
release:
|
||||
src: ["dist/**/*.{js,less,css}"]
|
||||
overwrite : true
|
||||
replacements: [ {
|
||||
from : /@VERSION/g
|
||||
to : "<%= pkg.version %>"
|
||||
} ]
|
||||
|
||||
"saucelabs-qunit":
|
||||
options:
|
||||
build: process.env.TRAVIS_JOB_ID
|
||||
throttled: 5
|
||||
framework: "qunit"
|
||||
# Map of extra parameters to be passed to sauce labs. example:
|
||||
# {'video-upload-on-pass': false, 'idle-timeout': 60}
|
||||
sauceConfig:
|
||||
"video-upload-on-pass": false
|
||||
recordVideo: true
|
||||
# Needed for Edge/Windows (as of 2019-06-02) and Firefox(?)
|
||||
iedriverVersion: "3.141.59"
|
||||
seleniumVersion: "3.141.59"
|
||||
# Array of optional arguments to be passed to the Sauce Connect tunnel.
|
||||
# See https://saucelabs.com/docs/additional-config
|
||||
tunnelArgs: [
|
||||
'-v',
|
||||
'--logfile', 'saucelabs-tunnel.log',
|
||||
'--tunnel-domains', 'localhost,travis.dev'
|
||||
# '--direct-domains', 'google.com'
|
||||
]
|
||||
|
||||
triage:
|
||||
options:
|
||||
testname: "Triage"
|
||||
build: "triage"
|
||||
# urls: ["http://wwwendt.de/tech/fancytree/test/unit/test-core.html"]
|
||||
# urls: ["http://127.0.0.1:9999/test/unit/test-jQuery19-ui19.html"]
|
||||
# urls: ["http://127.0.0.1:9999/test/unit/test-jQuery1x-mig-ui1x.html"]
|
||||
urls: ["http://localhost:9999/test/unit/test-core.html"]
|
||||
# urls: ["http://127.0.0.1:9999/test/unit/test-core.html"]
|
||||
# tunneled: false # Use bin/sc manually
|
||||
browsers: [
|
||||
# Issue #825
|
||||
# { browserName: "chrome", version: "dev", platform: "Windows 10" }
|
||||
# { browserName: "internet explorer", version: "9", platform: "Windows 7" }
|
||||
# { browserName: "internet explorer", version: "8", platform: "Windows 7" }
|
||||
# { browserName: "chrome", version: "latest", platform: "Windows 10" }
|
||||
# { browserName: "microsoftedge", version: "latest", platform: "Windows 10" }
|
||||
# { browserName: "safari", version: "12", platform: "macOS 10.14" }
|
||||
{ browserName: "firefox", version: "latest", platform: "Windows 10" }
|
||||
]
|
||||
|
||||
ui_112:
|
||||
options:
|
||||
testname: "Fancytree qunit tests (jQuery 3, jQuery UI 1.12)"
|
||||
# urls: ["http://wwwendt.de/tech/fancytree/test/unit/test-core.html"]
|
||||
urls: ["http://localhost:9999/test/unit/test-core.html"]
|
||||
# urls: ["http://127.0.0.1:9999/test/unit/test-core.html"]
|
||||
# jQuery 3 supports IE 9+ and latest Chrome/Edge/Firefox/Safari (-1)
|
||||
# jQuery UI 1.12 supports IE 11 and latest Chrome/Edge/Firefox/Safari (-1)
|
||||
browsers: [
|
||||
{ browserName: "chrome", version: "latest", platform: "Windows 10" }
|
||||
{ browserName: "chrome", version: "latest-1", platform: "Windows 10" }
|
||||
{ browserName: "firefox", version: "latest", platform: "Windows 10" }
|
||||
{ browserName: "firefox", version: "latest-1", platform: "Windows 10" }
|
||||
{ browserName: "firefox", version: "latest", platform: "Linux" }
|
||||
{ browserName: "microsoftedge", version: "latest", platform: "Windows 10" }
|
||||
{ browserName: "microsoftedge", version: "latest-1", platform: "Windows 10" }
|
||||
{ browserName: "internet explorer", version: "11", platform: "Windows 8.1" }
|
||||
{ browserName: "internet explorer", version: "10", platform: "Windows 8" }
|
||||
{ browserName: "internet explorer", version: "9", platform: "Windows 7" }
|
||||
# Test Saucelabs:
|
||||
# { browserName: "chrome", version: "latest", platform: "macOS 10.14" }
|
||||
# { browserName: "firefox", version: "latest", platform: "macOS 10.14" }
|
||||
|
||||
]
|
||||
# ui_111:
|
||||
# options:
|
||||
# testname: "Fancytree qunit tests (jQuery 1.11, jQuery UI 1.11)"
|
||||
# # urls: ["http://wwwendt.de/tech/fancytree/test/unit/test-jQuery111-ui111.html"]
|
||||
# urls: ["http://127.0.0.1:9999/test/unit/test-jQuery111-ui111.html"]
|
||||
# # jQuery 1.11 supports IE + and latest Chrome/Edge/Firefox/Safari (-1)
|
||||
# # jQuery UI 1.11 supports IE 7+ and ?
|
||||
# browsers: [
|
||||
# { browserName: "internet explorer", version: "10", platform: "Windows 8" }
|
||||
# # Issue #842:
|
||||
# # { browserName: "safari", version: "7", platform: "OS X 10.9" }
|
||||
# { browserName: "safari", version: "8", platform: "OS X 10.10" }
|
||||
# ]
|
||||
# ui_110:
|
||||
# options:
|
||||
# testname: "Fancytree qunit tests (jQuery 1.10, jQuery UI 1.10)"
|
||||
# # urls: ["http://wwwendt.de/tech/fancytree/test/unit/test-jQuery110-ui110.html"]
|
||||
# urls: ["http://127.0.0.1:9999/test/unit/test-jQuery110-ui110.html"]
|
||||
# # jQuery 1.10 dropped support for IE 6
|
||||
# # jQuery UI 1.10 supports IE 7+ and ?
|
||||
# browsers: [
|
||||
# # { browserName: "internet explorer", version: "8", platform: "Windows 7" }
|
||||
# { browserName: "internet explorer", version: "9", platform: "Windows 7" }
|
||||
# ]
|
||||
beta: # This tests are allowed to fail in the travis matrix
|
||||
options:
|
||||
testname: "Fancytree qunit tests ('dev' browser versions)"
|
||||
# urls: ["http://wwwendt.de/tech/fancytree/test/unit/test-core.html"]
|
||||
urls: ["http://localhost:9999/test/unit/test-core.html"]
|
||||
# urls: ["http://127.0.0.1:9999/test/unit/test-core.html"]
|
||||
browsers: [
|
||||
# Issue #825
|
||||
{ browserName: "chrome", version: "dev", platform: "Windows 10" } #, chromedriverVersion: "2.46.0" }
|
||||
# FF.dev is problematic: https://support.saucelabs.com/hc/en-us/articles/225253808-Firefox-Dev-Beta-Browser-Won-t-Start
|
||||
{ browserName: "firefox", version: "dev", platform: "Windows 10" }
|
||||
# 2019-06-02: known problem with Saucelabs using localhost on macOS:
|
||||
{ browserName: "safari", version: "12", platform: "macOS 10.14" }
|
||||
{ browserName: "safari", version: "11", platform: "macOS 10.13" }
|
||||
{ browserName: "safari", version: "10", platform: "macOS 10.12" }
|
||||
{ browserName: "safari", version: "9", platform: "OS X 10.11" }
|
||||
# { browserName: "safari", version: "8", platform: "OS X 10.10" }
|
||||
]
|
||||
|
||||
uglify:
|
||||
src_to_build:
|
||||
options: # see https://github.com/gruntjs/grunt-contrib-uglify/issues/366
|
||||
report: "min"
|
||||
# preserveComments: "some"
|
||||
preserveComments: /(?:^!|@(?:license|preserve|cc_on))/
|
||||
output:
|
||||
ascii_only: true # #815
|
||||
files: [
|
||||
{
|
||||
src: ["**/jquery.fancytree*.js", "!*.min.js"]
|
||||
cwd: "src/"
|
||||
dest: "build/"
|
||||
expand: true
|
||||
rename: (dest, src) ->
|
||||
folder = src.substring(0, src.lastIndexOf("/"))
|
||||
filename = src.substring(src.lastIndexOf("/"), src.length)
|
||||
filename = filename.substring(0, filename.lastIndexOf("."))
|
||||
return dest + folder + filename + ".min.js"
|
||||
}
|
||||
]
|
||||
|
||||
all_deps:
|
||||
options: # see https://github.com/gruntjs/grunt-contrib-uglify/issues/366
|
||||
report: "min"
|
||||
sourceMap: true
|
||||
# preserveComments: "some"
|
||||
preserveComments: /(?:^!|@(?:license|preserve|cc_on))/
|
||||
output:
|
||||
ascii_only: true # #815
|
||||
files: [
|
||||
{
|
||||
src: ["jquery.fancytree-all-deps.js"]
|
||||
cwd: "build/"
|
||||
dest: "build/"
|
||||
expand: true
|
||||
rename: (dest, src) ->
|
||||
folder = src.substring(0, src.lastIndexOf("/"))
|
||||
filename = src.substring(src.lastIndexOf("/"), src.length)
|
||||
filename = filename.substring(0, filename.lastIndexOf("."))
|
||||
return dest + folder + filename + ".min.js"
|
||||
}
|
||||
]
|
||||
|
||||
watch:
|
||||
less:
|
||||
files: "src/**/*.less"
|
||||
tasks: ["less:development"]
|
||||
eslint:
|
||||
options:
|
||||
atBegin: true
|
||||
files: ["src/*.js", "test/unit/*.js", "demo/**/*.js"]
|
||||
tasks: ["eslint:dev"]
|
||||
|
||||
yabs:
|
||||
release:
|
||||
common: # defaults for all tools
|
||||
manifests: ['package.json', 'bower.json']
|
||||
# The following tools are run in order:
|
||||
run_test: { tasks: ['test'] }
|
||||
check: { branch: ['master'], canPush: true, clean: true, cmpVersion: 'gte' }
|
||||
bump: {} # 'bump' also uses the increment mode `yabs:release:MODE`
|
||||
run_build: { tasks: ['make_dist'] }
|
||||
commit: { add: '.' }
|
||||
tag: {}
|
||||
push: { tags: true, useFollowTags: true },
|
||||
githubRelease:
|
||||
repo: 'mar10/fancytree'
|
||||
draft: false
|
||||
npmPublish: {}
|
||||
bump_develop: { inc: 'prepatch' }
|
||||
commit_develop: { message: 'Bump prerelease ({%= version %}) [ci skip]' }
|
||||
push_develop: {}
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
|
||||
# Load "grunt*" dependencies
|
||||
|
||||
for key of grunt.file.readJSON("package.json").devDependencies
|
||||
grunt.loadNpmTasks key if key isnt "grunt" and key.indexOf("grunt") is 0
|
||||
|
||||
# Register tasks
|
||||
|
||||
grunt.registerTask "server", ["connect:forever"]
|
||||
grunt.registerTask "dev", ["connect:dev", "watch"]
|
||||
# grunt.registerTask "prettier", ["eslint:fix"]
|
||||
grunt.registerTask "format", ["eslint:fix"]
|
||||
grunt.registerTask "test", [
|
||||
"eslint:dev",
|
||||
# "csslint",
|
||||
# "htmllint",
|
||||
"connect:dev" # start server
|
||||
"qunit:develop"
|
||||
]
|
||||
|
||||
grunt.registerTask "sauce", [
|
||||
"connect:sauce",
|
||||
"saucelabs-qunit:ui_112",
|
||||
# "saucelabs-qunit:ui_111",
|
||||
# "saucelabs-qunit:ui_110",
|
||||
]
|
||||
grunt.registerTask "sauce-optional", [
|
||||
"connect:sauce",
|
||||
"saucelabs-qunit:beta",
|
||||
]
|
||||
grunt.registerTask "sauce-triage", ["connect:sauce", "saucelabs-qunit:triage"]
|
||||
|
||||
# 2020-01-26 Saucelabs tests don't work.
|
||||
# Disable them in travis for now:
|
||||
grunt.registerTask "travis", ["test"]
|
||||
grunt.registerTask "travis-optional", []
|
||||
# if parseInt(process.env.TRAVIS_PULL_REQUEST, 10) > 0
|
||||
# # saucelab keys do not work on forks
|
||||
# # http://support.saucelabs.com/entries/25614798
|
||||
# grunt.registerTask "travis", ["test"]
|
||||
# grunt.registerTask "travis-optional", []
|
||||
# else
|
||||
# grunt.registerTask "travis", ["test", "sauce"]
|
||||
# grunt.registerTask "travis-optional", ["sauce-optional"]
|
||||
|
||||
grunt.registerTask "default", ["test"]
|
||||
grunt.registerTask "ci", ["test"] # Called by 'npm test'
|
||||
# Update package.json to latest versions (interactive)
|
||||
grunt.registerTask "dev-update", ["devUpdate"]
|
||||
|
||||
grunt.registerTask "build", [
|
||||
"less:development"
|
||||
"format"
|
||||
# `test` also starts the connect:dev server
|
||||
"test"
|
||||
"jsdoc:build"
|
||||
"docco:docs"
|
||||
"clean:build"
|
||||
"copy:build"
|
||||
"cssmin:build"
|
||||
"concat:core_to_build"
|
||||
"concat:bundle_to_build"
|
||||
"uglify:src_to_build"
|
||||
"concat:amd_bundle_min"
|
||||
"concat:all_deps"
|
||||
"uglify:all_deps"
|
||||
"clean:post_build"
|
||||
"replace:production"
|
||||
"eslint:build"
|
||||
"copy:ui_deps"
|
||||
"qunit:build"
|
||||
]
|
||||
|
||||
grunt.registerTask "make_dist", [
|
||||
# `build` also starts the connect:dev server
|
||||
"build"
|
||||
"clean:dist"
|
||||
"copy:dist"
|
||||
"clean:build"
|
||||
"replace:release"
|
||||
# "eslint:dist" # should rather use grunt-jsvalidate for minified output
|
||||
"qunit:dist"
|
||||
]
|
||||
|
||||
grunt.registerTask "upload", [
|
||||
"build"
|
||||
"exec:upload"
|
||||
]
|
||||
|
||||
grunt.registerTask "upload_force", [
|
||||
"build"
|
||||
"exec:upload_force"
|
||||
]
|
|
@ -0,0 +1,21 @@
|
|||
Copyright 2008-2020 Martin Wendt,
|
||||
https://wwWendt.de/
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -0,0 +1,71 @@
|
|||
#  Fancytree
|
||||
[](https://github.com/mar10/fancytree/releases/latest)
|
||||
[](https://travis-ci.org/mar10/fancytree)
|
||||
[](https://www.npmjs.com/package/jquery.fancytree)
|
||||
[](https://www.jsdelivr.com/package/npm/jquery.fancytree)
|
||||
[](https://github.com/prettier/prettier)
|
||||
[](https://stackoverflow.com/questions/tagged/fancytree)
|
||||
|
||||
<!--
|
||||
[](https://saucelabs.com/u/sauce-fancytree)
|
||||
-->
|
||||
|
||||
Fancytree (sequel of [DynaTree 1.x](https://code.google.com/p/dynatree/)) is a
|
||||
JavaScript tree view / tree grid plugin with support for keyboard, inline editing,
|
||||
filtering, checkboxes, drag'n'drop, and lazy loading.
|
||||
|
||||
[  ](https://wwWendt.de/tech/fancytree/demo "Live demo")
|
||||
|
||||
|
||||
### Status
|
||||
|
||||
[](https://github.com/mar10/fancytree/releases/latest)
|
||||
See the [change log](https://github.com/mar10/fancytree/blob/master/CHANGELOG.md)
|
||||
for details.
|
||||
|
||||
|
||||
### Get Started
|
||||
|
||||
* [Try the live demo](https://wwWendt.de/tech/fancytree/demo).
|
||||
* [Read the documentation](https://github.com/mar10/fancytree/wiki).
|
||||
* [Check the Q&A forum](https://groups.google.com/forum/#!forum/fancytree) or
|
||||
[Stackoverflow](http://stackoverflow.com/questions/tagged/fancytree) if you have questions.
|
||||
* Play with [jsFiddle](http://jsfiddle.net/mar10/KcxRd/),
|
||||
[CodePen](https://codepen.io/mar10/pen/WMWrbq),
|
||||
or [Plunker](http://plnkr.co/edit/8sdy3r?p=preview).
|
||||
* [Contribute](https://github.com/mar10/fancytree/wiki/HowtoContribute)
|
||||
|
||||
|
||||
### ES6 Quickstart
|
||||
|
||||
```js
|
||||
import $ from "jquery";
|
||||
|
||||
import 'jquery.fancytree/dist/skin-lion/ui.fancytree.less'; // CSS or LESS
|
||||
|
||||
import {createTree} from 'jquery.fancytree';
|
||||
|
||||
import 'jquery.fancytree/dist/modules/jquery.fancytree.edit';
|
||||
import 'jquery.fancytree/dist/modules/jquery.fancytree.filter';
|
||||
|
||||
const tree = createTree('#tree', {
|
||||
extensions: ['edit', 'filter'],
|
||||
source: {...},
|
||||
...
|
||||
});
|
||||
// Note: Loading and initialization may be asynchronous, so the nodes may not be accessible yet.
|
||||
```
|
||||
|
||||
See [module loader support](https://github.com/mar10/fancytree/wiki#use-a-module-loader) and
|
||||
[API docs](https://wwWendt.de/tech/fancytree/doc/jsdoc/Fancytree_Static.html#createTree).
|
||||
|
||||
|
||||
### Credits
|
||||
|
||||
Thanks to all [contributors](https://github.com/mar10/fancytree/contributors).
|
||||
|
||||
<!--
|
||||
### Browser Status Matrix
|
||||
|
||||
[](https://saucelabs.com/u/sauce-fancytree)
|
||||
-->
|
22
dd-sso/admin/src/admin/static/vendor/fancytree/bin/jsdoc3-moogle/README.md
vendored
Executable file
|
@ -0,0 +1,22 @@
|
|||
The default template for JSDoc 3 uses: [the Taffy Database library](http://taffydb.com/) and the [Underscore Template library](http://underscorejs.org/).
|
||||
|
||||
## Customized
|
||||
|
||||
Modified by Martin Wendt in 2017-08.
|
||||
|
||||
Changes are marked with `<!-- Start MOOGLE Changes -->` or `// --- Start MOOGLE Changes`
|
||||
|
||||
* Add a 'Methods:' section for class views, between 'Properties:' and 'Methods details'
|
||||
* Add google analytics hooks
|
||||
* Add a 'Fork me on Github'
|
||||
|
||||
|
||||
## Generating Typeface Fonts
|
||||
|
||||
The default template uses the [OpenSans](https://www.google.com/fonts/specimen/Open+Sans) typeface. The font files can be regenerated as follows:
|
||||
|
||||
1. Open the [OpenSans page at Font Squirrel](<http://www.fontsquirrel.com/fonts/open-sans>).
|
||||
2. Click on the 'Webfont Kit' tab.
|
||||
3. Either leave the subset drop-down as 'Western Latin (Default)', or, if we decide we need more glyphs, than change it to 'No Subsetting'.
|
||||
4. Click the 'DOWNLOAD @FONT-FACE KIT' button.
|
||||
5. For each typeface variant we plan to use, copy the 'eot', 'svg' and 'woff' files into the 'templates/default/static/fonts' directory.
|
701
dd-sso/admin/src/admin/static/vendor/fancytree/bin/jsdoc3-moogle/publish.js
vendored
Normal file
|
@ -0,0 +1,701 @@
|
|||
'use strict';
|
||||
|
||||
var doop = require('jsdoc/util/doop');
|
||||
var env = require('jsdoc/env');
|
||||
var fs = require('jsdoc/fs');
|
||||
var helper = require('jsdoc/util/templateHelper');
|
||||
var logger = require('jsdoc/util/logger');
|
||||
var path = require('jsdoc/path');
|
||||
var taffy = require('taffydb').taffy;
|
||||
var template = require('jsdoc/template');
|
||||
var util = require('util');
|
||||
|
||||
var htmlsafe = helper.htmlsafe;
|
||||
var linkto = helper.linkto;
|
||||
var resolveAuthorLinks = helper.resolveAuthorLinks;
|
||||
var hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
|
||||
var data;
|
||||
var view;
|
||||
|
||||
var outdir = path.normalize(env.opts.destination);
|
||||
|
||||
function find(spec) {
|
||||
return helper.find(data, spec);
|
||||
}
|
||||
|
||||
function tutoriallink(tutorial) {
|
||||
return helper.toTutorial(tutorial, null, {
|
||||
tag: 'em',
|
||||
classname: 'disabled',
|
||||
prefix: 'Tutorial: '
|
||||
});
|
||||
}
|
||||
|
||||
function getAncestorLinks(doclet) {
|
||||
return helper.getAncestorLinks(data, doclet);
|
||||
}
|
||||
|
||||
function hashToLink(doclet, hash) {
|
||||
var url;
|
||||
|
||||
if ( !/^(#.+)/.test(hash) ) {
|
||||
return hash;
|
||||
}
|
||||
|
||||
url = helper.createLink(doclet);
|
||||
url = url.replace(/(#.+|$)/, hash);
|
||||
|
||||
return '<a href="' + url + '">' + hash + '</a>';
|
||||
}
|
||||
|
||||
function needsSignature(doclet) {
|
||||
var needsSig = false;
|
||||
|
||||
// function and class definitions always get a signature
|
||||
if (doclet.kind === 'function' || doclet.kind === 'class') {
|
||||
needsSig = true;
|
||||
}
|
||||
// typedefs that contain functions get a signature, too
|
||||
else if (doclet.kind === 'typedef' && doclet.type && doclet.type.names &&
|
||||
doclet.type.names.length) {
|
||||
for (var i = 0, l = doclet.type.names.length; i < l; i++) {
|
||||
if (doclet.type.names[i].toLowerCase() === 'function') {
|
||||
needsSig = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// and namespaces that are functions get a signature (but finding them is a
|
||||
// bit messy)
|
||||
else if (doclet.kind === 'namespace' && doclet.meta && doclet.meta.code &&
|
||||
doclet.meta.code.type && doclet.meta.code.type.match(/[Ff]unction/)) {
|
||||
needsSig = true;
|
||||
}
|
||||
|
||||
return needsSig;
|
||||
}
|
||||
|
||||
function getSignatureAttributes(item) {
|
||||
var attributes = [];
|
||||
|
||||
if (item.optional) {
|
||||
attributes.push('opt');
|
||||
}
|
||||
|
||||
if (item.nullable === true) {
|
||||
attributes.push('nullable');
|
||||
}
|
||||
else if (item.nullable === false) {
|
||||
attributes.push('non-null');
|
||||
}
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
function updateItemName(item) {
|
||||
var attributes = getSignatureAttributes(item);
|
||||
var itemName = item.name || '';
|
||||
|
||||
if (item.variable) {
|
||||
itemName = '…' + itemName;
|
||||
}
|
||||
|
||||
if (attributes && attributes.length) {
|
||||
itemName = util.format( '%s<span class="signature-attributes">%s</span>', itemName,
|
||||
attributes.join(', ') );
|
||||
}
|
||||
|
||||
return itemName;
|
||||
}
|
||||
|
||||
function addParamAttributes(params) {
|
||||
return params.filter(function(param) {
|
||||
return param.name && param.name.indexOf('.') === -1;
|
||||
}).map(updateItemName);
|
||||
}
|
||||
|
||||
function buildItemTypeStrings(item) {
|
||||
var types = [];
|
||||
|
||||
if (item && item.type && item.type.names) {
|
||||
item.type.names.forEach(function(name) {
|
||||
types.push( linkto(name, htmlsafe(name)) );
|
||||
});
|
||||
}
|
||||
|
||||
return types;
|
||||
}
|
||||
|
||||
function buildAttribsString(attribs) {
|
||||
var attribsString = '';
|
||||
|
||||
if (attribs && attribs.length) {
|
||||
attribsString = htmlsafe( util.format('(%s) ', attribs.join(', ')) );
|
||||
}
|
||||
|
||||
return attribsString;
|
||||
}
|
||||
|
||||
function addNonParamAttributes(items) {
|
||||
var types = [];
|
||||
|
||||
items.forEach(function(item) {
|
||||
types = types.concat( buildItemTypeStrings(item) );
|
||||
});
|
||||
|
||||
return types;
|
||||
}
|
||||
|
||||
function addSignatureParams(f) {
|
||||
var params = f.params ? addParamAttributes(f.params) : [];
|
||||
|
||||
f.signature = util.format( '%s(%s)', (f.signature || ''), params.join(', ') );
|
||||
}
|
||||
|
||||
function addSignatureReturns(f) {
|
||||
var attribs = [];
|
||||
var attribsString = '';
|
||||
var returnTypes = [];
|
||||
var returnTypesString = '';
|
||||
var source = f.yields || f.returns;
|
||||
|
||||
// jam all the return-type attributes into an array. this could create odd results (for example,
|
||||
// if there are both nullable and non-nullable return types), but let's assume that most people
|
||||
// who use multiple @return tags aren't using Closure Compiler type annotations, and vice-versa.
|
||||
if (source) {
|
||||
source.forEach(function(item) {
|
||||
helper.getAttribs(item).forEach(function(attrib) {
|
||||
if (attribs.indexOf(attrib) === -1) {
|
||||
attribs.push(attrib);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
attribsString = buildAttribsString(attribs);
|
||||
}
|
||||
|
||||
if (source) {
|
||||
returnTypes = addNonParamAttributes(source);
|
||||
}
|
||||
if (returnTypes.length) {
|
||||
returnTypesString = util.format( ' → %s{%s}', attribsString, returnTypes.join('|') );
|
||||
}
|
||||
|
||||
f.signature = '<span class="signature">' + (f.signature || '') + '</span>' +
|
||||
'<span class="type-signature">' + returnTypesString + '</span>';
|
||||
}
|
||||
|
||||
function addSignatureTypes(f) {
|
||||
var types = f.type ? buildItemTypeStrings(f) : [];
|
||||
|
||||
f.signature = (f.signature || '') + '<span class="type-signature">' +
|
||||
(types.length ? ' :' + types.join('|') : '') + '</span>';
|
||||
}
|
||||
|
||||
function addAttribs(f) {
|
||||
var attribs = helper.getAttribs(f);
|
||||
var attribsString = buildAttribsString(attribs);
|
||||
|
||||
f.attribs = util.format('<span class="type-signature">%s</span>', attribsString);
|
||||
}
|
||||
|
||||
function shortenPaths(files, commonPrefix) {
|
||||
Object.keys(files).forEach(function(file) {
|
||||
files[file].shortened = files[file].resolved.replace(commonPrefix, '')
|
||||
// always use forward slashes
|
||||
.replace(/\\/g, '/');
|
||||
});
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
function getPathFromDoclet(doclet) {
|
||||
if (!doclet.meta) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return doclet.meta.path && doclet.meta.path !== 'null' ?
|
||||
path.join(doclet.meta.path, doclet.meta.filename) :
|
||||
doclet.meta.filename;
|
||||
}
|
||||
|
||||
function generate(title, docs, filename, resolveLinks) {
|
||||
var docData;
|
||||
var html;
|
||||
var outpath;
|
||||
|
||||
resolveLinks = resolveLinks !== false;
|
||||
|
||||
docData = {
|
||||
env: env,
|
||||
title: title,
|
||||
docs: docs
|
||||
};
|
||||
|
||||
outpath = path.join(outdir, filename);
|
||||
html = view.render('container.tmpl', docData);
|
||||
|
||||
if (resolveLinks) {
|
||||
html = helper.resolveLinks(html); // turn {@link foo} into <a href="foodoc.html">foo</a>
|
||||
}
|
||||
|
||||
fs.writeFileSync(outpath, html, 'utf8');
|
||||
}
|
||||
|
||||
function generateSourceFiles(sourceFiles, encoding) {
|
||||
encoding = encoding || 'utf8';
|
||||
Object.keys(sourceFiles).forEach(function(file) {
|
||||
var source;
|
||||
// links are keyed to the shortened path in each doclet's `meta.shortpath` property
|
||||
var sourceOutfile = helper.getUniqueFilename(sourceFiles[file].shortened);
|
||||
|
||||
helper.registerLink(sourceFiles[file].shortened, sourceOutfile);
|
||||
|
||||
try {
|
||||
source = {
|
||||
kind: 'source',
|
||||
code: helper.htmlsafe( fs.readFileSync(sourceFiles[file].resolved, encoding) )
|
||||
};
|
||||
}
|
||||
catch (e) {
|
||||
logger.error('Error while generating source file %s: %s', file, e.message);
|
||||
}
|
||||
|
||||
generate('Source: ' + sourceFiles[file].shortened, [source], sourceOutfile,
|
||||
false);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for classes or functions with the same name as modules (which indicates that the module
|
||||
* exports only that class or function), then attach the classes or functions to the `module`
|
||||
* property of the appropriate module doclets. The name of each class or function is also updated
|
||||
* for display purposes. This function mutates the original arrays.
|
||||
*
|
||||
* @private
|
||||
* @param {Array.<module:jsdoc/doclet.Doclet>} doclets - The array of classes and functions to
|
||||
* check.
|
||||
* @param {Array.<module:jsdoc/doclet.Doclet>} modules - The array of module doclets to search.
|
||||
*/
|
||||
function attachModuleSymbols(doclets, modules) {
|
||||
var symbols = {};
|
||||
|
||||
// build a lookup table
|
||||
doclets.forEach(function(symbol) {
|
||||
symbols[symbol.longname] = symbols[symbol.longname] || [];
|
||||
symbols[symbol.longname].push(symbol);
|
||||
});
|
||||
|
||||
modules.forEach(function(module) {
|
||||
if (symbols[module.longname]) {
|
||||
module.modules = symbols[module.longname]
|
||||
// Only show symbols that have a description. Make an exception for classes, because
|
||||
// we want to show the constructor-signature heading no matter what.
|
||||
.filter(function(symbol) {
|
||||
return symbol.description || symbol.kind === 'class';
|
||||
})
|
||||
.map(function(symbol) {
|
||||
symbol = doop(symbol);
|
||||
|
||||
if (symbol.kind === 'class' || symbol.kind === 'function') {
|
||||
symbol.name = symbol.name.replace('module:', '(require("') + '"))';
|
||||
}
|
||||
|
||||
return symbol;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function buildMemberNav(items, itemHeading, itemsSeen, linktoFn) {
|
||||
var nav = '';
|
||||
|
||||
if (items.length) {
|
||||
var itemsNav = '';
|
||||
|
||||
items.forEach(function(item) {
|
||||
var displayName;
|
||||
|
||||
if ( !hasOwnProp.call(item, 'longname') ) {
|
||||
itemsNav += '<li>' + linktoFn('', item.name) + '</li>';
|
||||
}
|
||||
else if ( !hasOwnProp.call(itemsSeen, item.longname) ) {
|
||||
if (env.conf.templates.default.useLongnameInNav) {
|
||||
displayName = item.longname;
|
||||
} else {
|
||||
displayName = item.name;
|
||||
}
|
||||
itemsNav += '<li>' + linktoFn(item.longname, displayName.replace(/\b(module|event):/g, '')) + '</li>';
|
||||
|
||||
itemsSeen[item.longname] = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (itemsNav !== '') {
|
||||
nav += '<h3>' + itemHeading + '</h3><ul>' + itemsNav + '</ul>';
|
||||
}
|
||||
}
|
||||
|
||||
return nav;
|
||||
}
|
||||
|
||||
function linktoTutorial(longName, name) {
|
||||
return tutoriallink(name);
|
||||
}
|
||||
|
||||
function linktoExternal(longName, name) {
|
||||
return linkto(longName, name.replace(/(^"|"$)/g, ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the navigation sidebar.
|
||||
* @param {object} members The members that will be used to create the sidebar.
|
||||
* @param {array<object>} members.classes
|
||||
* @param {array<object>} members.externals
|
||||
* @param {array<object>} members.globals
|
||||
* @param {array<object>} members.mixins
|
||||
* @param {array<object>} members.modules
|
||||
* @param {array<object>} members.namespaces
|
||||
* @param {array<object>} members.tutorials
|
||||
* @param {array<object>} members.events
|
||||
* @param {array<object>} members.interfaces
|
||||
* @return {string} The HTML for the navigation sidebar.
|
||||
*/
|
||||
function buildNav(members) {
|
||||
var globalNav;
|
||||
var nav = '<h2><a href="index.html">Home</a></h2>';
|
||||
var seen = {};
|
||||
var seenTutorials = {};
|
||||
|
||||
nav += buildMemberNav(members.modules, 'Modules', {}, linkto);
|
||||
nav += buildMemberNav(members.externals, 'Externals', seen, linktoExternal);
|
||||
nav += buildMemberNav(members.classes, 'Classes', seen, linkto);
|
||||
nav += buildMemberNav(members.events, 'Events', seen, linkto);
|
||||
nav += buildMemberNav(members.namespaces, 'Namespaces', seen, linkto);
|
||||
nav += buildMemberNav(members.mixins, 'Mixins', seen, linkto);
|
||||
nav += buildMemberNav(members.tutorials, 'Tutorials', seenTutorials, linktoTutorial);
|
||||
nav += buildMemberNav(members.interfaces, 'Interfaces', seen, linkto);
|
||||
|
||||
if (members.globals.length) {
|
||||
globalNav = '';
|
||||
|
||||
members.globals.forEach(function(g) {
|
||||
if ( g.kind !== 'typedef' && !hasOwnProp.call(seen, g.longname) ) {
|
||||
globalNav += '<li>' + linkto(g.longname, g.name) + '</li>';
|
||||
}
|
||||
seen[g.longname] = true;
|
||||
});
|
||||
|
||||
if (!globalNav) {
|
||||
// turn the heading into a link so you can actually get to the global page
|
||||
nav += '<h3>' + linkto('global', 'Global') + '</h3>';
|
||||
}
|
||||
else {
|
||||
nav += '<h3>Global</h3><ul>' + globalNav + '</ul>';
|
||||
}
|
||||
}
|
||||
|
||||
return nav;
|
||||
}
|
||||
|
||||
/**
|
||||
@param {TAFFY} taffyData See <http://taffydb.com/>.
|
||||
@param {object} opts
|
||||
@param {Tutorial} tutorials
|
||||
*/
|
||||
exports.publish = function(taffyData, opts, tutorials) {
|
||||
var classes;
|
||||
var conf;
|
||||
var externals;
|
||||
var files;
|
||||
var fromDir;
|
||||
var globalUrl;
|
||||
var indexUrl;
|
||||
var interfaces;
|
||||
var members;
|
||||
var mixins;
|
||||
var modules;
|
||||
var namespaces;
|
||||
var outputSourceFiles;
|
||||
var packageInfo;
|
||||
var packages;
|
||||
var sourceFilePaths = [];
|
||||
var sourceFiles = {};
|
||||
var staticFileFilter;
|
||||
var staticFilePaths;
|
||||
var staticFiles;
|
||||
var staticFileScanner;
|
||||
var templatePath;
|
||||
|
||||
data = taffyData;
|
||||
|
||||
conf = env.conf.templates || {};
|
||||
conf.default = conf.default || {};
|
||||
|
||||
templatePath = path.normalize(opts.template);
|
||||
view = new template.Template( path.join(templatePath, 'tmpl') );
|
||||
|
||||
// claim some special filenames in advance, so the All-Powerful Overseer of Filename Uniqueness
|
||||
// doesn't try to hand them out later
|
||||
indexUrl = helper.getUniqueFilename('index');
|
||||
// don't call registerLink() on this one! 'index' is also a valid longname
|
||||
|
||||
globalUrl = helper.getUniqueFilename('global');
|
||||
helper.registerLink('global', globalUrl);
|
||||
|
||||
// set up templating
|
||||
view.layout = conf.default.layoutFile ?
|
||||
path.getResourcePath(path.dirname(conf.default.layoutFile),
|
||||
path.basename(conf.default.layoutFile) ) :
|
||||
'layout.tmpl';
|
||||
|
||||
// set up tutorials for helper
|
||||
helper.setTutorials(tutorials);
|
||||
|
||||
data = helper.prune(data);
|
||||
data.sort('longname, version, since');
|
||||
helper.addEventListeners(data);
|
||||
|
||||
data().each(function(doclet) {
|
||||
var sourcePath;
|
||||
|
||||
doclet.attribs = '';
|
||||
|
||||
if (doclet.examples) {
|
||||
doclet.examples = doclet.examples.map(function(example) {
|
||||
var caption;
|
||||
var code;
|
||||
|
||||
if (example.match(/^\s*<caption>([\s\S]+?)<\/caption>(\s*[\n\r])([\s\S]+)$/i)) {
|
||||
caption = RegExp.$1;
|
||||
code = RegExp.$3;
|
||||
}
|
||||
|
||||
return {
|
||||
caption: caption || '',
|
||||
code: code || example
|
||||
};
|
||||
});
|
||||
}
|
||||
if (doclet.see) {
|
||||
doclet.see.forEach(function(seeItem, i) {
|
||||
doclet.see[i] = hashToLink(doclet, seeItem);
|
||||
});
|
||||
}
|
||||
|
||||
// build a list of source files
|
||||
if (doclet.meta) {
|
||||
sourcePath = getPathFromDoclet(doclet);
|
||||
sourceFiles[sourcePath] = {
|
||||
resolved: sourcePath,
|
||||
shortened: null
|
||||
};
|
||||
if (sourceFilePaths.indexOf(sourcePath) === -1) {
|
||||
sourceFilePaths.push(sourcePath);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// update outdir if necessary, then create outdir
|
||||
packageInfo = ( find({kind: 'package'}) || [] )[0];
|
||||
if (packageInfo && packageInfo.name) {
|
||||
outdir = path.join( outdir, packageInfo.name, (packageInfo.version || '') );
|
||||
}
|
||||
fs.mkPath(outdir);
|
||||
|
||||
// copy the template's static files to outdir
|
||||
fromDir = path.join(templatePath, 'static');
|
||||
staticFiles = fs.ls(fromDir, 3);
|
||||
|
||||
staticFiles.forEach(function(fileName) {
|
||||
var toDir = fs.toDir( fileName.replace(fromDir, outdir) );
|
||||
|
||||
fs.mkPath(toDir);
|
||||
fs.copyFileSync(fileName, toDir);
|
||||
});
|
||||
|
||||
// copy user-specified static files to outdir
|
||||
if (conf.default.staticFiles) {
|
||||
// The canonical property name is `include`. We accept `paths` for backwards compatibility
|
||||
// with a bug in JSDoc 3.2.x.
|
||||
staticFilePaths = conf.default.staticFiles.include ||
|
||||
conf.default.staticFiles.paths ||
|
||||
[];
|
||||
staticFileFilter = new (require('jsdoc/src/filter')).Filter(conf.default.staticFiles);
|
||||
staticFileScanner = new (require('jsdoc/src/scanner')).Scanner();
|
||||
|
||||
staticFilePaths.forEach(function(filePath) {
|
||||
var extraStaticFiles;
|
||||
|
||||
filePath = path.resolve(env.pwd, filePath);
|
||||
extraStaticFiles = staticFileScanner.scan([filePath], 10, staticFileFilter);
|
||||
|
||||
extraStaticFiles.forEach(function(fileName) {
|
||||
var sourcePath = fs.toDir(filePath);
|
||||
var toDir = fs.toDir( fileName.replace(sourcePath, outdir) );
|
||||
|
||||
fs.mkPath(toDir);
|
||||
fs.copyFileSync(fileName, toDir);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (sourceFilePaths.length) {
|
||||
sourceFiles = shortenPaths( sourceFiles, path.commonPrefix(sourceFilePaths) );
|
||||
}
|
||||
data().each(function(doclet) {
|
||||
var docletPath;
|
||||
var url = helper.createLink(doclet);
|
||||
|
||||
helper.registerLink(doclet.longname, url);
|
||||
|
||||
// add a shortened version of the full path
|
||||
if (doclet.meta) {
|
||||
docletPath = getPathFromDoclet(doclet);
|
||||
docletPath = sourceFiles[docletPath].shortened;
|
||||
if (docletPath) {
|
||||
doclet.meta.shortpath = docletPath;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
data().each(function(doclet) {
|
||||
var url = helper.longnameToUrl[doclet.longname];
|
||||
|
||||
if (url.indexOf('#') > -1) {
|
||||
doclet.id = helper.longnameToUrl[doclet.longname].split(/#/).pop();
|
||||
}
|
||||
else {
|
||||
doclet.id = doclet.name;
|
||||
}
|
||||
|
||||
if ( needsSignature(doclet) ) {
|
||||
addSignatureParams(doclet);
|
||||
addSignatureReturns(doclet);
|
||||
addAttribs(doclet);
|
||||
}
|
||||
});
|
||||
|
||||
// do this after the urls have all been generated
|
||||
data().each(function(doclet) {
|
||||
doclet.ancestors = getAncestorLinks(doclet);
|
||||
|
||||
if (doclet.kind === 'member') {
|
||||
addSignatureTypes(doclet);
|
||||
addAttribs(doclet);
|
||||
}
|
||||
|
||||
if (doclet.kind === 'constant') {
|
||||
addSignatureTypes(doclet);
|
||||
addAttribs(doclet);
|
||||
doclet.kind = 'member';
|
||||
}
|
||||
});
|
||||
|
||||
members = helper.getMembers(data);
|
||||
members.tutorials = tutorials.children;
|
||||
|
||||
// output pretty-printed source files by default
|
||||
outputSourceFiles = conf.default && conf.default.outputSourceFiles !== false;
|
||||
|
||||
// add template helpers
|
||||
view.find = find;
|
||||
view.linkto = linkto;
|
||||
view.resolveAuthorLinks = resolveAuthorLinks;
|
||||
view.tutoriallink = tutoriallink;
|
||||
view.htmlsafe = htmlsafe;
|
||||
view.outputSourceFiles = outputSourceFiles;
|
||||
|
||||
// once for all
|
||||
view.nav = buildNav(members);
|
||||
attachModuleSymbols( find({ longname: {left: 'module:'} }), members.modules );
|
||||
|
||||
// generate the pretty-printed source files first so other pages can link to them
|
||||
if (outputSourceFiles) {
|
||||
generateSourceFiles(sourceFiles, opts.encoding);
|
||||
}
|
||||
|
||||
if (members.globals.length) { generate('Global', [{kind: 'globalobj'}], globalUrl); }
|
||||
|
||||
// index page displays information from package.json and lists files
|
||||
files = find({kind: 'file'});
|
||||
packages = find({kind: 'package'});
|
||||
|
||||
generate('Home',
|
||||
packages.concat(
|
||||
[{
|
||||
kind: 'mainpage',
|
||||
readme: opts.readme,
|
||||
longname: (opts.mainpagetitle) ? opts.mainpagetitle : 'Main Page'
|
||||
}]
|
||||
).concat(files), indexUrl);
|
||||
|
||||
// set up the lists that we'll use to generate pages
|
||||
classes = taffy(members.classes);
|
||||
modules = taffy(members.modules);
|
||||
namespaces = taffy(members.namespaces);
|
||||
mixins = taffy(members.mixins);
|
||||
externals = taffy(members.externals);
|
||||
interfaces = taffy(members.interfaces);
|
||||
|
||||
Object.keys(helper.longnameToUrl).forEach(function(longname) {
|
||||
var myClasses = helper.find(classes, {longname: longname});
|
||||
var myExternals = helper.find(externals, {longname: longname});
|
||||
var myInterfaces = helper.find(interfaces, {longname: longname});
|
||||
var myMixins = helper.find(mixins, {longname: longname});
|
||||
var myModules = helper.find(modules, {longname: longname});
|
||||
var myNamespaces = helper.find(namespaces, {longname: longname});
|
||||
|
||||
if (myModules.length) {
|
||||
generate('Module: ' + myModules[0].name, myModules, helper.longnameToUrl[longname]);
|
||||
}
|
||||
|
||||
if (myClasses.length) {
|
||||
generate('Class: ' + myClasses[0].name, myClasses, helper.longnameToUrl[longname]);
|
||||
}
|
||||
|
||||
if (myNamespaces.length) {
|
||||
generate('Namespace: ' + myNamespaces[0].name, myNamespaces, helper.longnameToUrl[longname]);
|
||||
}
|
||||
|
||||
if (myMixins.length) {
|
||||
generate('Mixin: ' + myMixins[0].name, myMixins, helper.longnameToUrl[longname]);
|
||||
}
|
||||
|
||||
if (myExternals.length) {
|
||||
generate('External: ' + myExternals[0].name, myExternals, helper.longnameToUrl[longname]);
|
||||
}
|
||||
|
||||
if (myInterfaces.length) {
|
||||
generate('Interface: ' + myInterfaces[0].name, myInterfaces, helper.longnameToUrl[longname]);
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: move the tutorial functions to templateHelper.js
|
||||
function generateTutorial(title, tutorial, filename) {
|
||||
var tutorialData = {
|
||||
title: title,
|
||||
header: tutorial.title,
|
||||
content: tutorial.parse(),
|
||||
children: tutorial.children
|
||||
};
|
||||
var tutorialPath = path.join(outdir, filename);
|
||||
var html = view.render('tutorial.tmpl', tutorialData);
|
||||
|
||||
// yes, you can use {@link} in tutorials too!
|
||||
html = helper.resolveLinks(html); // turn {@link foo} into <a href="foodoc.html">foo</a>
|
||||
|
||||
fs.writeFileSync(tutorialPath, html, 'utf8');
|
||||
}
|
||||
|
||||
// tutorials can have only one parent so there is no risk for loops
|
||||
function saveChildren(node) {
|
||||
node.children.forEach(function(child) {
|
||||
generateTutorial('Tutorial: ' + child.title, child, helper.tutorialToUrl(child.name));
|
||||
saveChildren(child);
|
||||
});
|
||||
}
|
||||
|
||||
saveChildren(tutorials);
|
||||
};
|
1830
dd-sso/admin/src/admin/static/vendor/fancytree/bin/jsdoc3-moogle/static/fonts/OpenSans-Bold-webfont.svg
vendored
Normal file
After Width: | Height: | Size: 116 KiB |
After Width: | Height: | Size: 118 KiB |
After Width: | Height: | Size: 120 KiB |
1831
dd-sso/admin/src/admin/static/vendor/fancytree/bin/jsdoc3-moogle/static/fonts/OpenSans-Light-webfont.svg
vendored
Normal file
After Width: | Height: | Size: 114 KiB |