Compare commits

...

7 commits

58 changed files with 29552 additions and 611 deletions

5
.eslintrc.js Normal file
View file

@ -0,0 +1,5 @@
module.exports = {
extends: [
'@nextcloud',
]
}

3
.gitattributes vendored Normal file
View file

@ -0,0 +1,3 @@
/js/* binary
*.png -text
*.epgz -text

24
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,24 @@
version: 2
updates:
- package-ecosystem: composer
directory: "/"
schedule:
interval: weekly
day: saturday
time: "03:00"
timezone: Europe/Paris
open-pull-requests-limit: 10
labels:
- 3. to review
- dependencies
- package-ecosystem: npm
directory: "/"
schedule:
interval: weekly
day: saturday
time: "03:00"
timezone: Europe/Paris
open-pull-requests-limit: 10
labels:
- 3. to review
- dependencies

117
.github/workflows/command-compile.yml vendored Normal file
View file

@ -0,0 +1,117 @@
name: Compile Command
on:
issue_comment:
types: [created]
jobs:
init:
runs-on: ubuntu-latest
# On pull requests and if the comment starts with `/compile`
if: github.event.issue.pull_request != '' && startsWith(github.event.comment.body, '/compile')
outputs:
git_path: ${{ steps.git-path.outputs.path }}
arg1: ${{ steps.command.outputs.arg1 }}
arg2: ${{ steps.command.outputs.arg2 }}
head_ref: ${{ steps.comment-branch.outputs.head_ref }}
steps:
- name: Check actor permission
uses: skjnldsv/check-actor-permission@v2
with:
require: write
- name: Add reaction on start
uses: peter-evans/create-or-update-comment@v1
with:
token: ${{ secrets.COMMAND_BOT_PAT }}
repository: ${{ github.event.repository.full_name }}
comment-id: ${{ github.event.comment.id }}
reaction-type: "+1"
- name: Parse command
uses: skjnldsv/parse-command-comment@master
id: command
# Init path depending on which command is run
- name: Init path
id: git-path
run: |
if ${{ startsWith(steps.command.outputs.arg1, '/') }}; then
echo "::set-output name=path::${{ github.workspace }}${{steps.command.outputs.arg1}}"
else
echo "::set-output name=path::${{ github.workspace }}${{steps.command.outputs.arg2}}"
fi
- name: Init branch
uses: xt0rted/pull-request-comment-branch@v1
id: comment-branch
process:
runs-on: ubuntu-latest
needs: init
steps:
- name: Checkout ${{ needs.init.outputs.head_ref }}
uses: actions/checkout@v2
with:
token: ${{ secrets.COMMAND_BOT_PAT }}
fetch-depth: 0
ref: ${{ needs.init.outputs.head_ref }}
- name: Setup git
run: |
git config --local user.email "nextcloud-command@users.noreply.github.com"
git config --local user.name "nextcloud-command"
- name: Read package.json node and npm engines version
uses: skjnldsv/read-package-engines-version-actions@v1
id: package-engines-versions
with:
fallbackNode: '^12'
fallbackNpm: '^6'
- name: Set up node ${{ steps.package-engines-versions.outputs.nodeVersion }}
uses: actions/setup-node@v2
with:
node-version: ${{ steps.package-engines-versions.outputs.nodeVersion }}
cache: npm
- name: Set up npm ${{ steps.package-engines-versions.outputs.npmVersion }}
run: npm i -g npm@"${{ steps.package-engines-versions.outputs.npmVersion }}"
- name: Install dependencies & build
run: |
npm ci
npm run build --if-present
- name: Commit and push default
if: ${{ needs.init.outputs.arg1 != 'fixup' && needs.init.outputs.arg1 != 'amend' }}
run: |
git add ${{ needs.init.outputs.git_path }}
git commit --signoff -m 'Compile assets'
git push origin ${{ needs.init.outputs.head_ref }}
- name: Commit and push fixup
if: ${{ needs.init.outputs.arg1 == 'fixup' }}
run: |
git add ${{ needs.init.outputs.git_path }}
git commit --fixup=HEAD --signoff
git push origin ${{ needs.init.outputs.head_ref }}
- name: Commit and push amend
if: ${{ needs.init.outputs.arg1 == 'amend' }}
run: |
git add ${{ needs.init.outputs.git_path }}
git commit --amend --no-edit --signoff
git push --force origin ${{ needs.init.outputs.head_ref }}
- name: Add reaction on failure
uses: peter-evans/create-or-update-comment@v1
if: failure()
with:
token: ${{ secrets.COMMAND_BOT_PAT }}
repository: ${{ github.event.repository.full_name }}
comment-id: ${{ github.event.comment.id }}
reaction-type: "-1"

46
.github/workflows/command-rebase.yml vendored Normal file
View file

@ -0,0 +1,46 @@
# This workflow is provided via the organization template repository
#
# https://github.com/nextcloud/.github
# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
name: Rebase command
on:
issue_comment:
types: created
jobs:
rebase:
runs-on: ubuntu-latest
# On pull requests and if the comment starts with `/rebase`
if: github.event.issue.pull_request != '' && startsWith(github.event.comment.body, '/rebase')
steps:
- name: Add reaction on start
uses: peter-evans/create-or-update-comment@v1
with:
token: ${{ secrets.COMMAND_BOT_PAT }}
repository: ${{ github.event.repository.full_name }}
comment-id: ${{ github.event.comment.id }}
reaction-type: "+1"
- name: Checkout the latest code
uses: actions/checkout@v2
with:
fetch-depth: 0
token: ${{ secrets.COMMAND_BOT_PAT }}
- name: Automatic Rebase
uses: cirrus-actions/rebase@1.5
env:
GITHUB_TOKEN: ${{ secrets.COMMAND_BOT_PAT }}
- name: Add reaction on failure
uses: peter-evans/create-or-update-comment@v1
if: failure()
with:
token: ${{ secrets.COMMAND_BOT_PAT }}
repository: ${{ github.event.repository.full_name }}
comment-id: ${{ github.event.comment.id }}
reaction-type: "-1"

View file

@ -0,0 +1,29 @@
# This workflow is provided via the organization template repository
#
# https://github.com/nextcloud/.github
# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
name: Dependabot
on:
pull_request_target:
branches:
- master
- stable*
jobs:
auto-merge:
runs-on: ubuntu-latest
steps:
# Default github action approve
- uses: hmarr/auto-approve-action@v2
if: github.actor == 'dependabot[bot]' || github.actor == 'dependabot-preview[bot]'
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
# Nextcloud bot approve and merge request
- uses: ahmadnassri/action-dependabot-auto-merge@v2
if: github.actor == 'dependabot[bot]' || github.actor == 'dependabot-preview[bot]'
with:
target: minor
github-token: ${{ secrets.DEPENDABOT_AUTOMERGE_TOKEN }}

12
.github/workflows/fixup.yml vendored Normal file
View file

@ -0,0 +1,12 @@
name: Pull request checks
on: pull_request
jobs:
commit-message-check:
name: Block fixup and squash commits
runs-on: ubuntu-latest
steps:
- name: Run check
uses: xt0rted/block-autosquash-commits-action@main
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}

111
.github/workflows/lint.yml vendored Normal file
View file

@ -0,0 +1,111 @@
name: Lint
on:
pull_request:
push:
branches:
- master
- stable*
jobs:
php:
runs-on: ubuntu-latest
strategy:
matrix:
php-versions: ["7.3", "7.4", "8.0"]
name: php${{ matrix.php-versions }}
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up php ${{ matrix.php-versions }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
coverage: none
- name: Lint
run: composer run lint
php-cs-fixer:
runs-on: ubuntu-latest
strategy:
matrix:
php-versions: ["7.4"]
name: cs php${{ matrix.php-versions }}
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up php
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
coverage: none
- name: Install dependencies
run: composer i
- name: Run coding standards check
run: composer run cs:check || ( echo 'Please run `composer run cs:fix` to format your code' && exit 1 )
node:
runs-on: ubuntu-latest
name: eslint node
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up node
uses: actions/setup-node@v2
with:
node-version: 14
- name: Set up npm7
run: npm i -g npm@7
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
stylelint:
runs-on: ubuntu-latest
name: stylelint node
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up node
uses: actions/setup-node@v2
with:
node-version: 14
- name: Set up npm7
run: npm i -g npm@7
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run stylelint
xml-linters:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@master
- name: Download schema
run: wget https://apps.nextcloud.com/schema/apps/info.xsd
- name: Lint info.xml
uses: ChristophWurst/xmllint-action@v1
with:
xml-file: ./appinfo/info.xml
xml-schema-file: ./info.xsd

52
.github/workflows/node.yml vendored Normal file
View file

@ -0,0 +1,52 @@
# This workflow is provided via the organization template repository
#
# https://github.com/nextcloud/.github
# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
name: Node
on:
pull_request:
push:
branches:
- master
- stable*
jobs:
build:
runs-on: ubuntu-latest
name: node
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Read package.json node and npm engines version
uses: skjnldsv/read-package-engines-version-actions@v1.1
id: versions
with:
fallbackNode: '^12'
fallbackNpm: '^6'
- name: Set up node ${{ steps.versions.outputs.nodeVersion }}
uses: actions/setup-node@v2
with:
node-version: ${{ steps.versions.outputs.nodeVersion }}
- name: Set up npm ${{ steps.versions.outputs.npmVersion }}
run: npm i -g npm@"${{ steps.versions.outputs.npmVersion }}"
- name: Install dependencies & build
run: |
npm ci
npm run build --if-present
- name: Check webpack build changes
run: |
bash -c "[[ ! \"`git status --porcelain `\" ]] || exit 1"
- name: Show changes on failure
if: failure()
run: |
git status
git --no-pager diff

215
.github/workflows/phpunit.yml vendored Normal file
View file

@ -0,0 +1,215 @@
name: PHPUnit
on:
pull_request:
push:
branches:
- master
- stable*
env:
APP_NAME: upschooling
jobs:
php:
runs-on: ubuntu-latest
strategy:
# do not stop on another job's failure
fail-fast: false
matrix:
php-versions: ['7.4']
databases: ['sqlite']
server-versions: ['master']
name: php${{ matrix.php-versions }}-${{ matrix.databases }}-${{ matrix.server-versions }}
steps:
- name: Checkout server
uses: actions/checkout@v2
with:
repository: nextcloud/server
ref: ${{ matrix.server-versions }}
- name: Checkout submodules
shell: bash
run: |
auth_header="$(git config --local --get http.https://github.com/.extraheader)"
git submodule sync --recursive
git -c "http.extraheader=$auth_header" -c protocol.version=2 submodule update --init --force --recursive --depth=1
- name: Checkout app
uses: actions/checkout@v2
with:
path: apps/${{ env.APP_NAME }}
- name: Set up php ${{ matrix.php-versions }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
tools: phpunit
extensions: mbstring, iconv, fileinfo, intl, sqlite, pdo_sqlite
coverage: none
- name: Set up PHPUnit
working-directory: apps/${{ env.APP_NAME }}
run: composer i
- name: Set up Nextcloud
env:
DB_PORT: 4444
run: |
mkdir data
./occ maintenance:install --verbose --database=${{ matrix.databases }} --database-name=nextcloud --database-host=127.0.0.1 --database-port=$DB_PORT --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass password
./occ app:enable --force ${{ env.APP_NAME }}
php -S localhost:8080 &
- name: PHPUnit
working-directory: apps/${{ env.APP_NAME }}
run: ./vendor/phpunit/phpunit/phpunit -c phpunit.xml
- name: PHPUnit integration
working-directory: apps/${{ env.APP_NAME }}
run: ./vendor/phpunit/phpunit/phpunit -c phpunit.integration.xml
mysql:
runs-on: ubuntu-latest
strategy:
# do not stop on another job's failure
fail-fast: false
matrix:
php-versions: ['7.3', '7.4']
databases: ['mysql']
server-versions: ['master']
name: php${{ matrix.php-versions }}-${{ matrix.databases }}-${{ matrix.server-versions }}
services:
mysql:
image: mariadb:10.5
ports:
- 4444:3306/tcp
env:
MYSQL_ROOT_PASSWORD: rootpassword
options: --health-cmd="mysqladmin ping" --health-interval 5s --health-timeout 2s --health-retries 5
steps:
- name: Checkout server
uses: actions/checkout@v2
with:
repository: nextcloud/server
ref: ${{ matrix.server-versions }}
- name: Checkout submodules
shell: bash
run: |
auth_header="$(git config --local --get http.https://github.com/.extraheader)"
git submodule sync --recursive
git -c "http.extraheader=$auth_header" -c protocol.version=2 submodule update --init --force --recursive --depth=1
- name: Checkout app
uses: actions/checkout@v2
with:
path: apps/${{ env.APP_NAME }}
- name: Set up php ${{ matrix.php-versions }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
tools: phpunit
extensions: mbstring, iconv, fileinfo, intl, mysql, pdo_mysql
coverage: none
- name: Set up PHPUnit
working-directory: apps/${{ env.APP_NAME }}
run: composer i
- name: Set up Nextcloud
env:
DB_PORT: 4444
run: |
mkdir data
./occ maintenance:install --verbose --database=${{ matrix.databases }} --database-name=nextcloud --database-host=127.0.0.1 --database-port=$DB_PORT --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass password
./occ app:enable --force ${{ env.APP_NAME }}
php -S localhost:8080 &
- name: PHPUnit
working-directory: apps/${{ env.APP_NAME }}
run: ./vendor/phpunit/phpunit/phpunit -c phpunit.xml
- name: PHPUnit integration
working-directory: apps/${{ env.APP_NAME }}
run: ./vendor/phpunit/phpunit/phpunit -c phpunit.integration.xml
pgsql:
runs-on: ubuntu-latest
strategy:
# do not stop on another job's failure
fail-fast: false
matrix:
php-versions: ['7.4']
databases: ['pgsql']
server-versions: ['master']
name: php${{ matrix.php-versions }}-${{ matrix.databases }}-${{ matrix.server-versions }}
services:
postgres:
image: postgres
ports:
- 4444:5432/tcp
env:
POSTGRES_USER: root
POSTGRES_PASSWORD: rootpassword
POSTGRES_DB: nextcloud
options: --health-cmd pg_isready --health-interval 5s --health-timeout 2s --health-retries 5
steps:
- name: Checkout server
uses: actions/checkout@v2
with:
repository: nextcloud/server
ref: ${{ matrix.server-versions }}
- name: Checkout submodules
shell: bash
run: |
auth_header="$(git config --local --get http.https://github.com/.extraheader)"
git submodule sync --recursive
git -c "http.extraheader=$auth_header" -c protocol.version=2 submodule update --init --force --recursive --depth=1
- name: Checkout app
uses: actions/checkout@v2
with:
path: apps/${{ env.APP_NAME }}
- name: Set up php ${{ matrix.php-versions }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
tools: phpunit
extensions: mbstring, iconv, fileinfo, intl, pgsql, pdo_pgsql
coverage: none
- name: Set up PHPUnit
working-directory: apps/${{ env.APP_NAME }}
run: composer i
- name: Set up Nextcloud
env:
DB_PORT: 4444
run: |
mkdir data
./occ maintenance:install --verbose --database=${{ matrix.databases }} --database-name=nextcloud --database-host=127.0.0.1 --database-port=$DB_PORT --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass password
./occ app:enable --force ${{ env.APP_NAME }}
php -S localhost:8080 &
- name: PHPUnit
working-directory: apps/${{ env.APP_NAME }}
run: ./vendor/phpunit/phpunit/phpunit -c phpunit.xml
- name: PHPUnit integration
working-directory: apps/${{ env.APP_NAME }}
run: ./vendor/phpunit/phpunit/phpunit -c phpunit.integration.xml

10
.gitignore vendored
View file

@ -1,6 +1,10 @@
/.idea /.idea/
/build *.iml
/build/
node_modules/
/.php_cs.cache
/js/
### Composer ### ### Composer ###
composer.phar composer.phar
/vendor /vendor/

17
.php_cs.dist Normal file
View file

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
require_once './vendor/autoload.php';
use Nextcloud\CodingStandard\Config;
$config = new Config();
$config
->getFinder()
->notPath('build')
->notPath('l10n')
->notPath('src')
->notPath('vendor')
->in(__DIR__);
return $config;

View file

@ -1,64 +0,0 @@
sudo: false
dist: trusty
language: php
php:
- 5.6
- 7
- 7.1
env:
global:
- CORE_BRANCH=stable15
matrix:
- DB=pgsql
matrix:
allow_failures:
- env: DB=pgsql CORE_BRANCH=master
include:
- php: 5.6
env: DB=sqlite
- php: 5.6
env: DB=mysql
- php: 5.6
env: DB=pgsql CORE_BRANCH=master
fast_finish: true
before_install:
# enable a display for running JavaScript tests
- export DISPLAY=:99.0
- sh -e /etc/init.d/xvfb start
- nvm install 8
- npm install -g npm@latest
- make
- make appstore
# install core
- cd ../
- git clone https://github.com/nextcloud/server.git --recursive --depth 1 -b $CORE_BRANCH nextcloud
- mv "$TRAVIS_BUILD_DIR" nextcloud/apps/upschooling
before_script:
- if [[ "$DB" == 'pgsql' ]]; then createuser -U travis -s oc_autotest; fi
- if [[ "$DB" == 'mysql' ]]; then mysql -u root -e 'create database oc_autotest;'; fi
- if [[ "$DB" == 'mysql' ]]; then mysql -u root -e "CREATE USER 'oc_autotest'@'localhost' IDENTIFIED BY '';"; fi
- if [[ "$DB" == 'mysql' ]]; then mysql -u root -e "grant all on oc_autotest.* to 'oc_autotest'@'localhost';"; fi
- cd nextcloud
- mkdir data
- ./occ maintenance:install --database-name oc_autotest --database-user oc_autotest --admin-user admin --admin-pass admin --database $DB --database-pass=''
- ./occ app:enable upschooling
- php -S localhost:8080 &
- cd apps/upschooling
script:
- make test
after_failure:
- cat ../../data/nextcloud.log
addons:
firefox: 'latest'
mariadb: '10.1'
services:
- postgresql
- mariadb

12
CHANGELOG.md Normal file
View file

@ -0,0 +1,12 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## [0.0.2] - 2017-07-31
### Added
- First release

View file

@ -658,4 +658,4 @@ specific requirements.
You should also get your employer (if you work as a programmer) or school, You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary. if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see For more information on this, and how to apply and follow the GNU AGPL, see
<http://www.gnu.org/licenses/>. <http://www.gnu.org/licenses/>.

172
Makefile
View file

@ -1,73 +1,17 @@
# This file is licensed under the Affero General Public License version 3 or # This file is licensed under the Affero General Public License version 3 or
# later. See the COPYING file. # later. See the COPYING file.
# @author Bernhard Posselt <dev@bernhard-posselt.com>
# @copyright Bernhard Posselt 2016
# Generic Makefile for building and packaging a Nextcloud app which uses npm and
# Composer.
#
# Dependencies:
# * make
# * which
# * curl: used if phpunit and composer are not installed to fetch them from the web
# * tar: for building the archive
# * npm: for building and testing everything JS
#
# If no composer.json is in the app root directory, the Composer step
# will be skipped. The same goes for the package.json which can be located in
# the app root or the js/ directory.
#
# The npm command by launches the npm build script:
#
# npm run build
#
# The npm test command launches the npm test script:
#
# npm run test
#
# The idea behind this is to be completely testing and build tool agnostic. All
# build tools and additional package managers should be installed locally in
# your project, since this won't pollute people's global namespace.
#
# The following npm scripts in your package.json install and update the bower
# and npm dependencies and use gulp as build system (notice how everything is
# run from the node_modules folder):
#
# "scripts": {
# "test": "node node_modules/gulp-cli/bin/gulp.js karma",
# "prebuild": "npm install && node_modules/bower/bin/bower install && node_modules/bower/bin/bower update",
# "build": "node node_modules/gulp-cli/bin/gulp.js"
# },
app_name=$(notdir $(CURDIR)) app_name=$(notdir $(CURDIR))
build_tools_directory=$(CURDIR)/build/tools build_tools_directory=$(CURDIR)/build/tools
source_build_directory=$(CURDIR)/build/artifacts/source
source_package_name=$(source_build_directory)/$(app_name)
appstore_build_directory=$(CURDIR)/build/artifacts/appstore
appstore_package_name=$(appstore_build_directory)/$(app_name)
npm=$(shell which npm 2> /dev/null)
composer=$(shell which composer 2> /dev/null) composer=$(shell which composer 2> /dev/null)
all: build all: dev-setup lint build-js-production test
# Dev env management
dev-setup: clean clean-dev composer npm-init
# Fetches the PHP and JS dependencies and compiles the JS. If no composer.json
# is present, the composer step is skipped, if no package.json or js/package.json
# is present, the npm step is skipped
.PHONY: build
build:
ifneq (,$(wildcard $(CURDIR)/composer.json))
make composer
endif
ifneq (,$(wildcard $(CURDIR)/package.json))
make npm
endif
ifneq (,$(wildcard $(CURDIR)/js/package.json))
make npm
endif
# Installs and updates the composer dependencies. If composer is not installed # Installs and updates the composer dependencies. If composer is not installed
# a copy is fetched from the web # a copy is fetched from the web
.PHONY: composer
composer: composer:
ifeq (, $(composer)) ifeq (, $(composer))
@echo "No composer command available, downloading a copy from the web" @echo "No composer command available, downloading a copy from the web"
@ -75,81 +19,53 @@ ifeq (, $(composer))
curl -sS https://getcomposer.org/installer | php curl -sS https://getcomposer.org/installer | php
mv composer.phar $(build_tools_directory) mv composer.phar $(build_tools_directory)
php $(build_tools_directory)/composer.phar install --prefer-dist php $(build_tools_directory)/composer.phar install --prefer-dist
php $(build_tools_directory)/composer.phar update --prefer-dist
else else
composer install --prefer-dist composer install --prefer-dist
composer update --prefer-dist
endif endif
# Installs npm dependencies npm-init:
.PHONY: npm npm ci
npm:
ifeq (,$(wildcard $(CURDIR)/package.json)) npm-update:
cd js && $(npm) run build npm update
else
# Building
build-js:
npm run dev
build-js-production:
npm run build npm run build
endif
# Removes the appstore build watch-js:
.PHONY: clean npm run watch
serve-js:
npm run serve
# Linting
lint:
npm run lint
lint-fix:
npm run lint:fix
# Style linting
stylelint:
npm run stylelint
stylelint-fix:
npm run stylelint:fix
# Cleaning
clean: clean:
rm -rf ./build rm -rf js/*
# Same as clean but also removes dependencies installed by composer, bower and clean-dev:
# npm
.PHONY: distclean
distclean: clean
rm -rf vendor
rm -rf node_modules rm -rf node_modules
rm -rf js/vendor
rm -rf js/node_modules
# Builds the source and appstore package # Tests
.PHONY: dist test:
dist: ./vendor/phpunit/phpunit/phpunit -c phpunit.xml
make source ./vendor/phpunit/phpunit/phpunit -c phpunit.integration.xml
make appstore
# Builds the source package
.PHONY: source
source:
rm -rf $(source_build_directory)
mkdir -p $(source_build_directory)
tar cvzf $(source_package_name).tar.gz ../$(app_name) \
--exclude-vcs \
--exclude="../$(app_name)/build" \
--exclude="../$(app_name)/js/node_modules" \
--exclude="../$(app_name)/node_modules" \
--exclude="../$(app_name)/*.log" \
--exclude="../$(app_name)/js/*.log" \
# Builds the source package for the app store, ignores php and js tests
.PHONY: appstore
appstore:
rm -rf $(appstore_build_directory)
mkdir -p $(appstore_build_directory)
tar cvzf $(appstore_package_name).tar.gz ../$(app_name) \
--exclude-vcs \
--exclude="../$(app_name)/build" \
--exclude="../$(app_name)/tests" \
--exclude="../$(app_name)/Makefile" \
--exclude="../$(app_name)/*.log" \
--exclude="../$(app_name)/phpunit*xml" \
--exclude="../$(app_name)/composer.*" \
--exclude="../$(app_name)/js/node_modules" \
--exclude="../$(app_name)/js/tests" \
--exclude="../$(app_name)/js/test" \
--exclude="../$(app_name)/js/*.log" \
--exclude="../$(app_name)/js/package.json" \
--exclude="../$(app_name)/js/bower.json" \
--exclude="../$(app_name)/js/karma.*" \
--exclude="../$(app_name)/js/protractor.*" \
--exclude="../$(app_name)/package.json" \
--exclude="../$(app_name)/bower.json" \
--exclude="../$(app_name)/karma.*" \
--exclude="../$(app_name)/protractor\.*" \
--exclude="../$(app_name)/.*" \
--exclude="../$(app_name)/js/.*" \
.PHONY: test
test: composer
$(CURDIR)/vendor/phpunit/phpunit/phpunit -c phpunit.xml
$(CURDIR)/vendor/phpunit/phpunit/phpunit -c phpunit.integration.xml

View file

@ -1,52 +1,31 @@
# U Pschooling # UPschooling
Place this app in **nextcloud/apps/** Place this app in **nextcloud/apps/**
## Building the app [![PHPUnit GitHub Action](https://github.com/nextcloud/app-tutorial/workflows/PHPUnit/badge.svg)](https://github.com/nextcloud/app-tutorial/actions?query=workflow%3APHPUnit)
[![Node GitHub Action](https://github.com/nextcloud/app-tutorial/workflows/Node/badge.svg)](https://github.com/nextcloud/app-tutorial/actions?query=workflow%3ANode)
[![Lint GitHub Action](https://github.com/nextcloud/app-tutorial/workflows/Lint/badge.svg)](https://github.com/nextcloud/app-tutorial/actions?query=workflow%3ALint)
The app can be built by using the provided Makefile by running: This is the [tutorial app](https://docs.nextcloud.com/server/latest/developer_manual/app_development/tutorial.html) which shows how to develop a very simple notes app.
make ## Installing dependencies
This requires the following things to be present:
* make
* which
* tar: for building the archive
* curl: used if phpunit and composer are not installed to fetch them from the web
* npm: for building and testing everything JS, only required if a package.json is placed inside the **js/** folder
The make command will install or update Composer dependencies if a composer.json is present and also **npm run build** if a package.json is present in the **js/** folder. The npm **build** script should use local paths for build systems and package managers, so people that simply want to build the app won't need to install npm libraries globally, e.g.:
**package.json**:
```json
"scripts": {
"test": "node node_modules/gulp-cli/bin/gulp.js karma",
"prebuild": "npm install && node_modules/bower/bin/bower install && node_modules/bower/bin/bower update",
"build": "node node_modules/gulp-cli/bin/gulp.js"
}
```
## Publish to App Store
First get an account for the [App Store](http://apps.nextcloud.com/) then run:
make && make appstore
The archive is located in build/artifacts/appstore and can then be uploaded to the App Store.
## Running tests
You can use the provided Makefile to run all tests by using:
make test
This will run the PHP unit and integration tests and if a package.json is present in the **js/** folder will execute **npm run test**
Of course you can also install [PHPUnit](http://phpunit.de/getting-started.html) and use the configurations directly:
phpunit -c phpunit.xml make composer
or: ## Frontend development
phpunit -c phpunit.integration.xml The app tutorial also shows the very basic implementation of an app frontend using [Vue.js](https://vuejs.org/). To build the frontend code after doing changes to its source in `src/` requires to have Node and npm installed.
for integration tests - 👩‍💻 Run `make dev-setup` to install the frontend dependencies
- 🏗 To build the Javascript whenever you make changes, run `make build-js`
To continuously run the build when editing source files you can make use of the `make watch-js` command.

View file

@ -2,7 +2,7 @@
<info xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance" <info xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd"> xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd">
<id>upschooling</id> <id>upschooling</id>
<name>U Pschooling</name> <name>UPschooling</name>
<summary>UPschooling Support Platform</summary> <summary>UPschooling Support Platform</summary>
<description><![CDATA[Ticketsystem für UPschooling]]></description> <description><![CDATA[Ticketsystem für UPschooling]]></description>
<version>0.0.1</version> <version>0.0.1</version>
@ -16,7 +16,7 @@
</dependencies> </dependencies>
<navigations> <navigations>
<navigation> <navigation>
<name>U Pschooling</name> <name>Support</name>
<route>upschooling.page.index</route> <route>upschooling.page.index</route>
</navigation> </navigation>
</navigations> </navigations>

View file

@ -1,15 +1,13 @@
<?php <?php
/**
* Create your routes in here. The name is the lowercase name of the controller
* without the controller part, the stuff after the hash is the method.
* e.g. page#index -> OCA\UPschooling\Controller\PageController->index()
*
* The controller class has to be registered in the application.php file since
* it's instantiated in there
*/
return [ return [
'routes' => [ 'resources' => [
['name' => 'page#index', 'url' => '/', 'verb' => 'GET'], 'note' => ['url' => '/notes'],
['name' => 'page#do_echo', 'url' => '/echo', 'verb' => 'POST'], 'note_api' => ['url' => '/api/0.1/notes']
] ],
'routes' => [
['name' => 'page#index', 'url' => '/', 'verb' => 'GET'],
['name' => 'note_api#preflighted_cors', 'url' => '/api/0.1/{path}',
'verb' => 'OPTIONS', 'requirements' => ['path' => '.+']]
]
]; ];

