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
/build
/.idea/
*.iml
/build/
node_modules/
/.php_cs.cache
/js/
### Composer ###
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

172
Makefile
View file

@ -1,73 +1,17 @@
# This file is licensed under the Affero General Public License version 3 or
# 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))
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)
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
# a copy is fetched from the web
.PHONY: composer
composer:
ifeq (, $(composer))
@echo "No composer command available, downloading a copy from the web"
@ -75,81 +19,53 @@ ifeq (, $(composer))
curl -sS https://getcomposer.org/installer | php
mv composer.phar $(build_tools_directory)
php $(build_tools_directory)/composer.phar install --prefer-dist
php $(build_tools_directory)/composer.phar update --prefer-dist
else
composer install --prefer-dist
composer update --prefer-dist
endif
# Installs npm dependencies
.PHONY: npm
npm:
ifeq (,$(wildcard $(CURDIR)/package.json))
cd js && $(npm) run build
else
npm-init:
npm ci
npm-update:
npm update
# Building
build-js:
npm run dev
build-js-production:
npm run build
endif
# Removes the appstore build
.PHONY: clean
watch-js:
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:
rm -rf ./build
rm -rf js/*
# Same as clean but also removes dependencies installed by composer, bower and
# npm
.PHONY: distclean
distclean: clean
rm -rf vendor
clean-dev:
rm -rf node_modules
rm -rf js/vendor
rm -rf js/node_modules
# Builds the source and appstore package
.PHONY: dist
dist:
make source
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
# Tests
test:
./vendor/phpunit/phpunit/phpunit -c phpunit.xml
./vendor/phpunit/phpunit/phpunit -c phpunit.integration.xml

View file

@ -1,52 +1,31 @@
# UPschooling
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
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"
}
```
## Installing dependencies
## 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

@ -16,7 +16,7 @@
</dependencies>
<navigations>
<navigation>
<name>U Pschooling</name>
<name>Support</name>
<route>upschooling.page.index</route>
</navigation>
</navigations>

View file

@ -1,15 +1,13 @@
<?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 [
'resources' => [
'note' => ['url' => '/notes'],
'note_api' => ['url' => '/api/0.1/notes']
],
'routes' => [
['name' => 'page#index', 'url' => '/', 'verb' => 'GET'],
['name' => 'page#do_echo', 'url' => '/echo', 'verb' => 'POST'],
['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-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"
}
}

2405
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,34 @@
#hello {
color: red;
#app-content-wrapper {
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
namespace OCA\UPschooling\Controller;
use OCP\IRequest;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Http\DataResponse;
use OCA\UPschooling\AppInfo\Application;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\IRequest;
use OCP\Util;
class PageController extends Controller {
private $userId;
public function __construct($AppName, IRequest $request, $UserId){
parent::__construct($AppName, $request);
$this->userId = $UserId;
public function __construct(IRequest $request) {
parent::__construct(Application::APP_ID, $request);
}
/**
* 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
* @NoCSRFRequired
*
* Render default template
*/
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 -- chgrp -R 0 "$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 exec nextcloud chown -R 33 /var/www/html/custom_apps
"$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 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
namespace OCA\UPschooling\Tests\Unit\Controller;
namespace OCA\UPschooling\Controller;
use PHPUnit_Framework_TestCase;
use PHPUnit\Framework\TestCase;
use OCP\AppFramework\Http\TemplateResponse;
use OCA\UPschooling\Controller\PageController;
class PageControllerTest extends PHPUnit_Framework_TestCase {
class PageControllerTest extends TestCase {
private $controller;
private $userId = 'john';
public function setUp() {
public function setUp(): void {
$request = $this->getMockBuilder('OCP\IRequest')->getMock();
$this->controller = new PageController(
'upschooling', $request, $this->userId
);
$this->controller = new PageController($request);
}
public function testIndex() {
$result = $this->controller->index();
$this->assertEquals('index', $result->getTemplateName());
$this->assertEquals('main', $result->getTemplateName());
$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
if (!defined('PHPUNIT_RUN')) {
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();
require_once __DIR__ . '/../../../tests/bootstrap.php';

3
webpack.js Normal file
View file

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