diff --git a/.bowerrc b/.bowerrc new file mode 100644 index 00000000..cbc6fdd1 --- /dev/null +++ b/.bowerrc @@ -0,0 +1,3 @@ +{ + "directory": "js/vendor" +} diff --git a/.gitignore b/.gitignore index 780d7f59..be1bfbf1 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,9 @@ nbproject # test files clover.xml +node_modules +lib/composer +js/vendor +tools/* +js/templates/*.handlebars.js +tests/unit/results diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 00000000..0b055afd --- /dev/null +++ b/.jshintrc @@ -0,0 +1,34 @@ +{ + "camelcase": true, + "eqeqeq": true, + "immed": true, + "latedef": false, + "noarg": true, + "nonbsp": true, + "undef": true, + "unused": true, + "trailing": true, + "maxparams": 5, + "curly": true, + "jquery": true, + "maxlen": 120, + "indent": 4, + "browser": true, + "laxbreak": true, + "globals": { + "console": true, + "it": true, + "xit": true, + "expect": true, + "describe": true, + "beforeEach": true, + "afterEach": true, + "sinon": true, + "fakeServer": true, + "_": true, + "OC": true, + "OCA": true, + "t": true, + "n": true + } +} diff --git a/.travis.yml b/.travis.yml index 6fdfeb18..44bc8498 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,14 @@ php: - 7.0 - 7.1 +cache: + directories: + - $HOME/.cache/bower + - $HOME/.composer/cache + - $HOME/.npm + - /tmp/phantomjs + - tools + env: global: - CORE_BRANCH=master @@ -27,38 +35,28 @@ before_install: script: - cd apps/$APP_NAME/ - - # Run integration tests - - sh -c "if [ '$INTEGRATION' = '1' ]; then cd tests/integration; ./run.sh; fi" - - # Test the app - - sh -c "if [ '$JSTESTS' != '1' -a '$CODECHECK' = '1' -a '$INTEGRATION' != '1' ]; then make test-codecheck; fi" - - sh -c "if [ '$JSTESTS' != '1' -a '$CODECHECK' = '1' -a '$INTEGRATION' != '1' ]; then make test-codecheck; fi" - - sh -c "if [ '$JSTESTS' != '1' -a '$CODECHECK' = '2' -a '$INTEGRATION' != '1' ]; then make test-codecheck-deprecations; fi" - - # Run phpunit tests - - sh -c "if [ '$JSTESTS' != '1' -a '$CODECHECK' != '1' -a '$CODECHECK' != '2' -a '$INTEGRATION' != '1' ]; then make test-php; fi" - - # Create and upload coverage report - - sh -c "if [ '$JSTESTS' != '1' -a '$CODECHECK' != '1' -a '$CODECHECK' != '2' -a '$INTEGRATION' != '1' ]; then make test-upload-coverage; fi" - - # Run Javascript unit tests - - make test-js + - make $TEST matrix: include: + # PHP unit tests + code coverage - php: 5.6 - env: DB=mysql + env: DB=mysql;TEST=test-upload-coverage + # PHP unit tests - php: 5.6 - env: DB=pgsql + env: DB=pgsql;TEST=test-php + # Code checker / syntax - php: 5.6 - env: DB=mysql;CODECHECK=1 + env: DB=mysql;TEST=test-codecheck + # Code checker (deprecations only, allowed to fail) - php: 5.6 - env: DB=mysql;CODECHECK=2 + env: DB=mysql;TEST=test-codecheck-deprecations + # Javascript tests - php: 5.6 - env: DB=mysql;JSTESTS=1 + env: DB=mysql;TEST=test-js + # Integration tests - php: 7.0 - env: DB=mysql;INTEGRATION=1 + env: DB=mysql;TEST=test-integration allow_failures: - - env: DB=mysql;CODECHECK=2 + - env: DB=mysql;TEST=test-codecheck-deprecations fast_finish: true diff --git a/Makefile b/Makefile index 3b8a14ce..fc2f328c 100644 --- a/Makefile +++ b/Makefile @@ -2,73 +2,54 @@ OWNCLOUD_PATH=$(CURDIR)/../.. OCC=$(OWNCLOUD_PATH)/occ -PHPUNIT=$(OWNCLOUD_PATH)/lib/composer/phpunit/phpunit/phpunit app_name=customgroups -project_dir=$(CURDIR)/../$(app_name) +app_namespace=CustomGroups + build_dir=$(CURDIR)/build + +# these can be extended by included files +# to add for example generated files doc_files=README.md src_files= -src_dirs=appinfo lib +src_dirs=appinfo lib l10n js css templates all_src=$(src_files) $(src_dirs) $(doc_files) -market_dir=$(build_dir)/market - -.PHONY: all -all: market - -.PHONE: market -market: dist - cd $(build_dir); tar cvzf $(app_name).tar.gz $(app_name) - rm -Rf $(market_dir); mkdir -p $(market_dir) - mv $(build_dir)/$(app_name).tar.gz $(market_dir) - -$(build_dir)/$(app_name): - rm -Rf $@; mkdir -p $@ - cp -R $(all_src) $@ -.PHONY: dist -dist: $(build_dir)/$(app_name) +# rules to be extended by included files +test_rules= +clean_rules= +js_rules= +help_rules=help-base -.PHONY: distclean -distclean: clean +tools_path=$(shell pwd)/tools -.PHONY: clean -clean: clean-test - rm -rf $(build_dir) - -.PHONY: clean-test -clean-test: - rm tests/unit/clover.xml - rm tests/unit/*.phar - -.PHONY: test-syntax -test-syntax: - for F in $(shell find . -name \*.php); do \ - php -l "$$F" || exit $?; \ - done +.PHONY: all +# build tarball +all: help-hint $(build_dir)/$(app_name).tar.gz -.PHONY: test-codecheck -test-codecheck: test-syntax - $(OCC) app:check-code $(app_name) -c private -c strong-comparison +include rules/deps.mk +include rules/dist.mk +include rules/tests.mk +include rules/frontend.mk -.PHONY: test-codecheck-deprecations -test-codecheck-deprecations: - $(OCC) app:check-code $(app_name) -c deprecation +.PHONY: help-base +help-base: + @echo "Please use 'make ' where is one of" + @echo -.PHONY: test-php -test-php: test-syntax - $(OCC) app:enable $(app_name) - cd tests/unit && $(PHPUNIT) --configuration phpunit.xml +.PHONY: help-hint +help-hint: + @echo "Building $(app_name) app" + @echo + @echo "Note: You can type 'make help' for more targets" + @echo -.PHONY: test-upload-coverage -test-upload-coverage: - cd tests/unit && wget https://scrutinizer-ci.com/ocular.phar; php ocular.phar code-coverage:upload --format=php-clover clover.xml +.PHONY: help +help: $(help_rules) -.PHONY: test-js -test-js: - @echo No JS unit tests currently - #cd tests/js && npm install --deps; node_modules/karma/bin/karma start karma.config.js --single-run; +.PHONY: clean +clean: $(clean_rules) .PHONY: test -test: test-codecheck test-codecheck-deprecations test-php test-js +test: $(test_rules) diff --git a/README.md b/README.md index c6b2ea44..e78eca21 100644 --- a/README.md +++ b/README.md @@ -9,19 +9,18 @@ It is then possible to share files or folders with these groups. [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/owncloud/customgroups/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/owncloud/customgroups/?branch=master) [![Code Coverage](https://scrutinizer-ci.com/g/owncloud/customgroups/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/owncloud/customgroups/?branch=master) -## QA metrics on stable8 branch: +## Requirements -[![Build Status](https://travis-ci.org/owncloud/customgroups.svg?branch=stable8)](https://travis-ci.org/owncloud/customgroups/branches) -[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/owncloud/customgroups/badges/quality-score.png?b=stable8)](https://scrutinizer-ci.com/g/owncloud/customgroups/?branch=stable8) -[![Code Coverage](https://scrutinizer-ci.com/g/owncloud/customgroups/badges/coverage.png?b=stable8)](https://scrutinizer-ci.com/g/owncloud/customgroups/?branch=stable8) +* ownCloud 10.0 -## Requirements +## Building the app -* ownCloud 10 +* Make sure you have [Node JS](https://nodejs.org/) installed +* Run `make` and find the tarball in the "build" directory ## Install -* Install and enable the App +* Extract the resulting tarball in the "apps" folder in ownCloud ## Usage @@ -30,6 +29,10 @@ It is then possible to share files or folders with these groups. * Create custom group and add other users as members * Share file/folder with said groups +## Developing + +* Run `make help` to get information about the different targets. + ## Authors: [Vincent Petry](https://github.com/PVince81/) :: PVince81 at owncloud dot com diff --git a/appinfo/info.xml b/appinfo/info.xml index b27c79f2..6226491b 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -1,7 +1,7 @@ customgroups - User-defined custom groups + Custom groups CustomGroups Let users create and manage custom groups for sharing. https://github.com/owncloud/customgroups/ @@ -26,4 +26,7 @@ OCA\CustomGroups\Dav\RootCollection + + OCA\CustomGroups\SettingsPanel + diff --git a/appinfo/routes.php b/appinfo/routes.php new file mode 100644 index 00000000..fbf85035 --- /dev/null +++ b/appinfo/routes.php @@ -0,0 +1,27 @@ + + * + * @copyright Copyright (c) 2016, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +return [ + 'routes' => [ + ['name' => 'page#index', 'url' => '/', 'verb' => 'GET'], + ], + 'resources' => [] +]; diff --git a/bower.json b/bower.json new file mode 100644 index 00000000..0629c71a --- /dev/null +++ b/bower.json @@ -0,0 +1,11 @@ +{ + "name": "owncloud-customgroups", + "version": "0.1", + "homepage": "https://github.com/owncloud/customgroups/", + "license": "AGPL", + "private": true, + "ignore": [], + "dependencies": { + "handlebars": "4.0.5" + } +} diff --git a/composer.json b/composer.json new file mode 100644 index 00000000..192d06c4 --- /dev/null +++ b/composer.json @@ -0,0 +1,14 @@ +{ + "config" : { + "vendor-dir": "lib/composer", + "optimize-autoloader": true, + "classmap-authoritative": false + }, + "require-dev": { + "phpunit/phpunit": "^5.5", + "scrutinizer/ocular": "^1.3.1" + }, + "require": { + "php": ">=5.6" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 00000000..07cd9fc3 --- /dev/null +++ b/composer.lock @@ -0,0 +1,2174 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" + ], + "hash": "e4cb7b9f86e01f05aeb72228638a82e8", + "content-hash": "7cab391e7ac4cea2012c6c18d6a58d84", + "packages": [], + "packages-dev": [ + { + "name": "doctrine/annotations", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/annotations.git", + "reference": "30e07cf03edc3cd3ef579d0dd4dd8c58250799a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/30e07cf03edc3cd3ef579d0dd4dd8c58250799a5", + "reference": "30e07cf03edc3cd3ef579d0dd4dd8c58250799a5", + "shasum": "" + }, + "require": { + "doctrine/lexer": "1.*", + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "doctrine/cache": "1.*", + "phpunit/phpunit": "^5.6.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Docblock Annotations Parser", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "annotations", + "docblock", + "parser" + ], + "time": "2016-10-24 11:45:47" + }, + { + "name": "doctrine/instantiator", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", + "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", + "shasum": "" + }, + "require": { + "php": ">=5.3,<8.0-DEV" + }, + "require-dev": { + "athletic/athletic": "~0.1.8", + "ext-pdo": "*", + "ext-phar": "*", + "phpunit/phpunit": "~4.0", + "squizlabs/php_codesniffer": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "http://ocramius.github.com/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://github.com/doctrine/instantiator", + "keywords": [ + "constructor", + "instantiate" + ], + "time": "2015-06-14 21:17:01" + }, + { + "name": "doctrine/lexer", + "version": "v1.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "83893c552fd2045dd78aef794c31e694c37c0b8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/83893c552fd2045dd78aef794c31e694c37c0b8c", + "reference": "83893c552fd2045dd78aef794c31e694c37c0b8c", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "Doctrine\\Common\\Lexer\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Base library for a lexer that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "lexer", + "parser" + ], + "time": "2014-09-09 13:34:57" + }, + { + "name": "guzzlehttp/guzzle", + "version": "v3.8.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "4de0618a01b34aa1c8c33a3f13f396dcd3882eba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/4de0618a01b34aa1c8c33a3f13f396dcd3882eba", + "reference": "4de0618a01b34aa1c8c33a3f13f396dcd3882eba", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "php": ">=5.3.3", + "symfony/event-dispatcher": ">=2.1" + }, + "replace": { + "guzzle/batch": "self.version", + "guzzle/cache": "self.version", + "guzzle/common": "self.version", + "guzzle/http": "self.version", + "guzzle/inflection": "self.version", + "guzzle/iterator": "self.version", + "guzzle/log": "self.version", + "guzzle/parser": "self.version", + "guzzle/plugin": "self.version", + "guzzle/plugin-async": "self.version", + "guzzle/plugin-backoff": "self.version", + "guzzle/plugin-cache": "self.version", + "guzzle/plugin-cookie": "self.version", + "guzzle/plugin-curlauth": "self.version", + "guzzle/plugin-error-response": "self.version", + "guzzle/plugin-history": "self.version", + "guzzle/plugin-log": "self.version", + "guzzle/plugin-md5": "self.version", + "guzzle/plugin-mock": "self.version", + "guzzle/plugin-oauth": "self.version", + "guzzle/service": "self.version", + "guzzle/stream": "self.version" + }, + "require-dev": { + "doctrine/cache": "*", + "monolog/monolog": "1.*", + "phpunit/phpunit": "3.7.*", + "psr/log": "1.0.*", + "symfony/class-loader": "*", + "zendframework/zend-cache": "<2.3", + "zendframework/zend-log": "<2.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.8-dev" + } + }, + "autoload": { + "psr-0": { + "Guzzle": "src/", + "Guzzle\\Tests": "tests/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Guzzle Community", + "homepage": "https://github.com/guzzle/guzzle/contributors" + } + ], + "description": "Guzzle is a PHP HTTP client library and framework for building RESTful web service clients", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "rest", + "web service" + ], + "time": "2014-01-28 22:29:15" + }, + { + "name": "jms/metadata", + "version": "1.6.0", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/metadata.git", + "reference": "6a06970a10e0a532fb52d3959547123b84a3b3ab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/metadata/zipball/6a06970a10e0a532fb52d3959547123b84a3b3ab", + "reference": "6a06970a10e0a532fb52d3959547123b84a3b3ab", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "doctrine/cache": "~1.0", + "symfony/cache": "~3.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.5.x-dev" + } + }, + "autoload": { + "psr-0": { + "Metadata\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Class/method/property metadata management in PHP", + "keywords": [ + "annotations", + "metadata", + "xml", + "yaml" + ], + "time": "2016-12-05 10:18:33" + }, + { + "name": "jms/parser-lib", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/parser-lib.git", + "reference": "c509473bc1b4866415627af0e1c6cc8ac97fa51d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/parser-lib/zipball/c509473bc1b4866415627af0e1c6cc8ac97fa51d", + "reference": "c509473bc1b4866415627af0e1c6cc8ac97fa51d", + "shasum": "" + }, + "require": { + "phpoption/phpoption": ">=0.9,<2.0-dev" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-0": { + "JMS\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache2" + ], + "description": "A library for easily creating recursive-descent parsers.", + "time": "2012-11-18 18:08:43" + }, + { + "name": "jms/serializer", + "version": "1.4.2", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/serializer.git", + "reference": "f39d8b4660d5cef43b0c3265ce642173d9b2c58b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/serializer/zipball/f39d8b4660d5cef43b0c3265ce642173d9b2c58b", + "reference": "f39d8b4660d5cef43b0c3265ce642173d9b2c58b", + "shasum": "" + }, + "require": { + "doctrine/annotations": "^1.0", + "doctrine/instantiator": "^1.0.3", + "jms/metadata": "~1.1", + "jms/parser-lib": "1.*", + "php": ">=5.5.0", + "phpcollection/phpcollection": "~0.1", + "phpoption/phpoption": "^1.1" + }, + "conflict": { + "twig/twig": "<1.12" + }, + "require-dev": { + "doctrine/orm": "~2.1", + "doctrine/phpcr-odm": "^1.3|^2.0", + "ext-pdo_sqlite": "*", + "jackalope/jackalope-doctrine-dbal": "^1.1.5", + "phpunit/phpunit": "^4.8|^5.0", + "propel/propel1": "~1.7", + "symfony/filesystem": "^2.1", + "symfony/form": "~2.1", + "symfony/translation": "^2.1", + "symfony/validator": "^2.2", + "symfony/yaml": "^2.1", + "twig/twig": "~1.12|~2.0" + }, + "suggest": { + "symfony/yaml": "Required if you'd like to serialize data to YAML format." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-0": { + "JMS\\Serializer": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache2" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Library for (de-)serializing data of any complexity; supports XML, JSON, and YAML.", + "homepage": "http://jmsyst.com/libs/serializer", + "keywords": [ + "deserialization", + "jaxb", + "json", + "serialization", + "xml" + ], + "time": "2016-11-13 10:20:11" + }, + { + "name": "myclabs/deep-copy", + "version": "1.5.5", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "399c1f9781e222f6eb6cc238796f5200d1b7f108" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/399c1f9781e222f6eb6cc238796f5200d1b7f108", + "reference": "399c1f9781e222f6eb6cc238796f5200d1b7f108", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "doctrine/collections": "1.*", + "phpunit/phpunit": "~4.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "homepage": "https://github.com/myclabs/DeepCopy", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "time": "2016-10-31 17:19:45" + }, + { + "name": "phpcollection/phpcollection", + "version": "0.5.0", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-collection.git", + "reference": "f2bcff45c0da7c27991bbc1f90f47c4b7fb434a6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-collection/zipball/f2bcff45c0da7c27991bbc1f90f47c4b7fb434a6", + "reference": "f2bcff45c0da7c27991bbc1f90f47c4b7fb434a6", + "shasum": "" + }, + "require": { + "phpoption/phpoption": "1.*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.4-dev" + } + }, + "autoload": { + "psr-0": { + "PhpCollection": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache2" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "General-Purpose Collection Library for PHP", + "keywords": [ + "collection", + "list", + "map", + "sequence", + "set" + ], + "time": "2015-05-17 12:39:23" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "1.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/144c307535e82c8fdcaacbcfc1d6d8eeb896687c", + "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "^4.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "time": "2015-12-27 11:43:31" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "8331b5efe816ae05461b7ca1e721c01b46bafb3e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/8331b5efe816ae05461b7ca1e721c01b46bafb3e", + "reference": "8331b5efe816ae05461b7ca1e721c01b46bafb3e", + "shasum": "" + }, + "require": { + "php": ">=5.5", + "phpdocumentor/reflection-common": "^1.0@dev", + "phpdocumentor/type-resolver": "^0.2.0", + "webmozart/assert": "^1.0" + }, + "require-dev": { + "mockery/mockery": "^0.9.4", + "phpunit/phpunit": "^4.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "time": "2016-09-30 07:12:33" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "0.2.1", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "e224fb2ea2fba6d3ad6fdaef91cd09a172155ccb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/e224fb2ea2fba6d3ad6fdaef91cd09a172155ccb", + "reference": "e224fb2ea2fba6d3ad6fdaef91cd09a172155ccb", + "shasum": "" + }, + "require": { + "php": ">=5.5", + "phpdocumentor/reflection-common": "^1.0" + }, + "require-dev": { + "mockery/mockery": "^0.9.4", + "phpunit/phpunit": "^5.2||^4.8.24" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "time": "2016-11-25 06:54:22" + }, + { + "name": "phpoption/phpoption", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "94e644f7d2051a5f0fcf77d81605f152eecff0ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/94e644f7d2051a5f0fcf77d81605f152eecff0ed", + "reference": "94e644f7d2051a5f0fcf77d81605f152eecff0ed", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "4.7.*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-0": { + "PhpOption\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache2" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "time": "2015-07-25 16:39:46" + }, + { + "name": "phpspec/prophecy", + "version": "v1.6.2", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "6c52c2722f8460122f96f86346600e1077ce22cb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/6c52c2722f8460122f96f86346600e1077ce22cb", + "reference": "6c52c2722f8460122f96f86346600e1077ce22cb", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.2", + "php": "^5.3|^7.0", + "phpdocumentor/reflection-docblock": "^2.0|^3.0.2", + "sebastian/comparator": "^1.1", + "sebastian/recursion-context": "^1.0|^2.0" + }, + "require-dev": { + "phpspec/phpspec": "^2.0", + "phpunit/phpunit": "^4.8 || ^5.6.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.6.x-dev" + } + }, + "autoload": { + "psr-0": { + "Prophecy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "fake", + "mock", + "spy", + "stub" + ], + "time": "2016-11-21 14:58:47" + }, + { + "name": "phpunit/php-code-coverage", + "version": "4.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "903fd6318d0a90b4770a009ff73e4a4e9c437929" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/903fd6318d0a90b4770a009ff73e4a4e9c437929", + "reference": "903fd6318d0a90b4770a009ff73e4a4e9c437929", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0", + "phpunit/php-file-iterator": "~1.3", + "phpunit/php-text-template": "~1.2", + "phpunit/php-token-stream": "^1.4.2", + "sebastian/code-unit-reverse-lookup": "~1.0", + "sebastian/environment": "^1.3.2 || ^2.0", + "sebastian/version": "~1.0|~2.0" + }, + "require-dev": { + "ext-xdebug": ">=2.1.4", + "phpunit/phpunit": "^5.4" + }, + "suggest": { + "ext-dom": "*", + "ext-xdebug": ">=2.4.0", + "ext-xmlwriter": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "time": "2016-11-28 16:00:31" + }, + { + "name": "phpunit/php-file-iterator", + "version": "1.4.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3cc8f69b3028d0f96a9078e6295d86e9bf019be5", + "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "time": "2016-10-03 07:40:28" + }, + { + "name": "phpunit/php-text-template", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "time": "2015-06-21 13:50:34" + }, + { + "name": "phpunit/php-timer", + "version": "1.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/38e9124049cf1a164f1e4537caf19c99bf1eb260", + "reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4|~5" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "time": "2016-05-12 18:03:57" + }, + { + "name": "phpunit/php-token-stream", + "version": "1.4.9", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-token-stream.git", + "reference": "3b402f65a4cc90abf6e1104e388b896ce209631b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/3b402f65a4cc90abf6e1104e388b896ce209631b", + "reference": "3b402f65a4cc90abf6e1104e388b896ce209631b", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Wrapper around PHP's tokenizer extension.", + "homepage": "https://github.com/sebastianbergmann/php-token-stream/", + "keywords": [ + "tokenizer" + ], + "time": "2016-11-15 14:06:22" + }, + { + "name": "phpunit/phpunit", + "version": "5.7.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "af91da3f2671006ff5d0628023de3b7ac4d1ef09" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/af91da3f2671006ff5d0628023de3b7ac4d1ef09", + "reference": "af91da3f2671006ff5d0628023de3b7ac4d1ef09", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "myclabs/deep-copy": "~1.3", + "php": "^5.6 || ^7.0", + "phpspec/prophecy": "^1.6.2", + "phpunit/php-code-coverage": "^4.0.3", + "phpunit/php-file-iterator": "~1.4", + "phpunit/php-text-template": "~1.2", + "phpunit/php-timer": "^1.0.6", + "phpunit/phpunit-mock-objects": "^3.2", + "sebastian/comparator": "~1.2.2", + "sebastian/diff": "~1.2", + "sebastian/environment": "^1.3.4 || ^2.0", + "sebastian/exporter": "~2.0", + "sebastian/global-state": "^1.0 || ^2.0", + "sebastian/object-enumerator": "~2.0", + "sebastian/resource-operations": "~1.0", + "sebastian/version": "~1.0|~2.0", + "symfony/yaml": "~2.1|~3.0" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "3.0.2" + }, + "require-dev": { + "ext-pdo": "*" + }, + "suggest": { + "ext-xdebug": "*", + "phpunit/php-invoker": "~1.1" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.7.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "time": "2016-12-13 16:19:44" + }, + { + "name": "phpunit/phpunit-mock-objects", + "version": "3.4.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", + "reference": "3ab72b65b39b491e0c011e2e09bb2206c2aa8e24" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/3ab72b65b39b491e0c011e2e09bb2206c2aa8e24", + "reference": "3ab72b65b39b491e0c011e2e09bb2206c2aa8e24", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.2", + "php": "^5.6 || ^7.0", + "phpunit/php-text-template": "^1.2", + "sebastian/exporter": "^1.2 || ^2.0" + }, + "conflict": { + "phpunit/phpunit": "<5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.4" + }, + "suggest": { + "ext-soap": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Mock Object library for PHPUnit", + "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", + "keywords": [ + "mock", + "xunit" + ], + "time": "2016-12-08 20:27:08" + }, + { + "name": "psr/log", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", + "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "time": "2016-10-10 12:19:37" + }, + { + "name": "scrutinizer/ocular", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/scrutinizer-ci/ocular.git", + "reference": "b87630fa5a872283319a84133bb610163fe509b1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/scrutinizer-ci/ocular/zipball/b87630fa5a872283319a84133bb610163fe509b1", + "reference": "b87630fa5a872283319a84133bb610163fe509b1", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "~3.0", + "jms/serializer": "^1.0.0", + "phpoption/phpoption": "~1.0", + "symfony/console": "^2.0.5|~3.0", + "symfony/process": "~2.3|~3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.0", + "symfony/filesystem": "~2.0|~3.0" + }, + "bin": [ + "bin/ocular" + ], + "type": "library", + "autoload": { + "psr-4": { + "Scrutinizer\\Ocular\\": "src/Scrutinizer/Ocular" + } + }, + "notification-url": "https://packagist.org/downloads/", + "time": "2016-09-09 08:07:18" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "c36f5e7cfce482fde5bf8d10d41a53591e0198fe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/c36f5e7cfce482fde5bf8d10d41a53591e0198fe", + "reference": "c36f5e7cfce482fde5bf8d10d41a53591e0198fe", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "phpunit/phpunit": "~5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "time": "2016-02-13 06:45:14" + }, + { + "name": "sebastian/comparator", + "version": "1.2.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "6a1ed12e8b2409076ab22e3897126211ff8b1f7f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/6a1ed12e8b2409076ab22e3897126211ff8b1f7f", + "reference": "6a1ed12e8b2409076ab22e3897126211ff8b1f7f", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "sebastian/diff": "~1.2", + "sebastian/exporter": "~1.2 || ~2.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "http://www.github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "time": "2016-11-19 09:18:40" + }, + { + "name": "sebastian/diff", + "version": "1.4.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/13edfd8706462032c2f52b4b862974dd46b71c9e", + "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff" + ], + "time": "2015-12-08 07:14:41" + }, + { + "name": "sebastian/environment", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/5795ffe5dc5b02460c3e34222fee8cbe245d8fac", + "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "time": "2016-11-26 07:53:53" + }, + { + "name": "sebastian/exporter", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4", + "reference": "ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "sebastian/recursion-context": "~2.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "http://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "time": "2016-11-19 08:54:04" + }, + { + "name": "sebastian/global-state", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4", + "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.2" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "time": "2015-10-12 03:26:01" + }, + { + "name": "sebastian/object-enumerator", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "96f8a3f257b69e8128ad74d3a7fd464bcbaa3b35" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/96f8a3f257b69e8128ad74d3a7fd464bcbaa3b35", + "reference": "96f8a3f257b69e8128ad74d3a7fd464bcbaa3b35", + "shasum": "" + }, + "require": { + "php": ">=5.6", + "sebastian/recursion-context": "~2.0" + }, + "require-dev": { + "phpunit/phpunit": "~5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "time": "2016-11-19 07:35:10" + }, + { + "name": "sebastian/recursion-context", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "2c3ba150cbec723aa057506e73a8d33bdb286c9a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/2c3ba150cbec723aa057506e73a8d33bdb286c9a", + "reference": "2c3ba150cbec723aa057506e73a8d33bdb286c9a", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "time": "2016-11-19 07:33:16" + }, + { + "name": "sebastian/resource-operations", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", + "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", + "shasum": "" + }, + "require": { + "php": ">=5.6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "time": "2015-07-28 20:34:47" + }, + { + "name": "sebastian/version", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "time": "2016-10-03 07:35:21" + }, + { + "name": "symfony/console", + "version": "v3.2.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "d12aa9ca20f4db83ec58410978dab6afcb9d6aaa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/d12aa9ca20f4db83ec58410978dab6afcb9d6aaa", + "reference": "d12aa9ca20f4db83ec58410978dab6afcb9d6aaa", + "shasum": "" + }, + "require": { + "php": ">=5.5.9", + "symfony/debug": "~2.8|~3.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/event-dispatcher": "~2.8|~3.0", + "symfony/filesystem": "~2.8|~3.0", + "symfony/process": "~2.8|~3.0" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/filesystem": "", + "symfony/process": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Console Component", + "homepage": "https://symfony.com", + "time": "2016-12-11 14:34:22" + }, + { + "name": "symfony/debug", + "version": "v3.2.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/debug.git", + "reference": "9f923e68d524a3095c5a2ae5fc7220c7cbc12231" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/debug/zipball/9f923e68d524a3095c5a2ae5fc7220c7cbc12231", + "reference": "9f923e68d524a3095c5a2ae5fc7220c7cbc12231", + "shasum": "" + }, + "require": { + "php": ">=5.5.9", + "psr/log": "~1.0" + }, + "conflict": { + "symfony/http-kernel": ">=2.3,<2.3.24|~2.4.0|>=2.5,<2.5.9|>=2.6,<2.6.2" + }, + "require-dev": { + "symfony/class-loader": "~2.8|~3.0", + "symfony/http-kernel": "~2.8|~3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Debug\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Debug Component", + "homepage": "https://symfony.com", + "time": "2016-11-16 22:18:16" + }, + { + "name": "symfony/event-dispatcher", + "version": "v3.2.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "e8f47a327c2f0fd5aa04fa60af2b693006ed7283" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/e8f47a327c2f0fd5aa04fa60af2b693006ed7283", + "reference": "e8f47a327c2f0fd5aa04fa60af2b693006ed7283", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "~2.8|~3.0", + "symfony/dependency-injection": "~2.8|~3.0", + "symfony/expression-language": "~2.8|~3.0", + "symfony/stopwatch": "~2.8|~3.0" + }, + "suggest": { + "symfony/dependency-injection": "", + "symfony/http-kernel": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony EventDispatcher Component", + "homepage": "https://symfony.com", + "time": "2016-10-13 06:29:04" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "e79d363049d1c2128f133a2667e4f4190904f7f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/e79d363049d1c2128f133a2667e4f4190904f7f4", + "reference": "e79d363049d1c2128f133a2667e4f4190904f7f4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "time": "2016-11-14 01:06:16" + }, + { + "name": "symfony/process", + "version": "v3.2.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "02ea84847aad71be7e32056408bb19f3a616cdd3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/02ea84847aad71be7e32056408bb19f3a616cdd3", + "reference": "02ea84847aad71be7e32056408bb19f3a616cdd3", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Process Component", + "homepage": "https://symfony.com", + "time": "2016-11-24 10:40:28" + }, + { + "name": "symfony/yaml", + "version": "v3.2.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "a7095af4b97a0955f85c8989106c249fa649011f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/a7095af4b97a0955f85c8989106c249fa649011f", + "reference": "a7095af4b97a0955f85c8989106c249fa649011f", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "require-dev": { + "symfony/console": "~2.8|~3.0" + }, + "suggest": { + "symfony/console": "For validating YAML files using the lint command" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Yaml Component", + "homepage": "https://symfony.com", + "time": "2016-12-10 10:07:06" + }, + { + "name": "webmozart/assert", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/webmozart/assert.git", + "reference": "2db61e59ff05fe5126d152bd0655c9ea113e550f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozart/assert/zipball/2db61e59ff05fe5126d152bd0655c9ea113e550f", + "reference": "2db61e59ff05fe5126d152bd0655c9ea113e550f", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.6", + "sebastian/version": "^1.0.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "time": "2016-11-23 20:04:58" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=5.6" + }, + "platform-dev": [] +} diff --git a/css/app.css b/css/app.css new file mode 100644 index 00000000..3175da66 --- /dev/null +++ b/css/app.css @@ -0,0 +1,56 @@ + + +/* FIXME: must be in core somewhere */ +#customgroups .icon { + display: inline-block; +} + +#customgroups .group-name-title a, +#customgroups .grid td a { + visibility: hidden; + vertical-align: middle; +} + +#customgroups .grid tr { + line-height: 50px; +} + +#customgroups .avatar-column { + width: 32px; + padding-right: 0; +} + +#customgroups .grid .select > * { + cursor: pointer; +} + +#customgroups .group-name-title:hover a, +#customgroups .grid tr:hover td a { + visibility: visible; +} + +#customgroups .sidebar { + padding: 15px; +} + +/* FIXME: integrate with core styles */ +#customgroups .sidebar .close { + position: absolute; + top: 0; + right: 0; + padding: 15px; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)"; + opacity: .5; +} + +#customgroups .avatar { + display: inline-block; + overflow: hidden; + vertical-align: middle; + width: 32px; + height: 32px; +} + +#customgroups .group-name-title .avatar { + margin-right: 8px; +} diff --git a/js/App.js b/js/App.js new file mode 100644 index 00000000..3a13abe7 --- /dev/null +++ b/js/App.js @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2016 Vincent Petry + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +(function(OCA) { + + var App = OC.Backbone.View.extend({ + events: { + 'click .sidebar .action-close': '_onClickClose' + }, + + initialize: function() { + this.collection = new OCA.CustomGroups.GroupsCollection([], { + // admins can see all groups so don't set a user filter + userId: (OC.isUserAdmin() ? null : OC.getCurrentUser().uid) + }); + this.listView = new OCA.CustomGroups.GroupsView(this.collection); + this.listView.on('select', this._onSelectGroup, this); + + this.render(); + + this.collection.fetch(); + }, + + template: function(data) { + return OCA.CustomGroups.Templates.app(data); + }, + + render: function() { + this.$el.html(this.template({ + customGroupsTitle: t('customgroups', 'Custom Groups') + })); + + this.$groupsContainer = this.$('.groups-container').removeClass('icon-loading'); + this.$membersContainer = this.$('.members-container').removeClass('icon-loading'); + + this.listView.render(); + this.$groupsContainer.append(this.listView.$el); + }, + + _onSelectGroup: function(group) { + this.$membersContainer.empty(); + if (this.membersView) { + this.membersView.remove(); + } + + if (group !== null) { + OC.Apps.showAppSidebar(this.$membersContainer); + this.membersView = new OCA.CustomGroups.MembersView(group); + this.$membersContainer.append(this.membersView.$el); + + this.membersView.render(); + } else { + OC.Apps.hideAppSidebar(this.$membersContainer); + } + }, + + _onClickClose: function(ev) { + ev.preventDefault(); + OC.Apps.hideAppSidebar(this.$membersContainer); + return false; + }, + + }); + + OCA.CustomGroups = _.extend({ + ROLE_MEMBER: 'member', + ROLE_ADMIN: 'admin' + }, OCA.CustomGroups); + + OCA.CustomGroups.App = App; + +})(OCA); + +$(document).ready(function() { + var app = new OCA.CustomGroups.App(); + $('#customgroups').append(app.$el); +}); diff --git a/js/GroupModel.js b/js/GroupModel.js new file mode 100644 index 00000000..699f3454 --- /dev/null +++ b/js/GroupModel.js @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2016 Vincent Petry + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +(function(OC, OCA) { + var NS = '{' + OC.Files.Client.NS_OWNCLOUD + '}'; + + /** + * @class OCA.CustomGroups.GroupModel + * @classdesc + * + */ + var GroupModel = OC.Backbone.WebdavCollectionNode.extend( + /** @lends OCA.CustomGroups.GroupModel.prototype */ { + + childrenCollectionClass: OCA.CustomGroups.MembersCollection, + + davProperties: { + 'displayName': NS + 'display-name', + 'role': NS + 'role' + }, + + initialize: function() { + var self = this; + // group is part of a GroupsCollection ? + if (this.collection) { + // get owner + var userId = this.collection.getUserId(); + if (userId) { + // detect removal of list owner + this.getChildrenCollection().on('remove', function(membershipModel) { + if (membershipModel.id === userId) { + // remove current group from the list + self.collection.remove(self); + } + }); + } + } + }, + + url: function() { + return OC.linkToRemote('dav') + '/customgroups/groups/' + encodeURIComponent(this.get('id')); + } + }); + + OCA.CustomGroups = _.extend({}, OCA.CustomGroups); + OCA.CustomGroups.GroupModel = GroupModel; + +})(OC, OCA); + diff --git a/js/GroupsCollection.js b/js/GroupsCollection.js new file mode 100644 index 00000000..2dda0439 --- /dev/null +++ b/js/GroupsCollection.js @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2016 Vincent Petry + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ +(function(OC, OCA) { + /** + * @class OCA.CustomGroups.GroupsCollection + * @classdesc + * + */ + var GroupsCollection = OC.Backbone.WebdavChildrenCollection.extend( + /** @lends OCA.CustomGroups.GroupsCollection.prototype */ { + + model: OCA.CustomGroups.GroupModel, + + _userId: null, + + initialize: function(models, options) { + options = options || {}; + + this._userId = options.userId; + }, + + getUserId: function() { + return this._userId; + }, + + url: function() { + if (!this._userId) { + return OC.linkToRemote('dav') + '/customgroups/groups/'; + } else { + return OC.linkToRemote('dav') + '/customgroups/users/' + encodeURIComponent(this._userId) + '/'; + } + } + }); + + OCA.CustomGroups = _.extend({}, OCA.CustomGroups); + OCA.CustomGroups.GroupsCollection = GroupsCollection; + +})(OC, OCA); + diff --git a/js/GroupsView.js b/js/GroupsView.js new file mode 100644 index 00000000..699d34f6 --- /dev/null +++ b/js/GroupsView.js @@ -0,0 +1,284 @@ +/* + * Copyright (c) 2016 Vincent Petry + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +(function(OC, OCA) { + + /** + * @class OCA.CustomGroups.GroupsView + * @classdesc + * + */ + var GroupsView = OC.Backbone.View.extend( + /** @lends OCA.CustomGroups.GroupsView.prototype */ { + sync: OC.Backbone.davSync, + + _lastActive: null, + + events: { + 'submit form': '_onSubmitCreationForm', + 'click .select': '_onSelect', + 'click .action-rename-group': '_onRenameGroup', + 'click .action-delete-group': '_onDeleteGroup' + }, + + initialize: function(collection) { + this.collection = collection; + + this.collection.on('request', this._onRequest, this); + this.collection.on('sync', this._onEndRequest, this); + this.collection.on('add', this._onAddModel, this); + this.collection.on('change', this._onChangeModel, this); + this.collection.on('remove', this._onRemoveGroup, this); + }, + + _toggleLoading: function(state) { + this._loading = state; + this.$('.loading').toggleClass('hidden', !state); + }, + + _findGroupModelForEvent: function(ev) { + var $el = $(ev.target).closest('.group'); + var id = $el.attr('data-id'); + return this.collection.findWhere({'id': id}); + }, + + _onRenameGroup: function(ev) { + ev.preventDefault(); + + var model = this._findGroupModelForEvent(ev); + if (!model) { + return false; + } + + // TODO: use inline rename form + var newName = window.prompt('Enter new name', model.get('displayName')); + if (newName) { + // TODO: lock row during save + model.save({ + displayName: newName + }); + } + return false; + }, + + _onDeleteGroup: function(ev) { + ev.preventDefault(); + + var model = this._findGroupModelForEvent(ev); + if (!model) { + return false; + } + + // TODO: use undo approach + if (window.confirm('Confirm deletion of group ' + model.get('displayName') + ' ?')) { + model.destroy({ + wait: true + }); + } + return false; + }, + + _onRequest: function() { + this._toggleLoading(true); + this.$('.empty').addClass('hidden'); + }, + + _onEndRequest: function() { + this._toggleLoading(false); + this._updateEmptyState(); + }, + + _updateEmptyState: function() { + this.$('.empty').toggleClass('hidden', !!this.collection.length); + this.$('.grid').toggleClass('hidden', !this.collection.length); + }, + + /** + * Select by group id or model. + * + * @param {String|OCA.CustomGroups.GroupModel} group group id or group model + * @param {Object} [$el] optional DOM element of the entry + */ + select: function(group, $el) { + var model = group; + if (_.isString(model)) { + model = this.collection.findWhere({'id': model}); + } + + if (model) { + if (this._lastActive) { + this._lastActive.removeClass('active'); + } + + if (_.isUndefined($el)) { + $el = this.$('.group').filterAttr('data-id', model.id); + } + this._lastActive = $el.addClass('active'); + this.trigger('select', model); + } + }, + + _onSelect: function(ev) { + ev.preventDefault(); + var $el = $(ev.target).closest('.group'); + var id = $el.attr('data-id'); + this.select(id, $el); + }, + + /** + * Attempt creating a group with the given name. + * If the uri already exists, try another one. + * + * @param {string} groupName group display name + */ + _createGroup: function(groupName, index) { + var self = this; + // TODO: check if the current user already has a group with this name + + // it might happen that a group uri already exists for that name, + // so attempt multiple ones + this.collection.create({ + id: this._formatUri(groupName, index), + displayName: groupName, + role: OCA.CustomGroups.ROLE_ADMIN + }, { + wait: true, + success: function(model) { + self.$('[name=groupName]').val(''); + self.select(model); + }, + error: function(model, response) { + // stop at 100 to avoid running for too long... + if (response.status === 405 && (_.isUndefined(index) || index < 100)) { + if (_.isUndefined(index)) { + // attempt again with index + index = 1; + } + self._createGroup(groupName, index + 1); + return; + } + OC.Notification.showTemporary(t('customgroups', 'Could not create group')); + } + }); + }, + + _onSubmitCreationForm: function(ev) { + ev.preventDefault(); + var groupName = this.$('[name=groupName]').val(); + + if (groupName) { + this._createGroup(groupName); + } + return false; + }, + + _onAddModel: function(model, collection, options) { + var $el = $(this.itemTemplate(this._formatItem(model))); + if (!_.isUndefined(options.at) && collection.length > 1) { + this.$container.find('.group').eq(options.at).before($el); + } else { + this.$container.append($el); + } + this._postProcessRow($el, model); + this._updateEmptyState(); + }, + + _onChangeModel: function(model) { + var $el = $(this.itemTemplate(this._formatItem(model))); + var index = this.collection.indexOf(model); + this.$container.children().eq(index).replaceWith($el); + this._postProcessRow($el, model); + }, + + + _postProcessRow: function($el, model) { + $el.find('[title]').tooltip(); + // no group avatars, so using "imageplaceholder" directly instead of "avatar" + $el.find('.avatar').imageplaceholder( + // combine uri with display name for seed + model.id + ':' + model.get('displayName'), + model.get('displayName'), + 32 + ); + }, + + _onRemoveGroup: function(model, collection, options) { + var $groupEl; + if (_.isNumber(options.index)) { + $groupEl = this.$('.group-list .group:nth-child(' + (options.index + 1) + ')'); + } else { + // less efficient + $groupEl = this.$('.group-list .group').filterAttr('data-id', model.id); + } + if ($groupEl.hasClass('active')) { + // deselect + this.trigger('select', null); + this._lastActive = null; + } + $groupEl.remove(); + if (!collection.length) { + this._updateEmptyState(); + } + }, + + /** + * Converts a group display name to a valid URI name + * + * @param {string} groupName display name + * @return {string} group URI + */ + _formatUri: function(groupName, index) { + // TODO: strip unwanted chars + if (!_.isUndefined(index)) { + return groupName + index; + } + return groupName; + }, + + _formatItem: function(group) { + return { + id: group.id, + displayName: group.get('displayName'), + renameLabel: t('customgroups', 'Rename'), + deleteLabel: t('customgroups', 'Delete'), + canAdmin: OC.isUserAdmin() || group.get('role') === OCA.CustomGroups.ROLE_ADMIN, + roleDisplayName: (group.get('role') === OCA.CustomGroups.ROLE_ADMIN) ? + t('customgroups', 'Group admin') : + t('customgroups', 'Member') + }; + }, + + template: function(data) { + return OCA.CustomGroups.Templates.list(data); + }, + + itemTemplate: function(data) { + return OCA.CustomGroups.Templates.listItem(data); + }, + + render: function() { + this.$el.html(this.template({ + displayNameLabel: t('customgroups', 'Display name'), + newGroupPlaceholder: t('customgroups', 'Group name'), + newGroupSubmitLabel: t('customgroups', 'Create group'), + groupLabel: t('customgroups', 'Group'), + yourRoleLabel: t('customgroups', 'Your role'), + emptyMessage: t('customgroups', 'There are currently no user defined groups') + })); + this.$container = this.$('.group-list'); + this.delegateEvents(); + } + }); + + OCA.CustomGroups = _.extend({}, OCA.CustomGroups); + OCA.CustomGroups.GroupsView = GroupsView; + +})(OC, OCA); + diff --git a/js/MemberModel.js b/js/MemberModel.js new file mode 100644 index 00000000..fc115709 --- /dev/null +++ b/js/MemberModel.js @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2017 Vincent Petry + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +(function(OC, OCA) { + var NS = '{' + OC.Files.Client.NS_OWNCLOUD + '}'; + + /** + * @class OCA.CustomGroups.GroupModel + * @classdesc + * + */ + var MemberModel = OC.Backbone.WebdavNode.extend( + /** @lends OCA.CustomGroups.MemberModel.prototype */ { + + defaults: { + 'role': OCA.CustomGroups.ROLE_MEMBER + }, + + url: function() { + return OC.linkToRemote('dav') + '/customgroups/groups/' + + encodeURIComponent(this.collection.collectionNode.get('id')) + '/' + + encodeURIComponent(this.id); + }, + + davProperties: { + 'role': NS + 'role' + } + }); + + OCA.CustomGroups = _.extend({}, OCA.CustomGroups); + OCA.CustomGroups.MemberModel = MemberModel; + +})(OC, OCA); + diff --git a/js/MembersCollection.js b/js/MembersCollection.js new file mode 100644 index 00000000..e45a2118 --- /dev/null +++ b/js/MembersCollection.js @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2017 Vincent Petry + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ +(function(OC, OCA) { + /** + * @class OCA.CustomGroups.GroupsCollection + * @classdesc + * + */ + var MembersCollection = OC.Backbone.WebdavChildrenCollection.extend( + /** @lends OCA.CustomGroups.MembersCollection.prototype */ { + model: OCA.CustomGroups.MemberModel, + + url: function() { + return OC.linkToRemote('dav') + '/customgroups/groups/' + + encodeURIComponent(this.collectionNode.get('id')) + '/'; + } + }); + + OCA.CustomGroups = _.extend({}, OCA.CustomGroups); + OCA.CustomGroups.MembersCollection = MembersCollection; + +})(OC, OCA); + diff --git a/js/MembersView.js b/js/MembersView.js new file mode 100644 index 00000000..c91b4050 --- /dev/null +++ b/js/MembersView.js @@ -0,0 +1,278 @@ +/* + * Copyright (c) 2016 Vincent Petry + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +(function(OC, OCA) { + + /** + * @class OCA.CustomGroups.MembersView + * @classdesc + * + */ + var MembersView = OC.Backbone.View.extend( + /** @lends OCA.CustomGroups.MembersView.prototype */ { + sync: OC.Backbone.davSync, + + events: { + 'submit form': '_onSubmitCreationForm', + 'click .action-delete-member': '_onDeleteMember', + 'click .action-change-member-role': '_onChangeMemberRole', + 'click .action-leave-group': '_onClickLeaveGroup' + }, + + initialize: function(model) { + this.model = model; + this.collection = model.getChildrenCollection(); + + this.collection.on('request', this._onRequest, this); + this.collection.on('sync', this._onEndRequest, this); + this.collection.on('add', this._onAddModel, this); + this.collection.on('change', this._onChangeModel, this); + this.collection.on('remove', this._onRemoveMember, this); + + this.model.on('change:displayName', this._renderHeader, this); + + this.collection.reset([], {silent: true}); + this.collection.fetch(); + + _.bindAll( + this, + '_onSubmitCreationForm', + '_onDeleteMember', + '_onChangeMemberRole', + '_onClickLeaveGroup' + ); + }, + + remove: function() { + this.collection.off('request', this._onRequest, this); + this.collection.off('sync', this._onEndRequest, this); + this.collection.off('add', this._onAddModel, this); + this.collection.off('change', this._onChangeModel, this); + this.collection.off('remove', this._onRemoveMember, this); + this.model.off('change:displayName', this._renderHeader, this); + + this.$el.remove(); + }, + + _toggleLoading: function(state) { + this._loading = state; + this.$('.loading').toggleClass('hidden', !state); + }, + + _onRequest: function() { + this._toggleLoading(true); + this.$('.empty').addClass('hidden'); + }, + + _onEndRequest: function() { + this._toggleLoading(false); + this.$('.empty').toggleClass('hidden', !!this.collection.length); + }, + + _onClickLeaveGroup: function() { + var currentUserMembership = this.collection.get(OC.getCurrentUser().uid); + if (window.confirm('Confirm leaving of group ' + this.model.get('displayName') + ' ?')) { + currentUserMembership.destroy({ + wait: true, + error: function(model, response) { + if (response.status === 403) { + OC.Notification.showTemporary(t('customgroups', 'Cannot leave group without an administrator')); + } else { + OC.Notification.showTemporary(t('customgroups', 'Could not leave group due to a server error')); + } + } + }); + } + }, + + _onAddModel: function(model, collection, options) { + var $el = $(this.itemTemplate(this._formatMember(model))); + if (!_.isUndefined(options.index) && collection.length > 1) { + this.$container.find('.group-member').eq(options.index).before($el); + } else { + this.$container.append($el); + } + this._postProcessRow($el, model); + }, + + _onChangeModel: function(model) { + var $el = $(this.itemTemplate(this._formatMember(model))); + var index = this.collection.indexOf(model); + this.$container.children().eq(index).replaceWith($el); + this._postProcessRow($el, model); + }, + + _postProcessRow: function($el, model) { + $el.find('[title]').tooltip(); + $el.find('.avatar').avatar(model.id, 32); + }, + + _onRemoveMember: function(model, collection, options) { + var $memberEl; + if (_.isNumber(options.index)) { + $memberEl = this.$('.grid .group-member:nth-child(' + (options.index + 1) + ')'); + } else { + $memberEl = this.$('.grid .group-member').filterAttr('data-id', model.id); + } + $memberEl.remove(); + }, + + _onSubmitCreationForm: function(ev) { + ev.preventDefault(); + var $field = this.$('[name=memberUserId]'); + var userId = $field.val(); + + if (!userId) { + return false; + } + + this.collection.create({ + id: userId + }, { + wait: true, + success: function() { + $field.val(''); + }, + error: function(model, response) { + if (response.status === 412) { + OC.Notification.showTemporary(t( + 'customgroups', + 'User "{userId}" does not exist', + {userId: userId} + )); + } else if (response.status === 409) { + OC.Notification.showTemporary(t( + 'customgroups', + 'User "{userId}" is already a member of this group', + {userId: userId} + )); + } else { + OC.Notification.showTemporary(t( + 'customgroups', + 'Could not add user to group' + )); + } + } + }); + return false; + }, + + _onDeleteMember: function(ev) { + ev.preventDefault(); + var $row = $(ev.target).closest('.group-member'); + var id = $row.attr('data-id'); + + // deleting self ? + if (id === OC.getCurrentUser().uid) { + return this._onClickLeaveGroup(); + } + + var model = this.collection.findWhere({'id': id}); + + if (!model) { + return; + } + + // TODO: use undo approach + if (window.confirm('Confirm deletion of member ' + model.id + ' ?')) { + model.destroy(); + } + }, + + _onChangeMemberRole: function(ev) { + ev.preventDefault(); + var $row = $(ev.target).closest('.group-member'); + var id = $row.attr('data-id'); + + // changing own permissions ? + if (id === OC.getCurrentUser().uid) { + if (!window.confirm('Remove your admin powers ?')) { + return; + } + } + + var model = this.collection.findWhere({'id': id}); + + if (!model) { + return; + } + + // swap permissions + var newRole = (model.get('role') === OCA.CustomGroups.ROLE_ADMIN) ? + OCA.CustomGroups.ROLE_MEMBER : + OCA.CustomGroups.ROLE_ADMIN; + + model.save({ + role: newRole + }); + }, + + _formatMember: function(member) { + return { + id: member.id, + displayName: member.id, + canAdmin: OC.isUserAdmin() || this.model.get('role') === OCA.CustomGroups.ROLE_ADMIN, + roleDisplayName: (member.get('role') === OCA.CustomGroups.ROLE_ADMIN) ? + t('customgroups', 'Group admin') : + t('customgroups', 'Member') + }; + }, + + template: function(data) { + return OCA.CustomGroups.Templates.membersList(data); + }, + + headerTemplate: function(data) { + return OCA.CustomGroups.Templates.membersListHeader(data); + }, + + itemTemplate: function(data) { + return OCA.CustomGroups.Templates.membersListItem(data); + }, + + _renderHeader: function() { + var data = { + displayNameLabel: t('customgroups', 'Display name'), + newMemberPlaceholder: t('customgroups', 'Add member'), + newMemberSubmitLabel: t('customgroups', 'Add member'), + leaveGroupLabel: t('customgroups', 'Leave this group'), + closeLabel: t('customgroups', 'Close'), + // super admin might not be member + // not having a role means not being a member + userIsMember: !_.isUndefined(this.model.get('role')), + canAdmin: OC.isUserAdmin() || this.model.get('role') === OCA.CustomGroups.ROLE_ADMIN + }; + if (this.model) { + data.groupName = this.model.get('displayName'); + } + var $header = this.$('.header'); + $header.html(this.headerTemplate(data)); + $header.find('.avatar').imageplaceholder( + // combine uri with display name for seed + this.model.id + ':' + this.model.get('displayName'), + this.model.get('displayName'), + 32 + ); + }, + + render: function() { + this.$el.html(this.template()); + this.$('[title]').tooltip(); + this.$container = this.$('.group-member-list'); + this._renderHeader(); + this.delegateEvents(); + } + }); + + OCA.CustomGroups = _.extend({}, OCA.CustomGroups); + OCA.CustomGroups.MembersView = MembersView; + +})(OC, OCA); + diff --git a/js/modules.json b/js/modules.json new file mode 100644 index 00000000..478cc635 --- /dev/null +++ b/js/modules.json @@ -0,0 +1,15 @@ +[ + "App", + "MemberModel", + "MembersCollection", + "MembersView", + "GroupModel", + "GroupsCollection", + "GroupsView", + "templates/app.handlebars", + "templates/list.handlebars", + "templates/listItem.handlebars", + "templates/membersList.handlebars", + "templates/membersListHeader.handlebars", + "templates/membersListItem.handlebars" +] diff --git a/js/templates/app.handlebars b/js/templates/app.handlebars new file mode 100644 index 00000000..168c49c2 --- /dev/null +++ b/js/templates/app.handlebars @@ -0,0 +1,4 @@ +