3
babel.config.js Normal file
View file

@ -0,0 +1,3 @@
const babelConfig = require('@nextcloud/babel-config')
module.exports = babelConfig

View file

@ -10,6 +10,19 @@
], ],
"require": {}, "require": {},
"require-dev": { "require-dev": {
"phpunit/phpunit": "^5.4" "phpunit/phpunit": "^8.5",
"nextcloud/coding-standard": "^0.5.0"
},
"config": {
"optimize-autoloader": true,
"classmap-authoritative": true,
"platform": {
"php": "7.2"
}
},
"scripts": {
"lint": "find . -name \\*.php -not -path './vendor/*' -not -path './build/*' -print0 | xargs -0 -n1 php -l",
"cs:check": "php-cs-fixer fix --dry-run --diff",
"cs:fix": "php-cs-fixer fix"
} }
} }

2407
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,34 @@
#hello { #app-content-wrapper {
color: red; height: 100%;
} }
#editor {
height: 100%;
width: 100%;
}
#editor .input {
height: calc(100% - 51px);
width: 100%;
}
#editor .save {
height: 50px;
width: 100%;
text-align: center;
border-top: 1px solid #ccc;
background-color: #fafafa;
}
#editor textarea {
height: 100%;
width: 100%;
border: 0;
margin: 0;
border-radius: 0;
overflow-y: auto;
}
#editor button {
height: 44px;
}

View file

View file

@ -0,0 +1,13 @@
<?php
namespace OCA\UPschooling\AppInfo;
use OCP\AppFramework\App;
class Application extends App {
public const APP_ID = 'upschooling';
public function __construct() {
parent::__construct(self::APP_ID);
}
}

21
lib/Controller/Errors.php Normal file
View file

@ -0,0 +1,21 @@
<?php
namespace OCA\UPschooling\Controller;
use Closure;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCA\UPschooling\Service\NoteNotFound;
trait Errors {
protected function handleNotFound(Closure $callback): DataResponse {
try {
return new DataResponse($callback());
} catch (NoteNotFound $e) {
$message = ['message' => $e->getMessage()];
return new DataResponse($message, Http::STATUS_NOT_FOUND);
}
}
}

View file

@ -0,0 +1,80 @@
<?php
namespace OCA\UPschooling\Controller;
use OCA\UPschooling\AppInfo\Application;
use OCA\UPschooling\Service\NoteService;
use OCP\AppFramework\ApiController;
use OCP\AppFramework\Http\DataResponse;
use OCP\IRequest;
class NoteApiController extends ApiController {
/** @var NoteService */
private $service;
/** @var string */
private $userId;
use Errors;
public function __construct(IRequest $request,
NoteService $service,
$userId) {
parent::__construct(Application::APP_ID, $request);
$this->service = $service;
$this->userId = $userId;
}
/**
* @CORS
* @NoCSRFRequired
* @NoAdminRequired
*/
public function index(): DataResponse {
return new DataResponse($this->service->findAll($this->userId));
}
/**
* @CORS
* @NoCSRFRequired
* @NoAdminRequired
*/
public function show(int $id): DataResponse {
return $this->handleNotFound(function () use ($id) {
return $this->service->find($id, $this->userId);
});
}
/**
* @CORS
* @NoCSRFRequired
* @NoAdminRequired
*/
public function create(string $title, string $content): DataResponse {
return new DataResponse($this->service->create($title, $content,
$this->userId));
}
/**
* @CORS
* @NoCSRFRequired
* @NoAdminRequired
*/
public function update(int $id, string $title,
string $content): DataResponse {
return $this->handleNotFound(function () use ($id, $title, $content) {
return $this->service->update($id, $title, $content, $this->userId);
});
}
/**
* @CORS
* @NoCSRFRequired
* @NoAdminRequired
*/
public function destroy(int $id): DataResponse {
return $this->handleNotFound(function () use ($id) {
return $this->service->delete($id, $this->userId);
});
}
}