{{customGroupsTitle}}

+
+ + diff --git a/js/templates/list.handlebars b/js/templates/list.handlebars new file mode 100644 index 00000000..d5014e04 --- /dev/null +++ b/js/templates/list.handlebars @@ -0,0 +1,20 @@ +
+
+ + +
+
+ + + + + + + + + + + + + + diff --git a/js/templates/listItem.handlebars b/js/templates/listItem.handlebars new file mode 100644 index 00000000..3b54f693 --- /dev/null +++ b/js/templates/listItem.handlebars @@ -0,0 +1,10 @@ + +
+ {{displayName}} + {{#if canAdmin}} + + {{/if}} + + {{roleDisplayName}} + {{#if canAdmin}}{{/if}} + diff --git a/js/templates/membersList.handlebars b/js/templates/membersList.handlebars new file mode 100644 index 00000000..915684d5 --- /dev/null +++ b/js/templates/membersList.handlebars @@ -0,0 +1,13 @@ +
+
+ + + + + + + + + +
MemberRole
+ diff --git a/js/templates/membersListHeader.handlebars b/js/templates/membersListHeader.handlebars new file mode 100644 index 00000000..19f411fd --- /dev/null +++ b/js/templates/membersListHeader.handlebars @@ -0,0 +1,24 @@ +
+ + {{groupName}} +
+ +{{#if userIsMember}} + +{{/if}} + +{{#if canAdmin}} +
+
+ + +
+
+{{/if}} diff --git a/js/templates/membersListItem.handlebars b/js/templates/membersListItem.handlebars new file mode 100644 index 00000000..dc190890 --- /dev/null +++ b/js/templates/membersListItem.handlebars @@ -0,0 +1,10 @@ + +
+ {{displayName}} + {{roleDisplayName}} + {{#if canAdmin}} + + {{/if}} + + {{#if canAdmin}}{{deleteMemberLabel}}{{/if}} + diff --git a/l10n/.gitkeep b/l10n/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php new file mode 100644 index 00000000..7bf4d8d6 --- /dev/null +++ b/lib/Controller/PageController.php @@ -0,0 +1,40 @@ + + * + * @copyright Copyright (c) 2016, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\CustomGroups\Controller; + +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\TemplateResponse; + +class PageController extends Controller { + + /** + * @NoCSRFRequired + * @NoAdminRequired + */ + public function index() { + // TODO: cache or add to info.xml ? + $modules = json_decode(file_get_contents(__DIR__ . '/../../js/modules.json')); + return new TemplateResponse($this->appName, 'index', [ + 'modules' => $modules + ]); + } +} diff --git a/lib/Dav/CustomGroupsPlugin.php b/lib/Dav/CustomGroupsPlugin.php index 3043fff8..8deaf062 100644 --- a/lib/Dav/CustomGroupsPlugin.php +++ b/lib/Dav/CustomGroupsPlugin.php @@ -91,7 +91,8 @@ public function initialize(\Sabre\DAV\Server $server) { $this->server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc'; $ns = '{' . self::NS_OWNCLOUD . '}'; $this->server->resourceTypeMapping[MembershipNode::class] = $ns . 'customgroups-membership'; - $this->server->resourceTypeMapping[GroupsCollection::class] = $ns . 'customgroups-group'; + $this->server->resourceTypeMapping[GroupsCollection::class] = $ns . 'customgroups-groups'; + $this->server->resourceTypeMapping[GroupMembershipCollection::class] = $ns . 'customgroups-group'; $this->server->protectedProperties[] = $ns . 'user-id'; $this->server->protectedProperties[] = $ns . 'group-uri'; diff --git a/lib/Dav/MembershipNode.php b/lib/Dav/MembershipNode.php index d0e9a748..518c1bce 100644 --- a/lib/Dav/MembershipNode.php +++ b/lib/Dav/MembershipNode.php @@ -36,7 +36,6 @@ class MembershipNode implements \Sabre\DAV\INode, \Sabre\DAV\IProperties { const PROPERTY_ROLE = '{http://owncloud.org/ns}role'; const PROPERTY_USER_ID = '{http://owncloud.org/ns}user-id'; - const PROPERTY_GROUP_URI = '{http://owncloud.org/ns}group-uri'; /** * Custom groups handler @@ -182,9 +181,6 @@ public function getProperties($properties) { if ($properties === null || in_array(self::PROPERTY_USER_ID, $properties)) { $result[self::PROPERTY_USER_ID] = $this->memberInfo['user_id']; } - if ($properties === null || in_array(self::PROPERTY_GROUP_URI, $properties)) { - $result[self::PROPERTY_GROUP_URI] = $this->memberInfo['uri']; - } return $result; } diff --git a/lib/SettingsPanel.php b/lib/SettingsPanel.php new file mode 100644 index 00000000..f6bc84b3 --- /dev/null +++ b/lib/SettingsPanel.php @@ -0,0 +1,48 @@ + + * + * @copyright Copyright (c) 2017, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\CustomGroups; + +use OCP\Settings\ISettings; +use OCP\Template; + +class SettingsPanel implements ISettings { + + public function __construct() { + } + + public function getPanel() { + // TODO: cache or add to info.xml ? + $modules = json_decode(file_get_contents(__DIR__ . '/../js/modules.json'), true); + $tmpl = new Template('customgroups', 'index'); + $tmpl->assign('modules', $modules); + return $tmpl; + } + + public function getPriority() { + return 0; + } + + public function getSectionID() { + return 'customgroups'; + } + +} diff --git a/lib/composer/autoload.php b/lib/composer/autoload.php deleted file mode 100644 index 1e0233e1..00000000 --- a/lib/composer/autoload.php +++ /dev/null @@ -1,7 +0,0 @@ - - * Jordi Boggiano - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Composer\Autoload; - -/** - * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. - * - * $loader = new \Composer\Autoload\ClassLoader(); - * - * // register classes with namespaces - * $loader->add('Symfony\Component', __DIR__.'/component'); - * $loader->add('Symfony', __DIR__.'/framework'); - * - * // activate the autoloader - * $loader->register(); - * - * // to enable searching the include path (eg. for PEAR packages) - * $loader->setUseIncludePath(true); - * - * In this example, if you try to use a class in the Symfony\Component - * namespace or one of its children (Symfony\Component\Console for instance), - * the autoloader will first look for the class under the component/ - * directory, and it will then fallback to the framework/ directory if not - * found before giving up. - * - * This class is loosely based on the Symfony UniversalClassLoader. - * - * @author Fabien Potencier - * @author Jordi Boggiano - * @see http://www.php-fig.org/psr/psr-0/ - * @see http://www.php-fig.org/psr/psr-4/ - */ -class ClassLoader -{ - // PSR-4 - private $prefixLengthsPsr4 = array(); - private $prefixDirsPsr4 = array(); - private $fallbackDirsPsr4 = array(); - - // PSR-0 - private $prefixesPsr0 = array(); - private $fallbackDirsPsr0 = array(); - - private $useIncludePath = false; - private $classMap = array(); - private $classMapAuthoritative = false; - private $missingClasses = array(); - private $apcuPrefix; - - public function getPrefixes() - { - if (!empty($this->prefixesPsr0)) { - return call_user_func_array('array_merge', $this->prefixesPsr0); - } - - return array(); - } - - public function getPrefixesPsr4() - { - return $this->prefixDirsPsr4; - } - - public function getFallbackDirs() - { - return $this->fallbackDirsPsr0; - } - - public function getFallbackDirsPsr4() - { - return $this->fallbackDirsPsr4; - } - - public function getClassMap() - { - return $this->classMap; - } - - /** - * @param array $classMap Class to filename map - */ - public function addClassMap(array $classMap) - { - if ($this->classMap) { - $this->classMap = array_merge($this->classMap, $classMap); - } else { - $this->classMap = $classMap; - } - } - - /** - * Registers a set of PSR-0 directories for a given prefix, either - * appending or prepending to the ones previously set for this prefix. - * - * @param string $prefix The prefix - * @param array|string $paths The PSR-0 root directories - * @param bool $prepend Whether to prepend the directories - */ - public function add($prefix, $paths, $prepend = false) - { - if (!$prefix) { - if ($prepend) { - $this->fallbackDirsPsr0 = array_merge( - (array) $paths, - $this->fallbackDirsPsr0 - ); - } else { - $this->fallbackDirsPsr0 = array_merge( - $this->fallbackDirsPsr0, - (array) $paths - ); - } - - return; - } - - $first = $prefix[0]; - if (!isset($this->prefixesPsr0[$first][$prefix])) { - $this->prefixesPsr0[$first][$prefix] = (array) $paths; - - return; - } - if ($prepend) { - $this->prefixesPsr0[$first][$prefix] = array_merge( - (array) $paths, - $this->prefixesPsr0[$first][$prefix] - ); - } else { - $this->prefixesPsr0[$first][$prefix] = array_merge( - $this->prefixesPsr0[$first][$prefix], - (array) $paths - ); - } - } - - /** - * Registers a set of PSR-4 directories for a given namespace, either - * appending or prepending to the ones previously set for this namespace. - * - * @param string $prefix The prefix/namespace, with trailing '\\' - * @param array|string $paths The PSR-4 base directories - * @param bool $prepend Whether to prepend the directories - * - * @throws \InvalidArgumentException - */ - public function addPsr4($prefix, $paths, $prepend = false) - { - if (!$prefix) { - // Register directories for the root namespace. - if ($prepend) { - $this->fallbackDirsPsr4 = array_merge( - (array) $paths, - $this->fallbackDirsPsr4 - ); - } else { - $this->fallbackDirsPsr4 = array_merge( - $this->fallbackDirsPsr4, - (array) $paths - ); - } - } elseif (!isset($this->prefixDirsPsr4[$prefix])) { - // Register directories for a new namespace. - $length = strlen($prefix); - if ('\\' !== $prefix[$length - 1]) { - throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); - } - $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; - $this->prefixDirsPsr4[$prefix] = (array) $paths; - } elseif ($prepend) { - // Prepend directories for an already registered namespace. - $this->prefixDirsPsr4[$prefix] = array_merge( - (array) $paths, - $this->prefixDirsPsr4[$prefix] - ); - } else { - // Append directories for an already registered namespace. - $this->prefixDirsPsr4[$prefix] = array_merge( - $this->prefixDirsPsr4[$prefix], - (array) $paths - ); - } - } - - /** - * Registers a set of PSR-0 directories for a given prefix, - * replacing any others previously set for this prefix. - * - * @param string $prefix The prefix - * @param array|string $paths The PSR-0 base directories - */ - public function set($prefix, $paths) - { - if (!$prefix) { - $this->fallbackDirsPsr0 = (array) $paths; - } else { - $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; - } - } - - /** - * Registers a set of PSR-4 directories for a given namespace, - * replacing any others previously set for this namespace. - * - * @param string $prefix The prefix/namespace, with trailing '\\' - * @param array|string $paths The PSR-4 base directories - * - * @throws \InvalidArgumentException - */ - public function setPsr4($prefix, $paths) - { - if (!$prefix) { - $this->fallbackDirsPsr4 = (array) $paths; - } else { - $length = strlen($prefix); - if ('\\' !== $prefix[$length - 1]) { - throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); - } - $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; - $this->prefixDirsPsr4[$prefix] = (array) $paths; - } - } - - /** - * Turns on searching the include path for class files. - * - * @param bool $useIncludePath - */ - public function setUseIncludePath($useIncludePath) - { - $this->useIncludePath = $useIncludePath; - } - - /** - * Can be used to check if the autoloader uses the include path to check - * for classes. - * - * @return bool - */ - public function getUseIncludePath() - { - return $this->useIncludePath; - } - - /** - * Turns off searching the prefix and fallback directories for classes - * that have not been registered with the class map. - * - * @param bool $classMapAuthoritative - */ - public function setClassMapAuthoritative($classMapAuthoritative) - { - $this->classMapAuthoritative = $classMapAuthoritative; - } - - /** - * Should class lookup fail if not found in the current class map? - * - * @return bool - */ - public function isClassMapAuthoritative() - { - return $this->classMapAuthoritative; - } - - /** - * APCu prefix to use to cache found/not-found classes, if the extension is enabled. - * - * @param string|null $apcuPrefix - */ - public function setApcuPrefix($apcuPrefix) - { - $this->apcuPrefix = function_exists('apcu_fetch') && ini_get('apc.enabled') ? $apcuPrefix : null; - } - - /** - * The APCu prefix in use, or null if APCu caching is not enabled. - * - * @return string|null - */ - public function getApcuPrefix() - { - return $this->apcuPrefix; - } - - /** - * Registers this instance as an autoloader. - * - * @param bool $prepend Whether to prepend the autoloader or not - */ - public function register($prepend = false) - { - spl_autoload_register(array($this, 'loadClass'), true, $prepend); - } - - /** - * Unregisters this instance as an autoloader. - */ - public function unregister() - { - spl_autoload_unregister(array($this, 'loadClass')); - } - - /** - * Loads the given class or interface. - * - * @param string $class The name of the class - * @return bool|null True if loaded, null otherwise - */ - public function loadClass($class) - { - if ($file = $this->findFile($class)) { - includeFile($file); - - return true; - } - } - - /** - * Finds the path to the file where the class is defined. - * - * @param string $class The name of the class - * - * @return string|false The path if found, false otherwise - */ - public function findFile($class) - { - // class map lookup - if (isset($this->classMap[$class])) { - return $this->classMap[$class]; - } - if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { - return false; - } - if (null !== $this->apcuPrefix) { - $file = apcu_fetch($this->apcuPrefix.$class, $hit); - if ($hit) { - return $file; - } - } - - $file = $this->findFileWithExtension($class, '.php'); - - // Search for Hack files if we are running on HHVM - if (false === $file && defined('HHVM_VERSION')) { - $file = $this->findFileWithExtension($class, '.hh'); - } - - if (null !== $this->apcuPrefix) { - apcu_add($this->apcuPrefix.$class, $file); - } - - if (false === $file) { - // Remember that this class does not exist. - $this->missingClasses[$class] = true; - } - - return $file; - } - - private function findFileWithExtension($class, $ext) - { - // PSR-4 lookup - $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; - - $first = $class[0]; - if (isset($this->prefixLengthsPsr4[$first])) { - foreach ($this->prefixLengthsPsr4[$first] as $prefix => $length) { - if (0 === strpos($class, $prefix)) { - foreach ($this->prefixDirsPsr4[$prefix] as $dir) { - if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) { - return $file; - } - } - } - } - } - - // PSR-4 fallback dirs - foreach ($this->fallbackDirsPsr4 as $dir) { - if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { - return $file; - } - } - - // PSR-0 lookup - if (false !== $pos = strrpos($class, '\\')) { - // namespaced class name - $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) - . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); - } else { - // PEAR-like class name - $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; - } - - if (isset($this->prefixesPsr0[$first])) { - foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { - if (0 === strpos($class, $prefix)) { - foreach ($dirs as $dir) { - if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { - return $file; - } - } - } - } - } - - // PSR-0 fallback dirs - foreach ($this->fallbackDirsPsr0 as $dir) { - if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { - return $file; - } - } - - // PSR-0 include paths. - if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { - return $file; - } - - return false; - } -} - -/** - * Scope isolated include. - * - * Prevents access to $this/self from included files. - */ -function includeFile($file) -{ - include $file; -} diff --git a/lib/composer/composer/LICENSE b/lib/composer/composer/LICENSE deleted file mode 100644 index 1a281248..00000000 --- a/lib/composer/composer/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ - -Copyright (c) 2016 Nils Adermann, Jordi Boggiano - -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. - diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php deleted file mode 100644 index 71dd9c17..00000000 --- a/lib/composer/composer/autoload_classmap.php +++ /dev/null @@ -1,9 +0,0 @@ -= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); - if ($useStaticLoader) { - require_once __DIR__ . '/autoload_static.php'; - - call_user_func(\Composer\Autoload\ComposerStaticInit8da449fa80f819e0a774ccb262c7cf3a::getInitializer($loader)); - } else { - $map = require __DIR__ . '/autoload_namespaces.php'; - foreach ($map as $namespace => $path) { - $loader->set($namespace, $path); - } - - $map = require __DIR__ . '/autoload_psr4.php'; - foreach ($map as $namespace => $path) { - $loader->setPsr4($namespace, $path); - } - - $classMap = require __DIR__ . '/autoload_classmap.php'; - if ($classMap) { - $loader->addClassMap($classMap); - } - } - - $loader->register(true); - - return $loader; - } -} diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php deleted file mode 100644 index 65055bea..00000000 --- a/lib/composer/composer/autoload_static.php +++ /dev/null @@ -1,15 +0,0 @@ -= 0.8", + "scripts": { + "build": "make templates", + "watch": "npm-watch", + "test": "node_modules/.bin/karma start tests/js/karma.config.js --single-run" + }, + "watch": { + "build": { + "patterns": [ + "js/templates/*.{js,handlebars}" + ], + "ignore": "**/*.handlebars.js", + "extensions": "js,handlebars" + } + } +} diff --git a/rules/deps.mk b/rules/deps.mk new file mode 100644 index 00000000..d8b16788 --- /dev/null +++ b/rules/deps.mk @@ -0,0 +1,75 @@ +# Deps + +NODE_PREFIX=$(shell pwd) + +NPM := $(shell command -v npm 2> /dev/null) +ifndef NPM + $(error npm is not available on your system, please install npm) +endif + +BOWER=$(NODE_PREFIX)/node_modules/bower/bin/bower +JSDOC=$(NODE_PREFIX)/node_modules/.bin/jsdoc +COMPOSER=$(tools_path)/composer.phar + +composer_deps=lib/composer +composer_dev_deps=lib/composer/phpunit +nodejs_deps=node_modules +bower_deps=js/vendor +clean_rules+=clean-deps +help_rules+=help-deps + +.PHONY: help-deps +help-deps: + @echo -e "Dependencies:\n" + @echo -e "deps\t\tto fetch all dependencies" + @echo -e "update-composer\tto update composer.lock" + @echo + +.PHONY: clean-deps +clean-deps: clean-composer clean-bower clean-nodejs + rm -Rf $(tools_path)/*.phar + +.PHONY: clean-composer +clean-composer: + rm -Rf $(composer_deps)/ + +.PHONY: clean-bower +clean-bower: + rm -Rf $(bower_deps)/ + +.PHONY: clean-nodejs +clean-nodejs: + rm -Rf $(nodejs_deps)/ + +$(COMPOSER): + cd "$(tools_path)" && curl -ss https://getcomposer.org/installer | php + chmod u+x $@ + +$(composer_deps): $(COMPOSER) composer.json composer.lock + php $(COMPOSER) install --no-dev && touch $@ + +$(composer_dev_deps): $(COMPOSER) composer.json composer.lock + php $(COMPOSER) install && touch $@ + +.PHONY: update-composer +update-composer: + php $(COMPOSER) update + +# +# Node JS dependencies for tools +# +$(nodejs_deps): package.json + $(NPM) install --prefix $(NODE_PREFIX) && touch $@ + +$(BOWER): $(nodejs_deps) +$(JSDOC): $(nodejs_deps) + +$(bower_deps): $(BOWER) + $(BOWER) install && touch $@ + +.PHONY: deps +deps: $(composer_deps) $(bower_deps) + +.PHONY: dev-deps +dev-deps: $(composer_dev_deps) $(bower_deps) $(nodejs_deps) + diff --git a/rules/dist.mk b/rules/dist.mk new file mode 100644 index 00000000..41d48b9d --- /dev/null +++ b/rules/dist.mk @@ -0,0 +1,57 @@ +# Deps +# Build and dist rules + +help_rules+=help-dist +clean_rules+=clean-build + +.PHONY: help-market +help-dist: + @echo -e "Building:\n" + @echo -e "dist\t\tto build the distribution folder $(build_dir)/$(app_name)" + @echo -e "market\t\tto build the market tarball in $(build_dir)/$(app_name).tar.gz" + @echo -e "clean\t\tto clean everything" + @echo + +.PHONY: clean-build +clean-build: + rm -Rf $(build_dir) + +.PHONY: market +market: $(build_dir)/$(app_name).tar.gz + +$(build_dir)/$(app_name).tar.gz: $(build_dir)/$(app_name) + cd $(build_dir); tar czf $@ $(app_name) + +$(build_dir)/$(app_name): deps $(all_src) + mkdir -p $@ + cp -R $(all_src) $@ + @echo Removing unwanted files... + find $@ \( \ + -name .gitkeep -o \ + -name .gitignore -o \ + -name no-php \ + \) -print | xargs rm -Rf + find $@/{lib/composer/,js/vendor/} \( \ + -name bin -o \ + -name test -o \ + -name tests -o \ + -name examples -o \ + -name demo -o \ + -name demos -o \ + -name doc -o \ + -name travis -o \ + -name .bower.json -o \ + -name bower.json -o \ + -name package.json -o \ + -name testem.json -o \ + -iname \*.sh -o \ + -iname \*.exe \ + \) -print | xargs rm -Rf + touch $@ + +.PHONY: dist +dist: $(build_dir)/$(app_name) + +.PHONY: distclean +distclean: clean + diff --git a/rules/frontend.mk b/rules/frontend.mk new file mode 100644 index 00000000..e3ea1098 --- /dev/null +++ b/rules/frontend.mk @@ -0,0 +1,40 @@ +# Frontend rules + +HANDLEBARS=$(NODE_PREFIX)/node_modules/.bin/handlebars + +template_src=$(wildcard js/templates/*.handlebars) +clean_rules+=clean-templates +js_rules+=templates +help_rules+=help-frontend +js_namespace=OCA.$(app_namespace) + +.PHONY: help-frontend +help-frontend: + @echo -e "Frontend development:\n" + @echo -e "watch\t\tto watch for changes and rebuild all templates" + @echo -e "templates\tto manually build all templates" + @echo -e "clean-templates\tto clean generated templates" + @echo + +# add compiled templates to sources +all_src+=$(addsuffix .js, $(template_src)) + +$(HANDLEBARS): $(nodejs_deps) + +%.handlebars.js: %.handlebars $(HANDLEBARS) + $(HANDLEBARS) -n "$(js_namespace).Templates" $< > $@ + +.PHONY: templates +templates: $(addsuffix .js, $(template_src)) + +.PHONY: clean-templates +clean-templates: + rm -f $(addsuffix .js, $(template_src)) + +# +# Watch +# +.PHONY: watch +watch: $(nodejs_deps) + $(NPM) --prefix $(NODE_PREFIX) run watch + diff --git a/rules/tests.mk b/rules/tests.mk new file mode 100644 index 00000000..4e1f42f9 --- /dev/null +++ b/rules/tests.mk @@ -0,0 +1,83 @@ +# Tests + +PHPUNIT="$(shell pwd)"/lib/composer/phpunit/phpunit/phpunit +OCULAR=$(shell pwd)/lib/composer/scrutinizer/ocular/bin/ocular +KARMA=$(NODE_PREFIX)/node_modules/.bin/karma +JSHINT=$(NODE_PREFIX)/node_modules/.bin/jshint + +test_rules+=test-codecheck test-codecheck-deprecations test-php test-js +clean_rules+=clean-deps +help_rules+=help-test + +tests_unit_results=tests/unit/results +clover_xml=$(tests_unit_results)/clover.xml +phpunit_args=--log-junit $(tests_unit_results)/results.xml +ifndef NOCOVERAGE # env variable +phpunit_args+=--coverage-clover $(clover_xml) --coverage-html $(tests_unit_results)/coverage-html +endif + +.PHONY: help-test +help-test: + @echo -e "Testing:\n" + @echo -e "test\t\t\tto run all test suites" + @echo -e "test-syntax\t\tto run syntax checks" + @echo -e "test-codecheck\t\tto run the code checker" + @echo -e "test-php\t\tto run PHP test suites" + @echo -e "test-integration\tto run integration tests" + @echo -e "test-js\t\t\tto run JS test suites (single run)" + @echo -e "test-js-debug\t\tto run JS test and watch for changes" + @echo + +.PHONY: clean-test +clean-test: + rm -Rf $(tests_unit_results) + +.PHONY: test-syntax +test-syntax: test-syntax-php test-syntax-js + +.PHONY: test-syntax-php +test-syntax-php: + for F in $(shell find . -name \*.php | grep -v -e 'lib/composer' -e 'vendor'); do \ + php -l "$$F" > /dev/null || exit $?; \ + done + +.PHONY: test-syntax-js +test-syntax-js: $(JSHINT) + -$(JSHINT) --config .jshintrc --exclude-path .gitignore js tests/js + +.PHONY: test-codecheck +test-codecheck: test-syntax-php + $(OCC) app:check-code $(app_name) -c private -c strong-comparison + +.PHONY: test-codecheck-deprecations +test-codecheck-deprecations: + $(OCC) app:check-code $(app_name) -c deprecation + +.PHONY: test-php +test-php: $(PHPUNIT) test-syntax-php + $(OCC) app:enable $(app_name) + $(PHPUNIT) --configuration tests/unit/phpunit.xml $(phpunit_args) + +$(clover_xml): test-php + +.PHONY: test-upload-coverage +test-upload-coverage: $(OCULAR) $(clover_xml) + $(OCULAR) code-coverage:upload --format=php-clover $(clover_xml) + +.PHONY: test-integration +test-integration: test-syntax-php + cd tests/integration && OCC="$(OCC)" ./run.sh + +.PHONY: test-js +test-js: $(bower_deps) $(KARMA) $(all_src) test-syntax-js + $(KARMA) start tests/js/karma.config.js --single-run + +test-js-debug: $(bower_deps) $(KARMA) $(all_src) test-syntax-js + $(KARMA) start tests/js/karma.config.js + +$(PHPUNIT): $(composer_dev_deps) +$(OCULAR): $(composer_dev_deps) + +$(KARMA): $(nodejs_deps) +$(JSHINT): $(nodejs_deps) + diff --git a/templates/index.php b/templates/index.php new file mode 100644 index 00000000..7baf79b3 --- /dev/null +++ b/templates/index.php @@ -0,0 +1,33 @@ + + * + * @copyright Copyright (c) 2016, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +style('customgroups', 'app'); +style('settings', 'settings'); +script('core', 'oc-backbone'); +script('core', 'oc-backbone-webdav'); +script('customgroups', 'vendor/handlebars/handlebars'); + +foreach ($_['modules'] as $module) { + script('customgroups', $module); +} +?> + +
diff --git a/tests/integration/.gitignore b/tests/integration/.gitignore new file mode 100644 index 00000000..ce523f09 --- /dev/null +++ b/tests/integration/.gitignore @@ -0,0 +1,2 @@ +vendor +output diff --git a/tests/integration/run.sh b/tests/integration/run.sh index 49a985d7..a398e9b6 100755 --- a/tests/integration/run.sh +++ b/tests/integration/run.sh @@ -1,5 +1,7 @@ #!/usr/bin/env bash +mkdir -p output + composer install OC_PATH=../../../../ @@ -9,7 +11,6 @@ cd "$OC_PATH""$CORE_INT_TESTS_PATH" composer install cd - - OCC=${OC_PATH}occ SCENARIO_TO_RUN=$1 diff --git a/tests/js/AppSpec.js b/tests/js/AppSpec.js new file mode 100644 index 00000000..116d5803 --- /dev/null +++ b/tests/js/AppSpec.js @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2016 Vincent Petry + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +describe('App test', function() { + var app; + + afterEach(function() { + if (app) { + app.remove(); + } + app = null; + }); + + describe('rendering', function() { + it('renders containers', function() { + app = new OCA.CustomGroups.App(); + expect(app.$('.groups-container').length).toEqual(1); + expect(app.$('.members-container').length).toEqual(1); + }); + }); + + describe('group collection', function() { + var isUserAdminStub; + var getCurrentUserStub; + var collectionClassStub; + var collectionStub; + + beforeEach(function() { + isUserAdminStub = sinon.stub(OC, 'isUserAdmin'); + getCurrentUserStub = sinon.stub(OC, 'getCurrentUser'); + getCurrentUserStub.returns({uid: 'someadmin'}); + + collectionStub = sinon.createStubInstance(OCA.CustomGroups.GroupsCollection); + + collectionClassStub = sinon.stub(OCA.CustomGroups, 'GroupsCollection'); + collectionClassStub.returns(collectionStub); + }); + afterEach(function() { + isUserAdminStub.restore(); + getCurrentUserStub.restore(); + collectionClassStub.restore(); + }); + + it('initializes collection with null user for superadmin', function() { + isUserAdminStub.returns(true); + + app = new OCA.CustomGroups.App(); + expect(collectionClassStub.calledOnce).toEqual(true); + expect(collectionClassStub.getCall(0).args[1]).toEqual({ + userId: null + }); + }); + it('initializes collection with uid for regular users', function() { + isUserAdminStub.returns(false); + app = new OCA.CustomGroups.App(); + expect(collectionClassStub.calledOnce).toEqual(true); + expect(collectionClassStub.getCall(0).args[1]).toEqual({ + userId: 'someadmin' + }); + }); + it('fetches collection', function() { + app = new OCA.CustomGroups.App(); + + expect(collectionStub.fetch.calledOnce).toEqual(true); + }); + }); + describe('selection', function() { + var collectionClassStub; + var collection; + + var membersViewClassStub; + var membersViewStub; + + beforeEach(function() { + app = new OCA.CustomGroups.App(); + + collection = new OCA.CustomGroups.GroupsCollection([ + { + id: 'group1', + displayName: 'group one' + }, + { + id: 'group2', + displayName: 'group two' + } + ]); + + collectionClassStub = sinon.stub(OCA.CustomGroups, 'GroupsCollection'); + collectionClassStub.returns(collection); + + membersViewStub = sinon.createStubInstance(OCA.CustomGroups.MembersView); + membersViewClassStub = sinon.stub(OCA.CustomGroups, 'MembersView'); + membersViewClassStub.returns(membersViewStub); + }); + afterEach(function() { + collectionClassStub.restore(); + membersViewClassStub.restore(); + collection = null; + }); + + it('displays sidebar', function() { + var showSidebarStub = sinon.stub(OC.Apps, 'showAppSidebar'); + app.listView.trigger('select', collection.at(0)); + expect(showSidebarStub.calledOnce).toEqual(true); + expect(showSidebarStub.calledWith(app.$membersContainer)).toEqual(true); + showSidebarStub.restore(); + }); + + it('updates members view when selecting a group', function() { + app.listView.trigger('select', collection.at(0)); + + expect(membersViewClassStub.getCall(0).args[0]).toEqual(collection.at(0)); + expect(membersViewStub.render.calledOnce).toEqual(true); + }); + + it('hides sidebar if group deselected', function() { + var showSidebarStub = sinon.stub(OC.Apps, 'showAppSidebar'); + var hideSidebarStub = sinon.stub(OC.Apps, 'hideAppSidebar'); + app.listView.trigger('select', null); + expect(showSidebarStub.notCalled).toEqual(true); + expect(hideSidebarStub.calledOnce).toEqual(true); + expect(hideSidebarStub.calledWith(app.$membersContainer)).toEqual(true); + showSidebarStub.restore(); + hideSidebarStub.restore(); + }); + + it('destroys last members view if deselected', function() { + app.listView.trigger('select', collection.at(0)); + app.listView.trigger('select', null); + + expect(membersViewStub.remove.calledOnce).toEqual(true); + }); + it('destroys last member view if switching selection', function() { + app.listView.trigger('select', collection.at(0)); + app.listView.trigger('select', collection.at(1)); + + expect(membersViewStub.remove.calledOnce).toEqual(true); + }); + }); +}); diff --git a/tests/js/GroupModelSpec.js b/tests/js/GroupModelSpec.js new file mode 100644 index 00000000..fd09262e --- /dev/null +++ b/tests/js/GroupModelSpec.js @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2017 Vincent Petry + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +describe('GroupModel test', function() { + var GroupModel = OCA.CustomGroups.GroupModel; + var GroupsCollection = OCA.CustomGroups.GroupsCollection; + + it('returns the groups endpoint url with group name', function() { + var model = new GroupModel({id: 'group@1囧'}); + + expect(model.url()).toEqual(OC.linkToRemote('dav') + '/customgroups/groups/group%401%E5%9B%A7'); + }); + + it('removes itself from groups list if current user leaves the group', function() { + var groupId = 'group@1囧'; + var userId = 'user@example.com'; + var collection = new GroupsCollection([ + { + id: groupId, + displayName: 'group1呵呵' + } + ], {userId: userId}); + + var subCollection = collection.at(0).getChildrenCollection(); + var memberModel = new OCA.CustomGroups.MemberModel({ + id: userId + }); + // add manually, this would usually appear through fetching + subCollection.add(memberModel); + + expect(collection.get(groupId)).toBeDefined(); + + // now remove self from the member list (leave group) + subCollection.remove(userId); + + expect(collection.get(groupId)).not.toBeDefined(); + }); + +}); diff --git a/tests/js/GroupsCollectionSpec.js b/tests/js/GroupsCollectionSpec.js new file mode 100644 index 00000000..1d709aa0 --- /dev/null +++ b/tests/js/GroupsCollectionSpec.js @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2017 Vincent Petry + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +describe('GroupsCollection test', function() { + var GroupsCollection = OCA.CustomGroups.GroupsCollection; + + describe('url', function() { + it('returns the groups endpoint url if no user id was given (superadmin mode)', function() { + var collection = new GroupsCollection(); + + expect(collection.url()).toEqual(OC.linkToRemote('dav') + '/customgroups/groups/'); + }); + it('returns the users endpoint url if a user id was given', function() { + var collection = new GroupsCollection([], {userId: 'someuser0@_.@-'}); + + expect(collection.url()).toEqual(OC.linkToRemote('dav') + '/customgroups/users/someuser0%40_.%40-/'); + }); + }); + +}); diff --git a/tests/js/GroupsViewSpec.js b/tests/js/GroupsViewSpec.js new file mode 100644 index 00000000..947a7fae --- /dev/null +++ b/tests/js/GroupsViewSpec.js @@ -0,0 +1,346 @@ +/* + * Copyright (c) 2017 Vincent Petry + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +describe('GroupsView test', function() { + var view; + var collection; + + var imageplaceholderStub; + + beforeEach(function() { + imageplaceholderStub = sinon.stub($.fn, 'imageplaceholder'); + + collection = new OCA.CustomGroups.GroupsCollection(); + view = new OCA.CustomGroups.GroupsView(collection); + }); + afterEach(function() { + view.remove(); + view = null; + collection = null; + + imageplaceholderStub.restore(); + }); + + describe('rendering', function() { + beforeEach(function() { + view.render(); + }); + + it('renders empty list at first', function() { + expect(view.$('.group-list').length).toBeDefined(); + }); + + it('renders groups as they are added', function() { + collection.add([{ + id: 'group1', + displayName: 'Group One', + role: OCA.CustomGroups.ROLE_ADMIN + }, { + id: 'group2', + displayName: 'Group Two', + role: OCA.CustomGroups.ROLE_MEMBER + }]); + + expect(view.$('.group').length).toEqual(2); + expect(view.$('.group:eq(0) .group-display-name').text()).toEqual('Group One'); + expect(view.$('.group:eq(0) .role-display-name').text()).toEqual(t('customgroups', 'Group admin')); + expect(view.$('.group:eq(1) .group-display-name').text()).toEqual('Group Two'); + expect(view.$('.group:eq(1) .role-display-name').text()).toEqual(t('customgroups', 'Member')); + + // avatar + expect(imageplaceholderStub.calledTwice).toEqual(true); + expect(imageplaceholderStub.getCall(0).thisValue.get(0)).toEqual(view.$('.group:eq(0) .avatar').get(0)); + expect(imageplaceholderStub.getCall(0).args[0]).toEqual('group1:Group One'); + expect(imageplaceholderStub.getCall(0).args[1]).toEqual('Group One'); + expect(imageplaceholderStub.getCall(1).thisValue.get(0)).toEqual(view.$('.group:eq(1) .avatar').get(0)); + expect(imageplaceholderStub.getCall(1).args[0]).toEqual('group2:Group Two'); + expect(imageplaceholderStub.getCall(1).args[1]).toEqual('Group Two'); + }); + + it('renders admin actions when role is group admin', function() { + collection.add({ + id: 'group1', + displayName: 'Group One', + role: OCA.CustomGroups.ROLE_ADMIN + }); + + expect(view.$('.group:eq(0) .action-rename-group').length).toEqual(1); + expect(view.$('.group:eq(0) .action-delete-group').length).toEqual(1); + }); + it('does not renders admin actions when role is member', function() { + collection.add({ + id: 'group1', + displayName: 'Group One', + role: OCA.CustomGroups.ROLE_MEMBER + }); + + expect(view.$('.group:eq(0) .action-rename-group').length).toEqual(0); + expect(view.$('.group:eq(0) .action-delete-group').length).toEqual(0); + }); + + it('rerenders row when group changed', function() { + var group1 = collection.add({ + id: 'group1', + displayName: 'Group One', + role: OCA.CustomGroups.ROLE_ADMIN + }); + collection.add({ + id: 'group2', + displayName: 'Group Two', + role: OCA.CustomGroups.ROLE_MEMBER + }); + + group1.set({ + displayName: 'Group Renamed' + }); + + expect(view.$('.group').length).toEqual(2); + expect(view.$('.group:eq(0) .group-display-name').text()).toEqual('Group Renamed'); + expect(view.$('.group:eq(1) .group-display-name').text()).toEqual('Group Two'); + }); + + it('removes row when group deleted', function() { + var group1 = collection.add({ + id: 'group1', + displayName: 'Group One', + role: OCA.CustomGroups.ROLE_ADMIN + }); + var group2 = collection.add({ + id: 'group2', + displayName: 'Group Two', + role: OCA.CustomGroups.ROLE_MEMBER + }); + + + collection.remove(group1); + expect(view.$('.group').length).toEqual(1); + expect(view.$('.group:eq(0) .group-display-name').text()).toEqual('Group Two'); + + collection.remove(group2); + expect(view.$('.group').length).toEqual(0); + }); + + it('updates hidden message when needed', function() { + var group1 = collection.add({ + id: 'group1', + displayName: 'Group One', + role: OCA.CustomGroups.ROLE_ADMIN + }); + + expect(view.$('.empty').hasClass('hidden')).toEqual(true); + expect(view.$('.grid').hasClass('hidden')).toEqual(false); + + collection.remove(group1); + + expect(view.$('.empty').hasClass('hidden')).toEqual(false); + expect(view.$('.grid').hasClass('hidden')).toEqual(true); + }); + }); + + describe('selection', function() { + beforeEach(function() { + view.render(); + collection.add({ + id: 'group1', + displayName: 'Group One', + role: OCA.CustomGroups.ROLE_ADMIN + }); + collection.add({ + id: 'group2', + displayName: 'Group Two', + role: OCA.CustomGroups.ROLE_MEMBER + }); + }); + + it('updates "active" class when group is selected', function() { + var $row1 = view.$('.group:eq(0)'); + var $row2 = view.$('.group:eq(1)'); + + expect($row1.hasClass('active')).toEqual(false); + expect($row2.hasClass('active')).toEqual(false); + + $row1.click(); + expect($row1.hasClass('active')).toEqual(true); + expect($row2.hasClass('active')).toEqual(false); + + $row2.click(); + expect($row1.hasClass('active')).toEqual(false); + expect($row2.hasClass('active')).toEqual(true); + }); + + it('triggers "select" event when group is clicked', function() { + var handler = sinon.stub(); + view.on('select', handler); + + view.$('.group:eq(1)').click(); + + expect(handler.calledOnce).toEqual(true); + expect(handler.calledWith(collection.at(1))).toEqual(true); + }); + + it('triggers "select" event with null when selected group is deleted', function() { + view.$('.group:eq(1)').click(); + + var handler = sinon.stub(); + view.on('select', handler); + + collection.remove('group2'); + + expect(handler.calledOnce).toEqual(true); + expect(handler.calledWith(null)).toEqual(true); + }); + }); + + describe('creating groups', function() { + beforeEach(function() { + view.remove(); + collection = sinon.createStubInstance(OCA.CustomGroups.GroupsCollection); + view = new OCA.CustomGroups.GroupsView(collection); + view.render(); + }); + + it('creates group into collection and selects it', function() { + view.select = sinon.stub(); + + view.$('[name=groupName]').val('newgroup'); + view.$('[name=customGroupsCreationForm]').submit(); + + expect(collection.create.calledOnce).toEqual(true); + expect(collection.create.getCall(0).args[0]).toEqual({ + id: 'newgroup', + displayName: 'newgroup', + role: OCA.CustomGroups.ROLE_ADMIN + }); + + var model = new OCA.CustomGroups.GroupModel({id: 'newgroup'}); + collection.create.yieldTo('success', model); + + expect(view.$('[name=groupName]').val()).toEqual(''); + expect(view.select.calledOnce).toEqual(true); + expect(view.select.calledWith(model)).toEqual(true); + }); + + it('attempts multiple uris in case of name conflict', function() { + view.$('[name=groupName]').val('newgroup'); + view.$('[name=customGroupsCreationForm]').submit(); + + expect(collection.create.calledOnce).toEqual(true); + expect(collection.create.getCall(0).args[0]).toEqual({ + id: 'newgroup', + displayName: 'newgroup', + role: OCA.CustomGroups.ROLE_ADMIN + }); + + collection.create.yieldTo('error', collection, {status: 405} ); + + expect(collection.create.calledTwice).toEqual(true); + expect(collection.create.getCall(1).args[0]).toEqual({ + id: 'newgroup2', + displayName: 'newgroup', + role: OCA.CustomGroups.ROLE_ADMIN + }); + }); + + it('shows notification in case of error', function() { + var notificationStub = sinon.stub(OC.Notification, 'showTemporary'); + view.$('[name=groupName]').val('newgroup'); + view.$('[name=customGroupsCreationForm]').submit(); + + expect(collection.create.calledOnce).toEqual(true); + expect(collection.create.getCall(0).args[0]).toEqual({ + id: 'newgroup', + displayName: 'newgroup', + role: OCA.CustomGroups.ROLE_ADMIN + }); + + collection.create.yieldTo('error', collection, {status: 404} ); + + expect(notificationStub.calledOnce).toEqual(true); + + notificationStub.restore(); + }); + }); + + describe('actions', function() { + var promptStub; + var confirmStub; + var model; + + beforeEach(function() { + promptStub = sinon.stub(window, 'prompt'); + confirmStub = sinon.stub(window, 'confirm'); + + view.render(); + model = collection.add({ + id: 'group1', + displayName: 'Group One', + role: OCA.CustomGroups.ROLE_ADMIN + }); + }); + afterEach(function() { + promptStub.restore(); + confirmStub.restore(); + }); + + describe('rename group', function() { + beforeEach(function() { + model.save = sinon.stub(); + }); + + it('saves model with new name on confirm', function() { + promptStub.returns('Group Renamed'); + view.$('.group:eq(0) .action-rename-group').click(); + expect(model.save.calledOnce).toEqual(true); + expect(model.save.getCall(0).args[0]).toEqual({ + displayName: 'Group Renamed' + }); + + }); + it('does not save model on abort', function() { + promptStub.returns(null); + view.$('.group:eq(0) .action-rename-group').click(); + expect(model.save.notCalled).toEqual(true); + }); + }); + describe('delete group', function() { + beforeEach(function() { + model.destroy = sinon.stub(); + }); + + it('destroy model with on confirm', function() { + confirmStub.returns(true); + view.$('.group:eq(0) .action-delete-group').click(); + expect(model.destroy.calledOnce).toEqual(true); + }); + it('does not destroy model on abort', function() { + confirmStub.returns(false); + view.$('.group:eq(0) .action-delete-group').click(); + expect(model.destroy.notCalled).toEqual(true); + }); + }); + }); + + describe('loading', function() { + it('displays spinner while fetching', function() { + view.render(); + + collection.sync = sinon.spy(collection, 'sync'); + + collection.fetch(); + + expect(view.$('.loading').hasClass('hidden')).toEqual(false); + + expect(collection.sync.calledOnce).toEqual(true); + collection.sync.yieldTo('success'); + + expect(view.$('.loading').hasClass('hidden')).toEqual(true); + }); + }); +}); diff --git a/tests/js/MemberModelSpec.js b/tests/js/MemberModelSpec.js new file mode 100644 index 00000000..405b812d --- /dev/null +++ b/tests/js/MemberModelSpec.js @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2017 Vincent Petry + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +describe('MemberModel test', function() { + var GroupModel = OCA.CustomGroups.GroupModel; + + it('returns the groups endpoint url with group name', function() { + var groupModel = new GroupModel({id: 'group@1囧'}); + + var subCollection = groupModel.getChildrenCollection(); + var memberModel = subCollection.add({ + id: 'user1@example.com' + }); + + expect(memberModel.url()) + .toEqual(OC.linkToRemote('dav') + '/customgroups/groups/group%401%E5%9B%A7/user1%40example.com'); + }); + +}); diff --git a/tests/js/MembersCollectionSpec.js b/tests/js/MembersCollectionSpec.js new file mode 100644 index 00000000..0c9c49b5 --- /dev/null +++ b/tests/js/MembersCollectionSpec.js @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2017 Vincent Petry + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +describe('MembersCollection test', function() { + var GroupModel = OCA.CustomGroups.GroupModel; + + it('returns the groups endpoint url with group name', function() { + var groupModel = new GroupModel({id: 'group@1囧'}); + + var subCollection = groupModel.getChildrenCollection(); + + expect(subCollection.url()) + .toEqual(OC.linkToRemote('dav') + '/customgroups/groups/group%401%E5%9B%A7/'); + }); + +}); diff --git a/tests/js/MembersViewSpec.js b/tests/js/MembersViewSpec.js new file mode 100644 index 00000000..2ee20543 --- /dev/null +++ b/tests/js/MembersViewSpec.js @@ -0,0 +1,334 @@ +/* + * Copyright (c) 2017 Vincent Petry + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +describe('MembersView test', function() { + var view; + var collection; + var model; + + var imageplaceholderStub; + var avatarStub; + + var currentUserStub; + + beforeEach(function() { + imageplaceholderStub = sinon.stub($.fn, 'imageplaceholder'); + avatarStub = sinon.stub($.fn, 'avatar'); + currentUserStub = sinon.stub(OC, 'getCurrentUser').returns({uid: 'currentUser'}); + + model = new OCA.CustomGroups.GroupModel({ + id: 'group1', + displayName: 'Group One', + role: OCA.CustomGroups.ROLE_ADMIN + }); + collection = model.getChildrenCollection(); + view = new OCA.CustomGroups.MembersView(model); + }); + afterEach(function() { + view.remove(); + view = null; + collection = null; + + imageplaceholderStub.restore(); + avatarStub.restore(); + currentUserStub.restore(); + }); + + describe('rendering', function() { + beforeEach(function() { + view.render(); + }); + + it('renders empty list at first', function() { + expect(view.$('.group-member-list').length).toBeDefined(); + }); + + it('renders header', function() { + expect(view.$('.group-name-title-display').text()).toEqual('Group One'); + + expect(imageplaceholderStub.calledOnce).toEqual(true); + expect(imageplaceholderStub.getCall(0).thisValue.get(0)) + .toEqual(view.$('.group-name-title .avatar').get(0)); + expect(imageplaceholderStub.getCall(0).args[0]).toEqual('group1:Group One'); + expect(imageplaceholderStub.getCall(0).args[1]).toEqual('Group One'); + }); + + it('rerenders header if group display name changed', function() { + model.set({displayName: 'Group Renamed'}); + expect(view.$('.group-name-title-display').text()).toEqual('Group Renamed'); + }); + + it('renders members as they are added', function() { + collection.add([{ + id: 'user1', + role: OCA.CustomGroups.ROLE_ADMIN + }, { + id: 'user2', + role: OCA.CustomGroups.ROLE_MEMBER + }]); + + expect(view.$('.group-member').length).toEqual(2); + expect(view.$('.group-member:eq(0) .user-display-name').text()).toEqual('user1'); + expect(view.$('.group-member:eq(0) .role-display-name').text()).toEqual(t('customgroups', 'Group admin')); + expect(view.$('.group-member:eq(1) .user-display-name').text()).toEqual('user2'); + expect(view.$('.group-member:eq(1) .role-display-name').text()).toEqual(t('customgroups', 'Member')); + + // avatar + expect(avatarStub.calledTwice).toEqual(true); + expect(avatarStub.getCall(0).thisValue.get(0)).toEqual(view.$('.group-member:eq(0) .avatar').get(0)); + expect(avatarStub.getCall(0).args[0]).toEqual('user1'); + expect(avatarStub.getCall(0).args[1]).toEqual(32); + expect(avatarStub.getCall(1).thisValue.get(0)).toEqual(view.$('.group-member:eq(1) .avatar').get(0)); + expect(avatarStub.getCall(1).args[0]).toEqual('user2'); + expect(avatarStub.getCall(1).args[1]).toEqual(32); + }); + + it('renders admin actions when user\'s role in group is group admin', function() { + model.set({ + role: OCA.CustomGroups.ROLE_ADMIN + }); + collection.add({ + id: 'user1', + displayName: 'Group One', + role: OCA.CustomGroups.ROLE_ADMIN + }); + + expect(view.$('.group-member:eq(0) .action-change-member-role').length).toEqual(1); + expect(view.$('.group-member:eq(0) .action-delete-member').length).toEqual(1); + }); + it('does not renders admin actions when role is member', function() { + model.set({ + role: OCA.CustomGroups.ROLE_MEMBER + }); + collection.add({ + id: 'user2', + displayName: 'Group One', + role: OCA.CustomGroups.ROLE_ADMIN + }); + + expect(view.$('.group-member:eq(0) .action-change-member-role').length).toEqual(0); + expect(view.$('.group-member:eq(0) .action-delete-member').length).toEqual(0); + }); + + it('rerenders row when member role changed', function() { + var group1 = collection.add({ + id: 'user1', + role: OCA.CustomGroups.ROLE_ADMIN + }); + collection.add({ + id: 'user2', + role: OCA.CustomGroups.ROLE_MEMBER + }); + + group1.set({ + role: OCA.CustomGroups.ROLE_MEMBER + }); + + expect(view.$('.group-member').length).toEqual(2); + expect(view.$('.group-member:eq(0) .role-display-name').text()).toEqual(t('customgroups', 'Member')); + expect(view.$('.group-member:eq(1) .role-display-name').text()).toEqual(t('customgroups', 'Member')); + }); + + it('removes row when member deleted', function() { + var member1 = collection.add({ + id: 'user1', + role: OCA.CustomGroups.ROLE_ADMIN + }); + var member2 = collection.add({ + id: 'user2', + role: OCA.CustomGroups.ROLE_MEMBER + }); + + collection.remove(member1); + expect(view.$('.group-member').length).toEqual(1); + expect(view.$('.group-member:eq(0) .user-display-name').text()).toEqual('user2'); + + collection.remove(member2); + expect(view.$('.group-member').length).toEqual(0); + }); + }); + + describe('adding members', function() { + beforeEach(function() { + view.remove(); + + model = new OCA.CustomGroups.GroupModel({ + id: 'group1', + role: OCA.CustomGroups.ROLE_ADMIN + }); + collection = sinon.createStubInstance(OCA.CustomGroups.MembersCollection); + model.getChildrenCollection = sinon.stub().returns(collection); + + view = new OCA.CustomGroups.MembersView(model); + view.render(); + }); + + it('does not render form if the current user cannot admin this group', function() { + model.set({role: OCA.CustomGroups.ROLE_MEMBER}); + view.render(); + expect(view.$('[name=customGroupsAddMemberForm]').length).toEqual(0); + + model.set({role: OCA.CustomGroups.ROLE_ADMIN}); + view.render(); + expect(view.$('[name=customGroupsAddMemberForm]').length).toEqual(1); + }); + + it('creates member into collection', function() { + view.$('[name=memberUserId]').val('newuser'); + view.$('[name=customGroupsAddMemberForm]').submit(); + expect(collection.create.calledOnce).toEqual(true); + expect(collection.create.getCall(0).args[0]).toEqual({ + id: 'newuser' + }); + + collection.create.yieldTo('success'); + expect(view.$('[name=memberUserId]').val()).toEqual(''); + }); + + it('shows notification in case of error', function() { + var notificationStub = sinon.stub(OC.Notification, 'showTemporary'); + view.$('[name=memberUserId]').val('newuser'); + view.$('[name=customGroupsAddMemberForm]').submit(); + + expect(collection.create.calledOnce).toEqual(true); + collection.create.yieldTo('error', collection, {status: 412} ); + + expect(notificationStub.calledOnce).toEqual(true); + + notificationStub.restore(); + }); + }); + + describe('actions', function() { + var confirmStub; + var currentUserModel; + + beforeEach(function() { + view.render(); + confirmStub = sinon.stub(window, 'confirm'); + currentUserModel = collection.add({ + id: 'currentUser', + role: OCA.CustomGroups.ROLE_ADMIN + }); + currentUserModel.destroy = sinon.stub(); + collection.add({ + id: 'anotherAdmin', + role: OCA.CustomGroups.ROLE_ADMIN + }); + collection.add({ + id: 'anotherMember', + role: OCA.CustomGroups.ROLE_MEMBER + }); + }); + afterEach(function() { + confirmStub.restore(); + }); + + describe('leaving group', function() { + it('asks for confirmation before leaving group when clicking specific button', function() { + confirmStub.returns(true); + view.$('.action-leave-group').click(); + + expect(confirmStub.calledOnce).toEqual(true); + expect(confirmStub.getCall(0).args[0]).toContain('leaving'); + expect(currentUserModel.destroy.calledOnce).toEqual(true); + }); + it('asks for confirmation before leaving group when deleting self from list', function() { + confirmStub.returns(true); + view.$('.group-member:eq(0) .action-delete-member').click(); + + expect(confirmStub.calledOnce).toEqual(true); + expect(confirmStub.getCall(0).args[0]).toContain('leaving'); + expect(currentUserModel.destroy.calledOnce).toEqual(true); + }); + it('does not delete if aborted', function() { + confirmStub.returns(false); + view.$('.action-leave-group').click(); + + expect(currentUserModel.destroy.notCalled).toEqual(true); + }); + }); + + describe('deleting members', function() { + var model; + + beforeEach(function() { + model = collection.at(1); + + model.destroy = sinon.stub(); + }); + + it('asks for confirmation when deleting member', function() { + confirmStub.returns(true); + view.$('.group-member:eq(1) .action-delete-member').click(); + + expect(confirmStub.calledOnce).toEqual(true); + expect(model.destroy.calledOnce).toEqual(true); + }); + it('does not delete if aborted', function() { + confirmStub.returns(false); + view.$('.group-member:eq(1) .action-delete-member').click(); + + expect(model.destroy.notCalled).toEqual(true); + }); + }); + + describe('changing roles', function() { + var anotherAdmin; + var anotherMember; + + beforeEach(function() { + anotherAdmin = collection.at(1); + anotherMember = collection.at(2); + }); + + it('switches role when clicking change role', function() { + view.$('.group-member:eq(1) .action-change-member-role').click(); + expect(anotherAdmin.get('role')).toEqual(OCA.CustomGroups.ROLE_MEMBER); + + view.$('.group-member:eq(2) .action-change-member-role').click(); + expect(anotherMember.get('role')).toEqual(OCA.CustomGroups.ROLE_ADMIN); + + expect(confirmStub.notCalled).toEqual(true); + }); + it('asks for confirmation before removing own admin powers', function() { + confirmStub.returns(true); + view.$('.group-member:eq(0) .action-change-member-role').click(); + expect(confirmStub.calledOnce).toEqual(true); + + expect(currentUserModel.get('role')).toEqual(OCA.CustomGroups.ROLE_MEMBER); + }); + it('does not remove admin role if aborted', function() { + view.$('.group-member:eq(0) .action-change-member-role').click(); + confirmStub.returns(false); + expect(confirmStub.calledOnce).toEqual(true); + + expect(currentUserModel.get('role')).toEqual(OCA.CustomGroups.ROLE_ADMIN); + }); + }); + }); + + describe('loading', function() { + it('displays spinner while fetching', function() { + view.render(); + + collection.sync = sinon.spy(collection, 'sync'); + + collection.fetch(); + + expect(view.$('.loading').hasClass('hidden')).toEqual(false); + + expect(collection.sync.calledOnce).toEqual(true); + collection.sync.yieldTo('success'); + + expect(view.$('.loading').hasClass('hidden')).toEqual(true); + }); + }); +}); diff --git a/tests/js/karma.config.js b/tests/js/karma.config.js new file mode 100644 index 00000000..48672467 --- /dev/null +++ b/tests/js/karma.config.js @@ -0,0 +1,117 @@ +/** +* ownCloud +* +* @author Vincent Petry +* @copyright 2016 Vincent Petry +* +* This library is free software; you can redistribute it and/or +* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE +* License as published by the Free Software Foundation; either +* version 3 of the License, or any later version. +* +* This library is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU AFFERO GENERAL PUBLIC LICENSE for more details. +* +* You should have received a copy of the GNU Affero General Public +* License along with this library. If not, see . +* +*/ + +/** + * This node module is run by the karma executable to specify its configuration. + */ + +/* jshint node: true */ +module.exports = function(config) { + var _ = require('underscore'); + var basePath = '../../'; + var ownCloudPath = '../../'; + + // can't use wildcard due to loading order + var srcFiles = _.map(require(basePath + 'js/modules.json'), function(name) { + return 'js/' + name + '.js'; + }); + + var testFiles = [ + 'tests/js/*.js' + ]; + + var coreModules = require(ownCloudPath + '../../core/js/core.json'); + var coreLibs = [ + ownCloudPath + 'core/js/tests/lib/sinon-1.15.4.js', + ownCloudPath + 'core/js/tests/specHelper.js' + ]; + + coreLibs = coreLibs.concat(coreModules.vendor.map(function prependPath(path) { + return ownCloudPath + 'core/vendor/' + path; + })); + + coreLibs = coreLibs.concat(coreModules.modules.map(function prependPath(path) { + return ownCloudPath + 'core/js/' + path; + })); + + coreLibs = coreLibs.concat(coreModules.libraries.map(function prependPath(path) { + return ownCloudPath + 'core/js/' + path; + })); + + var files = [].concat(coreLibs, srcFiles, testFiles); + + config.set({ + + // base path, that will be used to resolve files and exclude + basePath: basePath, + + // frameworks to use + frameworks: ['jasmine', 'jasmine-sinon'], + + // list of files / patterns to load in the browser + files: files, + + // list of files to exclude + exclude: [ + + ], + + proxies: { + }, + + // test results reporter to use + // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' + reporters: ['progress'], + + junitReporter: { + outputFile: 'tests/js/results-js.xml' + }, + + // web server port + port: 9876, + + // enable / disable colors in the output (reporters and logs) + colors: true, + + // level of logging + logLevel: config.LOG_INFO, + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: true, + + // Start these browsers, currently available: + // - Chrome + // - ChromeCanary + // - Firefox + // - Opera (has to be installed with `npm install karma-opera-launcher`) + // - Safari (only Mac; has to be installed with `npm install karma-safari-launcher`) + // - PhantomJS + // - IE (only Windows; has to be installed with `npm install karma-ie-launcher`) + browsers: ['PhantomJS'], + + // If browser does not capture in given timeout [ms], kill it + captureTimeout: 60000, + + // Continuous Integration mode + // if true, it capture browsers, run tests and exit + singleRun: false + }); +}; diff --git a/tests/unit/Dav/MembershipNodeTest.php b/tests/unit/Dav/MembershipNodeTest.php index 9b1ee91e..a2cfbc7a 100644 --- a/tests/unit/Dav/MembershipNodeTest.php +++ b/tests/unit/Dav/MembershipNodeTest.php @@ -298,10 +298,6 @@ public function propsProvider() { MembershipNode::PROPERTY_USER_ID, self::NODE_USER, ], - [ - MembershipNode::PROPERTY_GROUP_URI, - 'group1', - ], ]; } diff --git a/tests/unit/phpunit.xml b/tests/unit/phpunit.xml index 8fad4d2e..06a0bc7a 100644 --- a/tests/unit/phpunit.xml +++ b/tests/unit/phpunit.xml @@ -17,9 +17,5 @@ ../../lib - - - - diff --git a/tools/.gitkeep b/tools/.gitkeep new file mode 100644 index 00000000..e69de29b