View file

@ -0,0 +1,70 @@
<?php
namespace OCA\UPschooling\Controller;
use OCA\UPschooling\AppInfo\Application;
use OCA\UPschooling\Service\NoteService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\DataResponse;
use OCP\IRequest;
class NoteController extends Controller {
/** @var NoteService */
private $service;
/** @var string */
private $userId;
use Errors;
public function __construct(IRequest $request,
NoteService $service,
$userId) {
parent::__construct(Application::APP_ID, $request);
$this->service = $service;
$this->userId = $userId;
}
/**
* @NoAdminRequired
*/
public function index(): DataResponse {
return new DataResponse($this->service->findAll($this->userId));
}
/**
* @NoAdminRequired
*/
public function show(int $id): DataResponse {
return $this->handleNotFound(function () use ($id) {
return $this->service->find($id, $this->userId);
});
}
/**
* @NoAdminRequired
*/
public function create(string $title, string $content): DataResponse {
return new DataResponse($this->service->create($title, $content,
$this->userId));
}
/**
* @NoAdminRequired
*/
public function update(int $id, string $title,
string $content): DataResponse {
return $this->handleNotFound(function () use ($id, $title, $content) {
return $this->service->update($id, $title, $content, $this->userId);
});
}
/**
* @NoAdminRequired
*/
public function destroy(int $id): DataResponse {
return $this->handleNotFound(function () use ($id) {
return $this->service->delete($id, $this->userId);
});
}
}

View file

@ -1,31 +1,27 @@
<?php <?php
namespace OCA\UPschooling\Controller; namespace OCA\UPschooling\Controller;
use OCP\IRequest; use OCA\UPschooling\AppInfo\Application;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Controller; use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\IRequest;
use OCP\Util;
class PageController extends Controller { class PageController extends Controller {
private $userId; public function __construct(IRequest $request) {
parent::__construct(Application::APP_ID, $request);
public function __construct($AppName, IRequest $request, $UserId){
parent::__construct($AppName, $request);
$this->userId = $UserId;
} }
/** /**
* CAUTION: the @Stuff turns off security checks; for this page no admin is
* required and no CSRF check. If you don't know what CSRF is, read
* it up in the docs or you might create a security hole. This is
* basically the only required method to add this exemption, don't
* add it to any other method if you don't exactly know what it does
*
* @NoAdminRequired * @NoAdminRequired
* @NoCSRFRequired * @NoCSRFRequired
*
* Render default template
*/ */
public function index() { public function index() {
return new TemplateResponse('upschooling', 'index'); // templates/index.php Util::addScript(Application::APP_ID, 'upschooling-main');
}
return new TemplateResponse(Application::APP_ID, 'main');
}
} }

21
lib/Db/Note.php Normal file
View file

@ -0,0 +1,21 @@
<?php
namespace OCA\UPschooling\Db;
use JsonSerializable;
use OCP\AppFramework\Db\Entity;
class Note extends Entity implements JsonSerializable {
protected $title;
protected $content;
protected $userId;
public function jsonSerialize(): array {
return [
'id' => $this->id,
'title' => $this->title,
'content' => $this->content
];
}
}

45
lib/Db/NoteMapper.php Normal file
View file

@ -0,0 +1,45 @@
<?php
namespace OCA\UPschooling\Db;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\Entity;
use OCP\AppFramework\Db\QBMapper;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
class NoteMapper extends QBMapper {
public function __construct(IDBConnection $db) {
parent::__construct($db, 'upschooling', Note::class);
}
/**
* @param int $id
* @param string $userId
* @return Entity|Note
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws DoesNotExistException
*/
public function find(int $id, string $userId): Note {
/* @var $qb IQueryBuilder */
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from('upschooling')
->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)))
->andWhere($qb->expr()->eq('user_id', $qb->createNamedParameter($userId)));
return $this->findEntity($qb);
}
/**
* @param string $userId
* @return array
*/
public function findAll(string $userId): array {
/* @var $qb IQueryBuilder */
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from('upschooling')
->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId)));
return $this->findEntities($qb);
}
}

View file

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace OCA\UPschooling\Migration;
use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\Migration\SimpleMigrationStep;
use OCP\Migration\IOutput;
class Version000000Date20181013124731 extends SimpleMigrationStep {
/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
if (!$schema->hasTable('upschooling')) {
$table = $schema->createTable('upschooling');
$table->addColumn('id', 'integer', [
'autoincrement' => true,
'notnull' => true,
]);
$table->addColumn('title', 'string', [
'notnull' => true,
'length' => 200
]);
$table->addColumn('user_id', 'string', [
'notnull' => true,
'length' => 200,
]);
$table->addColumn('content', 'text', [
'notnull' => true,
'default' => ''
]);
$table->setPrimaryKey(['id']);
$table->addIndex(['user_id'], 'upschooling_user_id_index');
}
return $schema;
}
}

View file

@ -0,0 +1,6 @@
<?php
namespace OCA\UPschooling\Service;
class NoteNotFound extends \Exception {
}

View file

@ -0,0 +1,76 @@
<?php
namespace OCA\UPschooling\Service;
use Exception;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCA\UPschooling\Db\Note;
use OCA\UPschooling\Db\NoteMapper;
class NoteService {
/** @var NoteMapper */
private $mapper;
public function __construct(NoteMapper $mapper) {
$this->mapper = $mapper;
}
public function findAll(string $userId): array {
return $this->mapper->findAll($userId);
}
private function handleException(Exception $e): void {
if ($e instanceof DoesNotExistException ||
$e instanceof MultipleObjectsReturnedException) {
throw new NoteNotFound($e->getMessage());
} else {
throw $e;
}
}
public function find($id, $userId) {
try {
return $this->mapper->find($id, $userId);
// in order to be able to plug in different storage backends like files
// for instance it is a good idea to turn storage related exceptions
// into service related exceptions so controllers and service users
// have to deal with only one type of exception
} catch (Exception $e) {
$this->handleException($e);
}
}
public function create($title, $content, $userId) {
$note = new Note();
$note->setTitle($title);
$note->setContent($content);
$note->setUserId($userId);
return $this->mapper->insert($note);
}
public function update($id, $title, $content, $userId) {
try {
$note = $this->mapper->find($id, $userId);
$note->setTitle($title);
$note->setContent($content);
return $this->mapper->update($note);
} catch (Exception $e) {
$this->handleException($e);
}
}
public function delete($id, $userId) {
try {
$note = $this->mapper->find($id, $userId);
$this->mapper->delete($note);
return $note;
} catch (Exception $e) {
$this->handleException($e);
}
}
}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

25700
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

50
package.json Normal file
View file

@ -0,0 +1,50 @@
{
"name": "upschooling",
"description": "A simple Nextcloud app tutorial for building a notes app",
"version": "19.0.0",
"author": "Julius Härtl <jus@bitgrid.net",
"contributors": [
"John Molakvoæ <skjnldsv@protonmail.com>"
],
"bugs": {
"url": "https://github.com/nextcloud/app-tutorial/issues"
},
"repository": {
"url": "https://github.com/nextcloud/app-tutorial",
"type": "git"
},
"homepage": "https://github.com/nextcloud/app-tutorial",
"license": "agpl",
"private": true,
"scripts": {
"build": "NODE_ENV=production webpack --progress --config webpack.js",
"dev": "NODE_ENV=development webpack --progress --config webpack.js",
"watch": "NODE_ENV=development webpack --progress --watch --config webpack.js",
"serve": "NODE_ENV=development webpack serve --progress --config webpack.js",
"lint": "eslint --ext .js,.vue src",
"lint:fix": "eslint --ext .js,.vue src --fix",
"stylelint": "stylelint css/*.css css/*.scss src/**/*.scss src/**/*.vue",
"stylelint:fix": "stylelint css/*.css css/*.scss src/**/*.scss src/**/*.vue --fix"
},
"dependencies": {
"@nextcloud/axios": "^1.6.0",
"@nextcloud/dialogs": "^3.1.2",
"@nextcloud/router": "^1.2.0",
"@nextcloud/vue": "^3.10.1",
"vue": "^2.6.14"
},
"browserslist": [
"extends @nextcloud/browserslist-config"
],
"engines": {
"node": ">=14.0.0",
"npm": ">=7.0.0"
},
"devDependencies": {
"@nextcloud/babel-config": "^1.0.0",
"@nextcloud/browserslist-config": "^2.1.0",
"@nextcloud/eslint-config": "^6.1.0",
"@nextcloud/stylelint-config": "^1.0.0-beta.0",
"@nextcloud/webpack-vue-config": "^4.1.0"
}
}

View file

@ -8,4 +8,3 @@ DIR="${0%/*}"
podman unshare -- chown -R 33 "$DIR" podman unshare -- chown -R 33 "$DIR"
podman unshare -- chgrp -R 0 "$DIR" podman unshare -- chgrp -R 0 "$DIR"
podman unshare -- chmod -R ug+rw "$DIR" podman unshare -- chmod -R ug+rw "$DIR"

View file

@ -8,5 +8,7 @@ DIR="${0%/*}"
podman run -d --name=nextcloud --replace=true -p 8080:80 -v "$DIR:/var/www/html/custom_apps/upschooling" docker.io/nextcloud podman run -d --name=nextcloud --replace=true -p 8080:80 -v "$DIR:/var/www/html/custom_apps/upschooling" docker.io/nextcloud
podman exec nextcloud chown -R 33 /var/www/html/custom_apps podman exec nextcloud chown -R 33 /var/www/html/custom_apps
"$DIR/podman-reown.sh" "$DIR/podman-reown.sh"
podman exec --user 33 nextcloud bash -c 'cd /var/www/html/custom_apps/upschooling && make' podman exec --user 33 nextcloud bash -c 'cd /var/www/html/custom_apps/upschooling && make composer'
podman exec --user 33 nextcloud php occ maintenance:install --database sqlite --admin-user admin --admin-pass admin
podman exec --user 33 nextcloud php occ config:system:set --value=true --type=boolean debug podman exec --user 33 nextcloud php occ config:system:set --value=true --type=boolean debug
podman exec --user 33 nextcloud php occ app:enable --force upschooling

234
src/App.vue Normal file
View file

@ -0,0 +1,234 @@
<template>
<div id="content" class="app-upschooling">
<AppNavigation>
<AppNavigationNew v-if="!loading"
:text="t('upschooling', 'New note')"
:disabled="false"
button-id="new-upschooling-button"
button-class="icon-add"
@click="newNote" />
<ul>
<AppNavigationItem v-for="note in notes"
:key="note.id"
:title="note.title ? note.title : t('upschooling', 'New note')"
:class="{active: currentNoteId === note.id}"
@click="openNote(note)">
<template slot="actions">
<ActionButton v-if="note.id === -1"
icon="icon-close"
@click="cancelNewNote(note)">
{{ t('upschooling', 'Cancel note creation') }}
</ActionButton>
<ActionButton v-else
icon="icon-delete"
@click="deleteNote(note)">
{{ t('upschooling', 'Delete note') }}
</ActionButton>
</template>
</AppNavigationItem>
</ul>
</AppNavigation>
<AppContent>
<div v-if="currentNote">
<input ref="title"
v-model="currentNote.title"
type="text"
:disabled="updating">
<textarea ref="content" v-model="currentNote.content" :disabled="updating" />
<input type="button"
class="primary"
:value="t('upschooling', 'Save')"
:disabled="updating || !savePossible"
@click="saveNote">
</div>
<div v-else id="emptycontent">
<div class="icon-file" />
<h2>{{ t('upschooling', 'Create a note to get started') }}</h2>
</div>
</AppContent>
</div>
</template>
<script>
import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
import AppContent from '@nextcloud/vue/dist/Components/AppContent'
import AppNavigation from '@nextcloud/vue/dist/Components/AppNavigation'
import AppNavigationItem from '@nextcloud/vue/dist/Components/AppNavigationItem'
import AppNavigationNew from '@nextcloud/vue/dist/Components/AppNavigationNew'
import '@nextcloud/dialogs/styles/toast.scss'
import { generateUrl } from '@nextcloud/router'
import { showError, showSuccess } from '@nextcloud/dialogs'
import axios from '@nextcloud/axios'
export default {
name: 'App',
components: {
ActionButton,
AppContent,
AppNavigation,
AppNavigationItem,
AppNavigationNew,
},
data() {
return {
notes: [],
currentNoteId: null,
updating: false,
loading: true,
}
},
computed: {
/**
* Return the currently selected note object
* @returns {Object|null}
*/
currentNote() {
if (this.currentNoteId === null) {
return null
}
return this.notes.find((note) => note.id === this.currentNoteId)
},
/**
* Returns true if a note is selected and its title is not empty
* @returns {Boolean}
*/
savePossible() {
return this.currentNote && this.currentNote.title !== ''
},
},
/**
* Fetch list of notes when the component is loaded
*/
async mounted() {
try {
const response = await axios.get(generateUrl('/apps/upschooling/notes'))
this.notes = response.data
} catch (e) {
console.error(e)
showError(t('upschooling', 'Could not fetch notes'))
}
this.loading = false
},
methods: {
/**
* Create a new note and focus the note content field automatically
* @param {Object} note Note object
*/
openNote(note) {
if (this.updating) {
return
}
this.currentNoteId = note.id
this.$nextTick(() => {
this.$refs.content.focus()
})
},
/**
* Action tiggered when clicking the save button
* create a new note or save
*/
saveNote() {
if (this.currentNoteId === -1) {
this.createNote(this.currentNote)
} else {
this.updateNote(this.currentNote)
}
},
/**
* Create a new note and focus the note content field automatically
* The note is not yet saved, therefore an id of -1 is used until it
* has been persisted in the backend
*/
newNote() {
if (this.currentNoteId !== -1) {
this.currentNoteId = -1
this.notes.push({
id: -1,
title: '',
content: '',
})
this.$nextTick(() => {
this.$refs.title.focus()
})
}
},
/**
* Abort creating a new note
*/
cancelNewNote() {
this.notes.splice(this.notes.findIndex((note) => note.id === -1), 1)
this.currentNoteId = null
},
/**
* Create a new note by sending the information to the server
* @param {Object} note Note object
*/
async createNote(note) {
this.updating = true
try {
const response = await axios.post(generateUrl('/apps/upschooling/notes'), note)
const index = this.notes.findIndex((match) => match.id === this.currentNoteId)
this.$set(this.notes, index, response.data)
this.currentNoteId = response.data.id
} catch (e) {
console.error(e)
showError(t('upschooling', 'Could not create the note'))
}
this.updating = false
},
/**
* Update an existing note on the server
* @param {Object} note Note object
*/
async updateNote(note) {
this.updating = true
try {
await axios.put(generateUrl(`/apps/upschooling/notes/${note.id}`), note)
} catch (e) {
console.error(e)
showError(t('upschooling', 'Could not update the note'))
}
this.updating = false
},
/**
* Delete a note, remove it from the frontend and show a hint
* @param {Object} note Note object
*/
async deleteNote(note) {
try {
await axios.delete(generateUrl(`/apps/upschooling/notes/${note.id}`))
this.notes.splice(this.notes.indexOf(note), 1)
if (this.currentNoteId === note.id) {
this.currentNoteId = null
}
showSuccess(t('upschooling', 'Note deleted'))
} catch (e) {
console.error(e)
showError(t('upschooling', 'Could not delete the note'))
}
},
},
}
</script>
<style scoped>
#app-content > div {
width: 100%;
height: 100%;
padding: 20px;
display: flex;
flex-direction: column;
flex-grow: 1;
}
input[type='text'] {
width: 100%;
}
textarea {
flex-grow: 1;
width: 100%;
}
</style>

35
src/main.js Normal file
View file

@ -0,0 +1,35 @@
/**
* @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* 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
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import { generateFilePath } from '@nextcloud/router'
import Vue from 'vue'
import App from './App'
// eslint-disable-next-line
__webpack_public_path__ = generateFilePath(appName, '', 'js/')
Vue.mixin({ methods: { t, n } })
export default new Vue({
el: '#content',
render: h => h(App),
})

3
stylelint.config.js Normal file
View file

@ -0,0 +1,3 @@
const stylelintConfig = require('@nextcloud/stylelint-config')
module.exports = stylelintConfig

View file

@ -1 +0,0 @@
<h1>Hello world</h1>

View file

@ -1,18 +0,0 @@
<?php
script('upschooling', 'script');
style('upschooling', 'style');
?>
<div id="app">
<div id="app-navigation">
<?php print_unescaped($this->inc('navigation/index')); ?>
<?php print_unescaped($this->inc('settings/index')); ?>
</div>
<div id="app-content">
<div id="app-content-wrapper">
<?php print_unescaped($this->inc('content/index')); ?>
</div>
</div>
</div>

1
templates/main.php Normal file
View file

@ -0,0 +1 @@
<div id="content"></div>

View file

@ -1,10 +0,0 @@
<ul>
<li><a href="#">First level entry</a></li>
<li>
<a href="#">First level container</a>
<ul>
<li><a href="#">Second level entry</a></li>
<li><a href="#">Second level entry</a></li>
</ul>
</li>
</ul>

View file

@ -1,10 +0,0 @@
<div id="app-settings">
<div id="app-settings-header">
<button class="settings-button"
data-apps-slide-toggle="#app-settings-content"
></button>
</div>
<div id="app-settings-content">
<!-- Your settings in here -->
</div>
</div>

View file

@ -1,29 +0,0 @@
<?php
namespace OCA\UPschooling\Tests\Integration\Controller;
use OCP\AppFramework\App;
use Test\TestCase;
/**
* This test shows how to make a small Integration Test. Query your class
* directly from the container, only pass in mocks if needed and run your tests
* against the database
*/
class AppTest extends TestCase {
private $container;
public function setUp() {
parent::setUp();
$app = new App('upschooling');
$this->container = $app->getContainer();
}
public function testAppInstalled() {
$appManager = $this->container->query('OCP\App\IAppManager');
$this->assertTrue($appManager->isInstalled('upschooling'));
}
}

View file

@ -0,0 +1,61 @@
<?php
namespace OCA\UPschooling\Tests\Integration\Controller;
use OCP\AppFramework\App;
use OCP\IRequest;
use PHPUnit\Framework\TestCase;
use OCA\UPschooling\Db\Note;
use OCA\UPschooling\Db\NoteMapper;
use OCA\UPschooling\Controller\NoteController;
class NoteIntegrationTest extends TestCase {
private $controller;
private $mapper;
private $userId = 'john';
public function setUp(): void {
$app = new App('upschooling');
$container = $app->getContainer();
// only replace the user id
$container->registerService('userId', function () {
return $this->userId;
});
// we do not care about the request but the controller needs it
$container->registerService(IRequest::class, function () {
return $this->createMock(IRequest::class);
});
$this->controller = $container->query(NoteController::class);
$this->mapper = $container->query(NoteMapper::class);
}
public function testUpdate() {
// create a new note that should be updated
$note = new Note();
$note->setTitle('old_title');
$note->setContent('old_content');
$note->setUserId($this->userId);
$id = $this->mapper->insert($note)->getId();
// fromRow does not set the fields as updated
$updatedNote = Note::fromRow([
'id' => $id,
'user_id' => $this->userId
]);
$updatedNote->setContent('content');
$updatedNote->setTitle('title');
$result = $this->controller->update($id, 'title', 'content');
$this->assertEquals($updatedNote, $result->getData());
// clean up
$this->mapper->delete($result->getData());
}
}

View file

@ -0,0 +1,12 @@
<?php
namespace OCA\UPschooling\Tests\Unit\Controller;
use OCA\UPschooling\Controller\NoteApiController;
class NoteApiControllerTest extends NoteControllerTest {
public function setUp(): void {
parent::setUp();
$this->controller = new NoteApiController($this->request, $this->service, $this->userId);
}
}

View file

@ -0,0 +1,54 @@
<?php
namespace OCA\UPschooling\Tests\Unit\Controller;
use PHPUnit\Framework\TestCase;
use OCP\AppFramework\Http;
use OCP\IRequest;
use OCA\UPschooling\Service\NoteNotFound;
use OCA\UPschooling\Service\NoteService;
use OCA\UPschooling\Controller\NoteController;
class NoteControllerTest extends TestCase {
protected $controller;
protected $service;
protected $userId = 'john';
protected $request;
public function setUp(): void {
$this->request = $this->getMockBuilder(IRequest::class)->getMock();
$this->service = $this->getMockBuilder(NoteService::class)
->disableOriginalConstructor()
->getMock();
$this->controller = new NoteController($this->request, $this->service, $this->userId);
}
public function testUpdate() {
$note = 'just check if this value is returned correctly';
$this->service->expects($this->once())
->method('update')
->with($this->equalTo(3),
$this->equalTo('title'),
$this->equalTo('content'),
$this->equalTo($this->userId))
->will($this->returnValue($note));
$result = $this->controller->update(3, 'title', 'content');
$this->assertEquals($note, $result->getData());
}
public function testUpdateNotFound() {
// test the correct status code if no note is found
$this->service->expects($this->once())
->method('update')
->will($this->throwException(new NoteNotFound()));
$result = $this->controller->update(3, 'title', 'content');
$this->assertEquals(Http::STATUS_NOT_FOUND, $result->getStatus());
}
}

View file

@ -1,31 +1,24 @@
<?php <?php
namespace OCA\UPschooling\Tests\Unit\Controller; namespace OCA\UPschooling\Controller;
use PHPUnit_Framework_TestCase; use PHPUnit\Framework\TestCase;
use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Http\TemplateResponse;
use OCA\UPschooling\Controller\PageController; class PageControllerTest extends TestCase {
class PageControllerTest extends PHPUnit_Framework_TestCase {
private $controller; private $controller;
private $userId = 'john';
public function setUp() { public function setUp(): void {
$request = $this->getMockBuilder('OCP\IRequest')->getMock(); $request = $this->getMockBuilder('OCP\IRequest')->getMock();
$this->controller = new PageController($request);
$this->controller = new PageController(
'upschooling', $request, $this->userId
);
} }
public function testIndex() { public function testIndex() {
$result = $this->controller->index(); $result = $this->controller->index();
$this->assertEquals('index', $result->getTemplateName()); $this->assertEquals('main', $result->getTemplateName());
$this->assertTrue($result instanceof TemplateResponse); $this->assertTrue($result instanceof TemplateResponse);
} }
} }

View file

@ -0,0 +1,62 @@
<?php
namespace OCA\UPschooling\Tests\Unit\Service;
use OCA\UPschooling\Service\NoteNotFound;
use PHPUnit\Framework\TestCase;
use OCP\AppFramework\Db\DoesNotExistException;
use OCA\UPschooling\Db\Note;
use OCA\UPschooling\Service\NoteService;
use OCA\UPschooling\Db\NoteMapper;
class NoteServiceTest extends TestCase {
private $service;
private $mapper;
private $userId = 'john';
public function setUp(): void {
$this->mapper = $this->getMockBuilder(NoteMapper::class)
->disableOriginalConstructor()
->getMock();
$this->service = new NoteService($this->mapper);
}
public function testUpdate() {
// the existing note
$note = Note::fromRow([
'id' => 3,
'title' => 'yo',
'content' => 'nope'
]);
$this->mapper->expects($this->once())
->method('find')
->with($this->equalTo(3))
->will($this->returnValue($note));
// the note when updated
$updatedNote = Note::fromRow(['id' => 3]);
$updatedNote->setTitle('title');
$updatedNote->setContent('content');
$this->mapper->expects($this->once())
->method('update')
->with($this->equalTo($updatedNote))
->will($this->returnValue($updatedNote));
$result = $this->service->update(3, 'title', 'content', $this->userId);
$this->assertEquals($updatedNote, $result);
}
public function testUpdateNotFound() {
$this->expectException(NoteNotFound::class);
// test the correct status code if no note is found
$this->mapper->expects($this->once())
->method('find')
->with($this->equalTo(3))
->will($this->throwException(new DoesNotExistException('')));
$this->service->update(3, 'title', 'content', $this->userId);
}
}

View file

@ -1,19 +1,3 @@
<?php <?php
if (!defined('PHPUNIT_RUN')) { require_once __DIR__ . '/../../../tests/bootstrap.php';
define('PHPUNIT_RUN', 1);
}
require_once __DIR__.'/../../../lib/base.php';
// Fix for "Autoload path not allowed: .../tests/lib/testcase.php"
\OC::$loader->addValidRoot(OC::$SERVERROOT . '/tests');
// Fix for "Autoload path not allowed: .../upschooling/tests/testcase.php"
\OC_App::loadApp('upschooling');
if(!class_exists('PHPUnit_Framework_TestCase')) {
require_once('PHPUnit/Autoload.php');
}
OC_Hook::clear();

3
webpack.js Normal file
View file

@ -0,0 +1,3 @@
const webpackConfig = require('@nextcloud/webpack-vue-config')
module.exports = webpackConfig