Compare commits
No commits in common. "live" and "system2/backend" have entirely different histories.
live
...
system2/ba
265 changed files with 2491 additions and 12533 deletions
15
.editorconfig
Normal file
15
.editorconfig
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[*.yml]
|
||||||
|
indent_size = 2
|
19
.env.example
Normal file
19
.env.example
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
APP_NAME="Lost & Found Backend"
|
||||||
|
APP_ENV=local
|
||||||
|
APP_KEY=
|
||||||
|
APP_DEBUG=false
|
||||||
|
APP_URL=http://localhost
|
||||||
|
APP_TIMEZONE=Europe/Berlin
|
||||||
|
|
||||||
|
LOG_CHANNEL=stack
|
||||||
|
LOG_SLACK_WEBHOOK_URL=
|
||||||
|
|
||||||
|
DB_CONNECTION=mysql
|
||||||
|
DB_HOST=dbserver
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_DATABASE=lostfound
|
||||||
|
DB_USERNAME=lostfound
|
||||||
|
DB_PASSWORD=lostfound
|
||||||
|
|
||||||
|
CACHE_DRIVER=file
|
||||||
|
QUEUE_CONNECTION=sync
|
|
@ -1,35 +0,0 @@
|
||||||
name: Bug Report
|
|
||||||
about: File a bug report
|
|
||||||
labels:
|
|
||||||
- Kind/Bug
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
Thanks for taking the time to fill out this bug report!
|
|
||||||
- type: textarea
|
|
||||||
id: what-happened
|
|
||||||
attributes:
|
|
||||||
label: What happened?
|
|
||||||
description: Also tell us, what did you expect to happen?
|
|
||||||
placeholder: Tell us what you see!
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: dropdown
|
|
||||||
id: browsers
|
|
||||||
attributes:
|
|
||||||
label: What browsers are you seeing the problem on?
|
|
||||||
multiple: true
|
|
||||||
options:
|
|
||||||
- Firefox (Windows)
|
|
||||||
- Firefox (MacOS)
|
|
||||||
- Firefox (Linux)
|
|
||||||
- Firefox (Android)
|
|
||||||
- Firefox (iOS)
|
|
||||||
- Chrome (Windows)
|
|
||||||
- Chrome (MacOS)
|
|
||||||
- Chrome (Linux)
|
|
||||||
- Chrome (Android)
|
|
||||||
- Chrome (iOS)
|
|
||||||
- Safari
|
|
||||||
- Microsoft Edge
|
|
|
@ -1,27 +0,0 @@
|
||||||
name: 'New Feature'
|
|
||||||
about: 'This template is for new features'
|
|
||||||
labels:
|
|
||||||
- Kind/Feature
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
Before creating a Feature Ticket, please check for duplicates.
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
### Implementation Checklist
|
|
||||||
- [ ] concept
|
|
||||||
- [ ] frontend
|
|
||||||
- [ ] backend
|
|
||||||
- [ ] unittests
|
|
||||||
- [ ] tested on staging
|
|
||||||
visible: [ content ]
|
|
||||||
- type: textarea
|
|
||||||
id: description
|
|
||||||
attributes:
|
|
||||||
label: 'Feature Description'
|
|
||||||
description: 'Explain the the feature.'
|
|
||||||
placeholder: Description
|
|
||||||
validations:
|
|
||||||
required: true
|
|
|
@ -1,20 +0,0 @@
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
runs-on: docker
|
|
||||||
container:
|
|
||||||
image: ghcr.io/catthehacker/ubuntu:act-22.04
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: '3.11'
|
|
||||||
cache-dependency-path: '**/requirements.dev.txt'
|
|
||||||
- name: Install dependencies
|
|
||||||
working-directory: core
|
|
||||||
run: pip3 install -r requirements.dev.txt
|
|
||||||
- name: Run django tests
|
|
||||||
working-directory: core
|
|
||||||
run: python3 manage.py test
|
|
|
@ -1,60 +0,0 @@
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- testing
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
runs-on: docker
|
|
||||||
container:
|
|
||||||
image: ghcr.io/catthehacker/ubuntu:act-22.04
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: '3.11'
|
|
||||||
cache-dependency-path: '**/requirements.dev.txt'
|
|
||||||
- name: Install dependencies
|
|
||||||
working-directory: core
|
|
||||||
run: pip3 install -r requirements.dev.txt
|
|
||||||
- name: Run django tests
|
|
||||||
working-directory: core
|
|
||||||
run: python3 manage.py test
|
|
||||||
|
|
||||||
deploy:
|
|
||||||
needs: [test]
|
|
||||||
runs-on: docker
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Install ansible
|
|
||||||
run: |
|
|
||||||
apt update -y
|
|
||||||
apt install python3-pip -y
|
|
||||||
python3 -m pip install ansible
|
|
||||||
python3 -m pip install ansible-lint
|
|
||||||
|
|
||||||
- name: Populate relevant files
|
|
||||||
run: |
|
|
||||||
mkdir ~/.ssh
|
|
||||||
echo "${{ secrets.C3LF_SSH_TESTING }}" > ~/.ssh/id_ed25519
|
|
||||||
chmod 0600 ~/.ssh/id_ed25519
|
|
||||||
ls -lah ~/.ssh
|
|
||||||
command -v ssh-agent >/dev/null || ( apt-get update -y && apt-get install openssh-client -y )
|
|
||||||
eval $(ssh-agent -s)
|
|
||||||
ssh-add ~/.ssh/id_ed25519
|
|
||||||
echo "andromeda.lab.or.it ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDXPoO0PE+B9PYwbGaLo98zhbmjAkp6eBtVeZe43v/+T" >> ~/.ssh/known_hosts
|
|
||||||
mkdir /etc/ansible
|
|
||||||
echo "${{ secrets.C3LF_INVENTORY_TESTING }}" > /etc/ansible/hosts
|
|
||||||
|
|
||||||
- name: Check ansible version
|
|
||||||
run: |
|
|
||||||
ansible --version
|
|
||||||
|
|
||||||
- name: List ansible hosts
|
|
||||||
run: |
|
|
||||||
ansible -m ping Andromeda
|
|
||||||
|
|
||||||
- name: Deploy testing
|
|
||||||
run: |
|
|
||||||
cd deploy/ansible
|
|
||||||
ansible-playbook playbooks/deploy-c3lf-sys3.yml
|
|
15
.gitignore
vendored
15
.gitignore
vendored
|
@ -1,8 +1,15 @@
|
||||||
|
/vendor
|
||||||
/.idea
|
/.idea
|
||||||
|
Homestead.json
|
||||||
|
Homestead.yaml
|
||||||
.env
|
.env
|
||||||
.local
|
.local
|
||||||
|
/public/docs
|
||||||
|
/public/staticimages
|
||||||
|
/public/thumbnails
|
||||||
|
/resources/docs
|
||||||
|
|
||||||
staticfiles/
|
composer.lock
|
||||||
userfiles/
|
composer.phar
|
||||||
*.db
|
|
||||||
|
.phpunit.result.cache
|
||||||
|
|
9
.styleci.yml
Normal file
9
.styleci.yml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
php:
|
||||||
|
preset: laravel
|
||||||
|
enabled:
|
||||||
|
- alpha_ordered_imports
|
||||||
|
disabled:
|
||||||
|
- length_ordered_imports
|
||||||
|
- unused_use
|
||||||
|
js: true
|
||||||
|
css: true
|
29
app/Console/Kernel.php
Normal file
29
app/Console/Kernel.php
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console;
|
||||||
|
|
||||||
|
use Illuminate\Console\Scheduling\Schedule;
|
||||||
|
use Laravel\Lumen\Console\Kernel as ConsoleKernel;
|
||||||
|
|
||||||
|
class Kernel extends ConsoleKernel
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The Artisan commands provided by your application.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $commands = [
|
||||||
|
//
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the application's command schedule.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Console\Scheduling\Schedule $schedule
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function schedule(Schedule $schedule)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
42
app/Container.php
Normal file
42
app/Container.php
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class Container extends Model
|
||||||
|
{
|
||||||
|
use SoftDeletes;
|
||||||
|
/**
|
||||||
|
* The attributes that are mass assignable.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $fillable = [
|
||||||
|
'cid', 'name'
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
protected $primaryKey = 'cid';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes excluded from the model's JSON form.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $hidden = ['created_at', 'deleted_at', 'updated_at'];
|
||||||
|
|
||||||
|
static function all($columns=Array()){
|
||||||
|
return Container::leftJoin('items','items.cid','=','containers.cid')
|
||||||
|
->select('containers.cid', 'name', DB::raw('count(items.iid) as itemCount'))
|
||||||
|
->groupBy('containers.cid', 'name')->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
static function find($id){
|
||||||
|
return Container::leftJoin('items','items.cid','=','containers.cid')
|
||||||
|
->select('containers.cid', 'name', DB::raw('count(items.iid) as itemCount'))
|
||||||
|
->groupBy('containers.cid', 'name')->where(Container::primaryKey, '=', $id)->first();
|
||||||
|
}
|
||||||
|
}
|
27
app/Event.php
Normal file
27
app/Event.php
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Event extends Model
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes that are mass assignable.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $fillable = [
|
||||||
|
'eid', 'name', 'slug', 'start', 'end', 'pre_start', 'post_end'
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
protected $primaryKey = 'eid';
|
||||||
|
/**
|
||||||
|
* The attributes excluded from the model's JSON form.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $hidden = ['created_at','updated_at'];
|
||||||
|
}
|
10
app/Events/Event.php
Normal file
10
app/Events/Event.php
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
abstract class Event
|
||||||
|
{
|
||||||
|
use SerializesModels;
|
||||||
|
}
|
16
app/Events/ExampleEvent.php
Normal file
16
app/Events/ExampleEvent.php
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
class ExampleEvent extends Event
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Create a new event instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
50
app/Exceptions/Handler.php
Normal file
50
app/Exceptions/Handler.php
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exceptions;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use Illuminate\Auth\Access\AuthorizationException;
|
||||||
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use Laravel\Lumen\Exceptions\Handler as ExceptionHandler;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||||
|
|
||||||
|
class Handler extends ExceptionHandler
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* A list of the exception types that should not be reported.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $dontReport = [
|
||||||
|
AuthorizationException::class,
|
||||||
|
HttpException::class,
|
||||||
|
ModelNotFoundException::class,
|
||||||
|
ValidationException::class,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Report or log an exception.
|
||||||
|
*
|
||||||
|
* This is a great spot to send exceptions to Sentry, Bugsnag, etc.
|
||||||
|
*
|
||||||
|
* @param \Exception $exception
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function report(Exception $exception)
|
||||||
|
{
|
||||||
|
parent::report($exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render an exception into an HTTP response.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @param \Exception $exception
|
||||||
|
* @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse
|
||||||
|
*/
|
||||||
|
public function render($request, Exception $exception)
|
||||||
|
{
|
||||||
|
return parent::render($request, $exception);
|
||||||
|
}
|
||||||
|
}
|
53
app/File.php
Normal file
53
app/File.php
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use TheSeer\Tokenizer\Exception;
|
||||||
|
|
||||||
|
class File extends Model
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes that are mass assignable.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $fillable = [
|
||||||
|
'hash', 'iid'
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $primaryKey = 'hash';
|
||||||
|
public $incrementing = false;
|
||||||
|
protected $keyType = 'string';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes excluded from the model's JSON form.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $hidden = ['created_at','updated_at'];
|
||||||
|
|
||||||
|
public static function create(array $attributes = [])
|
||||||
|
{
|
||||||
|
if (!isset($attributes['data'])) {
|
||||||
|
throw new Exception("foo" );
|
||||||
|
}
|
||||||
|
$pos = strpos($attributes['data'], ",");
|
||||||
|
$image = base64_decode(substr($attributes['data'], $pos + 1), true);
|
||||||
|
if (!$image) {
|
||||||
|
throw new Exception("foo" );
|
||||||
|
}
|
||||||
|
$hash = md5(time());
|
||||||
|
if (!file_exists('staticimages'))
|
||||||
|
mkdir('staticimages', 0755, true);
|
||||||
|
file_put_contents('staticimages/' . $hash, $image);
|
||||||
|
|
||||||
|
$attributes['hash'] = $hash;
|
||||||
|
return static::query()->create($attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
46
app/Http/Controllers/ContainerController.php
Normal file
46
app/Http/Controllers/ContainerController.php
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Container;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @group Container management
|
||||||
|
*
|
||||||
|
* APIs for creating, deleting, updating and viewing containers
|
||||||
|
*/
|
||||||
|
class ContainerController extends Controller
|
||||||
|
{
|
||||||
|
|
||||||
|
public function showAllContainers()
|
||||||
|
{
|
||||||
|
return response()->json(Container::all());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function showOneContainer($id)
|
||||||
|
{
|
||||||
|
return response()->json(Container::find($id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(Request $request)
|
||||||
|
{
|
||||||
|
$container = Container::create($request->all());
|
||||||
|
|
||||||
|
return response()->json($container, 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update($id, Request $request)
|
||||||
|
{
|
||||||
|
$container = Container::findOrFail($id);
|
||||||
|
$container->update($request->all());
|
||||||
|
|
||||||
|
return response()->json($container, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete($id)
|
||||||
|
{
|
||||||
|
Container::findOrFail($id)->delete();
|
||||||
|
return response('Deleted Successfully', 200);
|
||||||
|
}
|
||||||
|
}
|
10
app/Http/Controllers/Controller.php
Normal file
10
app/Http/Controllers/Controller.php
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Laravel\Lumen\Routing\Controller as BaseController;
|
||||||
|
|
||||||
|
class Controller extends BaseController
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
41
app/Http/Controllers/EventController.php
Normal file
41
app/Http/Controllers/EventController.php
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Event;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class EventController extends Controller
|
||||||
|
{
|
||||||
|
|
||||||
|
public function showAllEvents()
|
||||||
|
{
|
||||||
|
return response()->json(Event::all());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function showOneEvent($id)
|
||||||
|
{
|
||||||
|
return response()->json(Event::find($id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(Request $request)
|
||||||
|
{
|
||||||
|
$event = Event::create($request->all());
|
||||||
|
|
||||||
|
return response()->json($event, 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update($id, Request $request)
|
||||||
|
{
|
||||||
|
$event = Event::findOrFail($id);
|
||||||
|
$event->update($request->all());
|
||||||
|
|
||||||
|
return response()->json($event, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete($id)
|
||||||
|
{
|
||||||
|
Event::findOrFail($id)->delete();
|
||||||
|
return response('Deleted Successfully', 200);
|
||||||
|
}
|
||||||
|
}
|
32
app/Http/Controllers/FileController.php
Normal file
32
app/Http/Controllers/FileController.php
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\File;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class FileController extends Controller
|
||||||
|
{
|
||||||
|
|
||||||
|
public function showAllFiles()
|
||||||
|
{
|
||||||
|
return response()->json(File::all());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function showOneFile($id)
|
||||||
|
{
|
||||||
|
return response()->json(File::find($id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(Request $request)
|
||||||
|
{
|
||||||
|
$file = File::create($request->only(['data','iid']));
|
||||||
|
return response()->json($file, 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete($id)
|
||||||
|
{
|
||||||
|
File::findOrFail($id)->delete();
|
||||||
|
return response('Deleted Successfully', 200);
|
||||||
|
}
|
||||||
|
}
|
83
app/Http/Controllers/ItemController.php
Normal file
83
app/Http/Controllers/ItemController.php
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Container;
|
||||||
|
use App\File;
|
||||||
|
use App\Item;
|
||||||
|
use App\Event;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class ItemController extends Controller
|
||||||
|
{
|
||||||
|
|
||||||
|
public function showAllItems()
|
||||||
|
{
|
||||||
|
return response()->json(Item::all());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function showByEvent($event)
|
||||||
|
{
|
||||||
|
$eid = Event::where('slug','=',$event)->first()->eid;
|
||||||
|
$q = Item::byEvent($eid);
|
||||||
|
return response()->json($q->get());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function searchByEvent($event, $query)
|
||||||
|
{
|
||||||
|
$eid = Event::where('slug','=',$event)->first()->eid;
|
||||||
|
$query_tokens = explode(" ",base64_decode ( $query , true));
|
||||||
|
$q = Item::byEvent($eid);
|
||||||
|
foreach ($query_tokens as $token)
|
||||||
|
if(!empty($token))
|
||||||
|
$q = $q->where('items.description','like','%'.$token.'%');
|
||||||
|
return response()->json($q->get());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function showOneItem($event, $id)
|
||||||
|
{
|
||||||
|
$eid = Event::where('slug','=',$event)->first()->eid;
|
||||||
|
return response()->json(Item::byEvent($eid)->where('uid', '=', $id)->first());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create($event, Request $request)
|
||||||
|
{
|
||||||
|
$eid = Event::where('slug','=',$event)->first()->eid;
|
||||||
|
$newitem = $request->except(['dataImage']);
|
||||||
|
$newitem['eid'] = "".$eid;
|
||||||
|
$item = Item::create($newitem);
|
||||||
|
|
||||||
|
if($request->get('dataImage')) {
|
||||||
|
$file = File::create(array('data' => $request->get('dataImage'), 'iid' => $item['iid']));
|
||||||
|
$item['file'] = $file['hash'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json($item, 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update($event, $id, Request $request)
|
||||||
|
{
|
||||||
|
$eid = Event::where('slug', $event)->first()->eid;
|
||||||
|
$item = Item::where('eid', $eid)->where('uid', $id)->first();
|
||||||
|
$item->update($request->except(['file', 'box', 'dataImage']));
|
||||||
|
|
||||||
|
if($request->get('returned')===true){
|
||||||
|
$item->update(['returned_at' => DB::raw( 'current_timestamp' )]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if($request->get('dataImage')) {
|
||||||
|
$file = File::create(array('data' => $request->get('dataImage'), 'iid' => $item['iid']));
|
||||||
|
$item['file'] = $file['hash'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json(Item::find($item['uid']), 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete($event, $id)
|
||||||
|
{
|
||||||
|
$eid = Event::where('slug','=',$event)->first()->eid;
|
||||||
|
Item::where('eid', $eid)->where('uid', $id)->first()->delete();
|
||||||
|
return response()->json(array("status"=>'Deleted Successfully'), 200);
|
||||||
|
}
|
||||||
|
}
|
44
app/Http/Middleware/Authenticate.php
Normal file
44
app/Http/Middleware/Authenticate.php
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Contracts\Auth\Factory as Auth;
|
||||||
|
|
||||||
|
class Authenticate
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The authentication guard factory instance.
|
||||||
|
*
|
||||||
|
* @var \Illuminate\Contracts\Auth\Factory
|
||||||
|
*/
|
||||||
|
protected $auth;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new middleware instance.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Contracts\Auth\Factory $auth
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct(Auth $auth)
|
||||||
|
{
|
||||||
|
$this->auth = $auth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @param \Closure $next
|
||||||
|
* @param string|null $guard
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle($request, Closure $next, $guard = null)
|
||||||
|
{
|
||||||
|
if ($this->auth->guard($guard)->guest()) {
|
||||||
|
return response('Unauthorized.', 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
20
app/Http/Middleware/ExampleMiddleware.php
Normal file
20
app/Http/Middleware/ExampleMiddleware.php
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
|
||||||
|
class ExampleMiddleware
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @param \Closure $next
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle($request, Closure $next)
|
||||||
|
{
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
65
app/Item.php
Normal file
65
app/Item.php
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
|
||||||
|
class Item extends Model
|
||||||
|
{
|
||||||
|
|
||||||
|
use SoftDeletes;
|
||||||
|
/**
|
||||||
|
* The attributes that are mass assignable.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $fillable = [
|
||||||
|
'iid', 'uid', 'description', 'eid', 'cid', 'returned_at'
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
protected $primaryKey = 'iid';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes excluded from the model's JSON form.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $hidden = ['created_at','updated_at', 'deleted_at', 'returned_at', 'eid', 'iid'];
|
||||||
|
|
||||||
|
public static function restored($callback)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function create(array $attributes = [])
|
||||||
|
{
|
||||||
|
$uid = static::query()->withTrashed()->where('eid',$attributes['eid'])->max('uid') + 1;
|
||||||
|
$attributes['uid'] = $uid;
|
||||||
|
$item = static::query()->create($attributes);
|
||||||
|
return Item::find($item->iid);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function extended($columns=Array()){
|
||||||
|
return Item::whereNull('returned_at')
|
||||||
|
->join('containers','items.cid','=','containers.cid')
|
||||||
|
->leftJoin('currentfiles','items.iid','=','currentfiles.iid');
|
||||||
|
}
|
||||||
|
|
||||||
|
static function byEvent($eid){
|
||||||
|
return Item::extended()->where('eid','=',$eid)
|
||||||
|
->select('items.*','currentfiles.hash as file', 'containers.name as box');
|
||||||
|
}
|
||||||
|
|
||||||
|
static function all($columns=Array()){
|
||||||
|
return Item::extended($columns)
|
||||||
|
->select('items.*','currentfiles.hash as file', 'containers.name as box')
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
static function find($id){
|
||||||
|
return Item::extended()
|
||||||
|
->select('items.*','currentfiles.hash as file', 'containers.name as box')
|
||||||
|
->where('items.iid', '=', $id)->first();
|
||||||
|
}
|
||||||
|
}
|
26
app/Jobs/ExampleJob.php
Normal file
26
app/Jobs/ExampleJob.php
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs;
|
||||||
|
|
||||||
|
class ExampleJob extends Job
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Create a new job instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the job.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
24
app/Jobs/Job.php
Normal file
24
app/Jobs/Job.php
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs;
|
||||||
|
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
abstract class Job implements ShouldQueue
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Queueable Jobs
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This job base class provides a central location to place any logic that
|
||||||
|
| is shared across all of your jobs. The trait included with the class
|
||||||
|
| provides access to the "queueOn" and "delay" queue helper methods.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
use InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
}
|
31
app/Listeners/ExampleListener.php
Normal file
31
app/Listeners/ExampleListener.php
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Listeners;
|
||||||
|
|
||||||
|
use App\Events\ExampleEvent;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
|
||||||
|
class ExampleListener
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Create the event listener.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the event.
|
||||||
|
*
|
||||||
|
* @param ExampleEvent $event
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function handle(ExampleEvent $event)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
25
app/Providers/AppServiceProvider.php
Normal file
25
app/Providers/AppServiceProvider.php
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class AppServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Register any application services.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function register()
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
public function boot()
|
||||||
|
{
|
||||||
|
Schema::defaultStringLength(191);
|
||||||
|
}
|
||||||
|
}
|
39
app/Providers/AuthServiceProvider.php
Normal file
39
app/Providers/AuthServiceProvider.php
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use App\User;
|
||||||
|
use Illuminate\Support\Facades\Gate;
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
|
class AuthServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Register any application services.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function register()
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Boot the authentication services for the application.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function boot()
|
||||||
|
{
|
||||||
|
// Here you may define how you wish users to be authenticated for your Lumen
|
||||||
|
// application. The callback which receives the incoming request instance
|
||||||
|
// should return either a User instance or null. You're free to obtain
|
||||||
|
// the User instance via an API token or any other method necessary.
|
||||||
|
|
||||||
|
$this->app['auth']->viaRequest('api', function ($request) {
|
||||||
|
if ($request->input('api_token')) {
|
||||||
|
return User::where('api_token', $request->input('api_token'))->first();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
19
app/Providers/EventServiceProvider.php
Normal file
19
app/Providers/EventServiceProvider.php
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use Laravel\Lumen\Providers\EventServiceProvider as ServiceProvider;
|
||||||
|
|
||||||
|
class EventServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The event listener mappings for the application.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $listen = [
|
||||||
|
'App\Events\ExampleEvent' => [
|
||||||
|
'App\Listeners\ExampleListener',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
32
app/User.php
Normal file
32
app/User.php
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use Illuminate\Auth\Authenticatable;
|
||||||
|
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
|
||||||
|
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Laravel\Lumen\Auth\Authorizable;
|
||||||
|
|
||||||
|
class User extends Model implements AuthenticatableContract, AuthorizableContract
|
||||||
|
{
|
||||||
|
use Authenticatable, Authorizable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes that are mass assignable.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $fillable = [
|
||||||
|
'name', 'email',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes excluded from the model's JSON form.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $hidden = [
|
||||||
|
'password',
|
||||||
|
];
|
||||||
|
}
|
35
artisan
Executable file
35
artisan
Executable file
|
@ -0,0 +1,35 @@
|
||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Symfony\Component\Console\Input\ArgvInput;
|
||||||
|
use Symfony\Component\Console\Output\ConsoleOutput;
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Create The Application
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| First we need to get an application instance. This creates an instance
|
||||||
|
| of the application / container and bootstraps the application so it
|
||||||
|
| is ready to receive HTTP / Console requests from the environment.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
$app = require __DIR__.'/bootstrap/app.php';
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Run The Artisan Application
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When we run the console application, the current CLI command will be
|
||||||
|
| executed in this console and the response sent back to a terminal
|
||||||
|
| or another output device for the developers. Here goes nothing!
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
$kernel = $app->make(
|
||||||
|
'Illuminate\Contracts\Console\Kernel'
|
||||||
|
);
|
||||||
|
|
||||||
|
exit($kernel->handle(new ArgvInput, new ConsoleOutput));
|
33
backup.sh
Normal file
33
backup.sh
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
#backup.sh /var/www/lfbackend OPTION
|
||||||
|
#OPTION
|
||||||
|
# - F FULL BACKUP
|
||||||
|
# - T Only save images from today
|
||||||
|
# - I Incremental Backup (Not implemented yet)
|
||||||
|
# (OPTIONS T and I only apply for images)
|
||||||
|
|
||||||
|
#CRON
|
||||||
|
#17 * * * * root cd /tmp && /var/www/lfbackend/backup.sh /var/www/lfbackend T
|
||||||
|
#45 5 * * * root cd /tmp && /var/www/lfbackend/backup.sh /var/www/lfbackend F
|
||||||
|
|
||||||
|
OPTION=$2
|
||||||
|
|
||||||
|
source $1/.env
|
||||||
|
|
||||||
|
TS=`date +%Y%m%d%H%M%S`
|
||||||
|
|
||||||
|
mysqldump -u $DB_USERNAME -p$DB_PASSWORD -h $DB_HOST $DB_DATABASE > database.sql
|
||||||
|
if [ "$OPTION" == "T" ]
|
||||||
|
then
|
||||||
|
tar -N "`date +%Y-%m-%d`" -zcvf images.tar.gz -C $1/public/staticimages/ .
|
||||||
|
elif [ "$OPTION" == "I" ]
|
||||||
|
then
|
||||||
|
tar -zcvf images.tar.gz -C $1/public/staticimages/ .
|
||||||
|
else
|
||||||
|
tar -zcvf images.tar.gz -C $1/public/staticimages/ .
|
||||||
|
fi
|
||||||
|
|
||||||
|
gzip -f database.sql
|
||||||
|
tar -cvf backup_${TS}_${OPTION}.tar database.sql.gz images.tar.gz
|
||||||
|
|
102
bootstrap/app.php
Normal file
102
bootstrap/app.php
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__.'/../vendor/autoload.php';
|
||||||
|
|
||||||
|
(new Laravel\Lumen\Bootstrap\LoadEnvironmentVariables(
|
||||||
|
dirname(__DIR__)
|
||||||
|
))->bootstrap();
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Create The Application
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here we will load the environment and create the application instance
|
||||||
|
| that serves as the central piece of this framework. We'll use this
|
||||||
|
| application as an "IoC" container and router for this framework.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
$app = new Laravel\Lumen\Application(
|
||||||
|
dirname(__DIR__)
|
||||||
|
);
|
||||||
|
|
||||||
|
$app->withFacades();
|
||||||
|
|
||||||
|
$app->withEloquent();
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Register Container Bindings
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Now we will register a few bindings in the service container. We will
|
||||||
|
| register the exception handler and the console kernel. You may add
|
||||||
|
| your own bindings here if you like or you can make another file.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
$app->singleton(
|
||||||
|
Illuminate\Contracts\Debug\ExceptionHandler::class,
|
||||||
|
App\Exceptions\Handler::class
|
||||||
|
);
|
||||||
|
|
||||||
|
$app->singleton(
|
||||||
|
Illuminate\Contracts\Console\Kernel::class,
|
||||||
|
App\Console\Kernel::class
|
||||||
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Register Middleware
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Next, we will register the middleware with the application. These can
|
||||||
|
| be global middleware that run before and after each request into a
|
||||||
|
| route or middleware that'll be assigned to some specific routes.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
// $app->middleware([
|
||||||
|
// App\Http\Middleware\ExampleMiddleware::class
|
||||||
|
// ]);
|
||||||
|
|
||||||
|
// $app->routeMiddleware([
|
||||||
|
// 'auth' => App\Http\Middleware\Authenticate::class,
|
||||||
|
// ]);
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Register Service Providers
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here we will register all of the application's service providers which
|
||||||
|
| are used to bind services into the container. Service providers are
|
||||||
|
| totally optional, so you are not required to uncomment this line.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
// $app->register(App\Providers\AppServiceProvider::class);
|
||||||
|
// $app->register(App\Providers\AuthServiceProvider::class);
|
||||||
|
// $app->register(App\Providers\EventServiceProvider::class);
|
||||||
|
$app->register(\Mpociot\ApiDoc\ApiDocGeneratorServiceProvider::class);
|
||||||
|
$app->configure('apidoc');
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Load The Application Routes
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Next we will include the routes file so that they can all be added to
|
||||||
|
| the application. This will provide all of the URLs the application
|
||||||
|
| can respond to, as well as the controllers that may handle them.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
$app->router->group([
|
||||||
|
'namespace' => 'App\Http\Controllers',
|
||||||
|
], function ($router) {
|
||||||
|
require __DIR__.'/../routes/web.php';
|
||||||
|
});
|
||||||
|
|
||||||
|
return $app;
|
44
composer.json
Normal file
44
composer.json
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
{
|
||||||
|
"name": "laravel/lumen",
|
||||||
|
"description": "The Laravel Lumen Framework.",
|
||||||
|
"keywords": ["framework", "laravel", "lumen"],
|
||||||
|
"license": "MIT",
|
||||||
|
"type": "project",
|
||||||
|
"require": {
|
||||||
|
"php": "^7.2",
|
||||||
|
"doctrine/dbal": "^2.10",
|
||||||
|
"laravel/lumen-framework": "^6.0",
|
||||||
|
"mpociot/laravel-apidoc-generator": "^4.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"fzaninotto/faker": "^1.4",
|
||||||
|
"phpunit/phpunit": "^8.0",
|
||||||
|
"mockery/mockery": "^1.0"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"classmap": [
|
||||||
|
"database/seeds",
|
||||||
|
"database/factories"
|
||||||
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"App\\": "app/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"classmap": [
|
||||||
|
"tests/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"post-root-package-install": [
|
||||||
|
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"preferred-install": "dist",
|
||||||
|
"sort-packages": true,
|
||||||
|
"optimize-autoloader": true
|
||||||
|
},
|
||||||
|
"minimum-stability": "dev",
|
||||||
|
"prefer-stable": true
|
||||||
|
}
|
245
config/apidoc.php
Normal file
245
config/apidoc.php
Normal file
|
@ -0,0 +1,245 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
/*
|
||||||
|
* The type of documentation output to generate.
|
||||||
|
* - "static" will generate a static HTMl page in the /public/docs folder,
|
||||||
|
* - "laravel" will generate the documentation as a Blade view,
|
||||||
|
* so you can add routing and authentication.
|
||||||
|
*/
|
||||||
|
'type' => 'static',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The router to be used (Laravel or Dingo).
|
||||||
|
*/
|
||||||
|
'router' => 'laravel',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The base URL to be used in examples and the Postman collection.
|
||||||
|
* By default, this will be the value of config('app.url').
|
||||||
|
*/
|
||||||
|
'base_url' => null,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Generate a Postman collection in addition to HTML docs.
|
||||||
|
* For 'static' docs, the collection will be generated to public/docs/collection.json.
|
||||||
|
* For 'laravel' docs, it will be generated to storage/app/apidoc/collection.json.
|
||||||
|
* The `ApiDoc::routes()` helper will add routes for both the HTML and the Postman collection.
|
||||||
|
*/
|
||||||
|
'postman' => [
|
||||||
|
/*
|
||||||
|
* Specify whether the Postman collection should be generated.
|
||||||
|
*/
|
||||||
|
'enabled' => true,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The name for the exported Postman collection. Default: config('app.name')." API"
|
||||||
|
*/
|
||||||
|
'name' => null,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The description for the exported Postman collection.
|
||||||
|
*/
|
||||||
|
'description' => null,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The routes for which documentation should be generated.
|
||||||
|
* Each group contains rules defining which routes should be included ('match', 'include' and 'exclude' sections)
|
||||||
|
* and rules which should be applied to them ('apply' section).
|
||||||
|
*/
|
||||||
|
'routes' => [
|
||||||
|
[
|
||||||
|
/*
|
||||||
|
* Specify conditions to determine what routes will be parsed in this group.
|
||||||
|
* A route must fulfill ALL conditions to pass.
|
||||||
|
*/
|
||||||
|
'match' => [
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Match only routes whose domains match this pattern (use * as a wildcard to match any characters).
|
||||||
|
*/
|
||||||
|
'domains' => [
|
||||||
|
'*',
|
||||||
|
// 'domain1.*',
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Match only routes whose paths match this pattern (use * as a wildcard to match any characters).
|
||||||
|
*/
|
||||||
|
'prefixes' => [
|
||||||
|
'*',
|
||||||
|
// 'users/*',
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Match only routes registered under this version. This option is ignored for Laravel router.
|
||||||
|
* Note that wildcards are not supported.
|
||||||
|
*/
|
||||||
|
'versions' => [
|
||||||
|
'v1',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Include these routes when generating documentation,
|
||||||
|
* even if they did not match the rules above.
|
||||||
|
* Note that the route must be referenced by name here (wildcards are supported).
|
||||||
|
*/
|
||||||
|
'include' => [
|
||||||
|
// 'users.index', 'healthcheck*'
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Exclude these routes when generating documentation,
|
||||||
|
* even if they matched the rules above.
|
||||||
|
* Note that the route must be referenced by name here (wildcards are supported).
|
||||||
|
*/
|
||||||
|
'exclude' => [
|
||||||
|
// 'users.create', 'admin.*'
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Specify rules to be applied to all the routes in this group when generating documentation
|
||||||
|
*/
|
||||||
|
'apply' => [
|
||||||
|
/*
|
||||||
|
* Specify headers to be added to the example requests
|
||||||
|
*/
|
||||||
|
'headers' => [
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'Accept' => 'application/json',
|
||||||
|
// 'Authorization' => 'Bearer {token}',
|
||||||
|
// 'Api-Version' => 'v2',
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If no @response or @transformer declarations are found for the route,
|
||||||
|
* we'll try to get a sample response by attempting an API call.
|
||||||
|
* Configure the settings for the API call here.
|
||||||
|
*/
|
||||||
|
'response_calls' => [
|
||||||
|
/*
|
||||||
|
* API calls will be made only for routes in this group matching these HTTP methods (GET, POST, etc).
|
||||||
|
* List the methods here or use '*' to mean all methods. Leave empty to disable API calls.
|
||||||
|
*/
|
||||||
|
'methods' => ['GET'],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Laravel config variables which should be set for the API call.
|
||||||
|
* This is a good place to ensure that notifications, emails
|
||||||
|
* and other external services are not triggered
|
||||||
|
* during the documentation API calls
|
||||||
|
*/
|
||||||
|
'config' => [
|
||||||
|
'app.env' => 'documentation',
|
||||||
|
'app.debug' => false,
|
||||||
|
// 'service.key' => 'value',
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Cookies which should be sent with the API call.
|
||||||
|
*/
|
||||||
|
'cookies' => [
|
||||||
|
// 'name' => 'value'
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Query parameters which should be sent with the API call.
|
||||||
|
*/
|
||||||
|
'queryParams' => [
|
||||||
|
// 'key' => 'value',
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Body parameters which should be sent with the API call.
|
||||||
|
*/
|
||||||
|
'bodyParams' => [
|
||||||
|
// 'key' => 'value',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'strategies' => [
|
||||||
|
'metadata' => [
|
||||||
|
\Mpociot\ApiDoc\Extracting\Strategies\Metadata\GetFromDocBlocks::class,
|
||||||
|
],
|
||||||
|
'urlParameters' => [
|
||||||
|
\Mpociot\ApiDoc\Extracting\Strategies\UrlParameters\GetFromUrlParamTag::class,
|
||||||
|
],
|
||||||
|
'queryParameters' => [
|
||||||
|
\Mpociot\ApiDoc\Extracting\Strategies\QueryParameters\GetFromQueryParamTag::class,
|
||||||
|
],
|
||||||
|
'headers' => [
|
||||||
|
\Mpociot\ApiDoc\Extracting\Strategies\RequestHeaders\GetFromRouteRules::class,
|
||||||
|
],
|
||||||
|
'bodyParameters' => [
|
||||||
|
\Mpociot\ApiDoc\Extracting\Strategies\BodyParameters\GetFromBodyParamTag::class,
|
||||||
|
],
|
||||||
|
'responses' => [
|
||||||
|
\Mpociot\ApiDoc\Extracting\Strategies\Responses\UseTransformerTags::class,
|
||||||
|
\Mpociot\ApiDoc\Extracting\Strategies\Responses\UseResponseTag::class,
|
||||||
|
\Mpociot\ApiDoc\Extracting\Strategies\Responses\UseResponseFileTag::class,
|
||||||
|
\Mpociot\ApiDoc\Extracting\Strategies\Responses\UseApiResourceTags::class,
|
||||||
|
\Mpociot\ApiDoc\Extracting\Strategies\Responses\ResponseCalls::class,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Custom logo path. The logo will be copied from this location
|
||||||
|
* during the generate process. Set this to false to use the default logo.
|
||||||
|
*
|
||||||
|
* Change to an absolute path to use your custom logo. For example:
|
||||||
|
* 'logo' => resource_path('views') . '/api/logo.png'
|
||||||
|
*
|
||||||
|
* If you want to use this, please be aware of the following rules:
|
||||||
|
* - the image size must be 230 x 52
|
||||||
|
*/
|
||||||
|
'logo' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Name for the group of routes which do not have a @group set.
|
||||||
|
*/
|
||||||
|
'default_group' => 'general',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Example requests for each endpoint will be shown in each of these languages.
|
||||||
|
* Supported options are: bash, javascript, php, python
|
||||||
|
* You can add a language of your own, but you must publish the package's views
|
||||||
|
* and define a corresponding view for it in the partials/example-requests directory.
|
||||||
|
* See https://laravel-apidoc-generator.readthedocs.io/en/latest/generating-documentation.html
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
'example_languages' => [
|
||||||
|
'bash',
|
||||||
|
'javascript',
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Configure how responses are transformed using @transformer and @transformerCollection
|
||||||
|
* Requires league/fractal package: composer require league/fractal
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
'fractal' => [
|
||||||
|
/* If you are using a custom serializer with league/fractal,
|
||||||
|
* you can specify it here.
|
||||||
|
*
|
||||||
|
* Serializers included with league/fractal:
|
||||||
|
* - \League\Fractal\Serializer\ArraySerializer::class
|
||||||
|
* - \League\Fractal\Serializer\DataArraySerializer::class
|
||||||
|
* - \League\Fractal\Serializer\JsonApiSerializer::class
|
||||||
|
*
|
||||||
|
* Leave as null to use no serializer or return a simple JSON.
|
||||||
|
*/
|
||||||
|
'serializer' => null,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If you would like the package to generate the same example values for parameters on each run,
|
||||||
|
* set this to any number (eg. 1234)
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
'faker_seed' => null,
|
||||||
|
];
|
42
config/database.php
Normal file
42
config/database.php
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'default' => env('DB_CONNECTION', 'sqlite'),
|
||||||
|
'connections' => [
|
||||||
|
'sqlite' => [
|
||||||
|
'driver' => 'sqlite',
|
||||||
|
'url' => env('DATABASE_URL'),
|
||||||
|
'database' => env('DB_DATABASE', database_path('default.sqlite')),
|
||||||
|
'prefix' => '',
|
||||||
|
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
||||||
|
],
|
||||||
|
'mysql' => [
|
||||||
|
'driver' => 'mysql',
|
||||||
|
'url' => env('DATABASE_URL'),
|
||||||
|
'host' => env('DB_HOST', '127.0.0.1'),
|
||||||
|
'port' => env('DB_PORT', '3306'),
|
||||||
|
'database' => env('DB_DATABASE', 'default_db'),
|
||||||
|
'username' => env('DB_USERNAME', 'default_user'),
|
||||||
|
'password' => env('DB_PASSWORD', ''),
|
||||||
|
'unix_socket' => env('DB_SOCKET', ''),
|
||||||
|
'charset' => 'utf8mb4',
|
||||||
|
'collation' => 'utf8mb4_unicode_ci',
|
||||||
|
'prefix' => '',
|
||||||
|
'prefix_indexes' => true,
|
||||||
|
'strict' => true,
|
||||||
|
'engine' => null,
|
||||||
|
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||||
|
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||||
|
]) : [],
|
||||||
|
],
|
||||||
|
'sqlite_testing' => [
|
||||||
|
'driver' => 'sqlite',
|
||||||
|
'database' => ':memory:',
|
||||||
|
'prefix' => '',
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'fetch' => PDO::FETCH_CLASS, // Returns DB objects in an array format.
|
||||||
|
'migrations' => 'migrations'
|
||||||
|
];
|
||||||
|
|
||||||
|
?>
|
6
container/db/01_init.sql
Normal file
6
container/db/01_init.sql
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
DROP DATABASE IF EXISTS lostfound;
|
||||||
|
CREATE DATABASE lostfound;
|
||||||
|
|
||||||
|
CREATE OR REPLACE USER lostfound IDENTIFIED BY 'lostfound';
|
||||||
|
|
||||||
|
GRANT ALL privileges ON `lostfound`.* TO 'lostfound';
|
9
container/web/general.conf
Normal file
9
container/web/general.conf
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
add_header Strict-Transport-Security "max-age=15768000; includeSubDomains;";
|
||||||
|
add_header X-Content-Type-Options nosniff;
|
||||||
|
add_header X-XSS-Protection "1; mode=block";
|
||||||
|
add_header X-Robots-Tag none;
|
||||||
|
add_header X-Download-Options noopen;
|
||||||
|
add_header X-Permitted-Cross-Domain-Policies none;
|
||||||
|
add_header Access-Control-Allow-Origin '*';
|
||||||
|
|
||||||
|
client_max_body_size 50m;
|
7
container/web/images.conf
Normal file
7
container/web/images.conf
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
rewrite '^/1/images/([0-9a-fA-F]{32})/?$' /staticimages/$1 last;
|
||||||
|
rewrite '^/1/thumbs/([0-9a-fA-F]{32})/?$' /thumbnails/$1 last;
|
||||||
|
# rewrite '^/thumbnails/([0-9a-fA-F]{32})$' /thumbnail.php?id=$1 last;
|
||||||
|
|
||||||
|
location /thumbnails/ {
|
||||||
|
try_files $uri /thumbnail.php?id=$uri;
|
||||||
|
}
|
8
container/web/init.sh
Normal file
8
container/web/init.sh
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
cd /app
|
||||||
|
echo "executing COMPOSER UPDATE"
|
||||||
|
composer update
|
||||||
|
echo "executing DATABASE MIGRATE"
|
||||||
|
php artisan migrate --force
|
15
container/web/location-root.conf
Normal file
15
container/web/location-root.conf
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
location / {
|
||||||
|
if ($request_method = OPTIONS) {
|
||||||
|
add_header Content-Length 0;
|
||||||
|
add_header Content-Type text/plain;
|
||||||
|
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
|
||||||
|
add_header Access-Control-Allow-Origin '*'; #$http_origin;
|
||||||
|
add_header Access-Control-Allow-Headers "Authorization, Content-Type";
|
||||||
|
add_header Access-Control-Allow-Credentials true;
|
||||||
|
return 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
try_files $uri $uri/ /index.php?$query_string;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -1,14 +0,0 @@
|
||||||
[run]
|
|
||||||
source = .
|
|
||||||
|
|
||||||
[report]
|
|
||||||
fail_under = 100
|
|
||||||
show_missing = True
|
|
||||||
skip_covered = True
|
|
||||||
omit =
|
|
||||||
*/tests/*
|
|
||||||
*/migrations/*
|
|
||||||
core/asgi.py
|
|
||||||
core/wsgi.py
|
|
||||||
core/settings.py
|
|
||||||
manage.py
|
|
129
core/.gitignore
vendored
129
core/.gitignore
vendored
|
@ -1,129 +0,0 @@
|
||||||
# Byte-compiled / optimized / DLL files
|
|
||||||
__pycache__/
|
|
||||||
*.py[cod]
|
|
||||||
*$py.class
|
|
||||||
|
|
||||||
# C extensions
|
|
||||||
*.so
|
|
||||||
|
|
||||||
# Distribution / packaging
|
|
||||||
.Python
|
|
||||||
build/
|
|
||||||
develop-eggs/
|
|
||||||
dist/
|
|
||||||
downloads/
|
|
||||||
eggs/
|
|
||||||
.eggs/
|
|
||||||
lib/
|
|
||||||
lib64/
|
|
||||||
parts/
|
|
||||||
sdist/
|
|
||||||
var/
|
|
||||||
wheels/
|
|
||||||
pip-wheel-metadata/
|
|
||||||
share/python-wheels/
|
|
||||||
*.egg-info/
|
|
||||||
.installed.cfg
|
|
||||||
*.egg
|
|
||||||
MANIFEST
|
|
||||||
|
|
||||||
# PyInstaller
|
|
||||||
# Usually these files are written by a python script from a template
|
|
||||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
||||||
*.manifest
|
|
||||||
*.spec
|
|
||||||
|
|
||||||
# Installer logs
|
|
||||||
pip-log.txt
|
|
||||||
pip-delete-this-directory.txt
|
|
||||||
|
|
||||||
# Unit test / coverage reports
|
|
||||||
htmlcov/
|
|
||||||
.tox/
|
|
||||||
.nox/
|
|
||||||
.coverage
|
|
||||||
.coverage.*
|
|
||||||
.cache
|
|
||||||
nosetests.xml
|
|
||||||
coverage.xml
|
|
||||||
*.cover
|
|
||||||
*.py,cover
|
|
||||||
.hypothesis/
|
|
||||||
.pytest_cache/
|
|
||||||
|
|
||||||
# Translations
|
|
||||||
*.mo
|
|
||||||
*.pot
|
|
||||||
|
|
||||||
# Django stuff:
|
|
||||||
*.log
|
|
||||||
local_settings.py
|
|
||||||
db.sqlite3
|
|
||||||
db.sqlite3-journal
|
|
||||||
|
|
||||||
# Flask stuff:
|
|
||||||
instance/
|
|
||||||
.webassets-cache
|
|
||||||
|
|
||||||
# Scrapy stuff:
|
|
||||||
.scrapy
|
|
||||||
|
|
||||||
# Sphinx documentation
|
|
||||||
docs/_build/
|
|
||||||
|
|
||||||
# PyBuilder
|
|
||||||
target/
|
|
||||||
|
|
||||||
# Jupyter Notebook
|
|
||||||
.ipynb_checkpoints
|
|
||||||
|
|
||||||
# IPython
|
|
||||||
profile_default/
|
|
||||||
ipython_config.py
|
|
||||||
|
|
||||||
# pyenv
|
|
||||||
.python-version
|
|
||||||
|
|
||||||
# pipenv
|
|
||||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
||||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
||||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
||||||
# install all needed dependencies.
|
|
||||||
#Pipfile.lock
|
|
||||||
|
|
||||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
|
||||||
__pypackages__/
|
|
||||||
|
|
||||||
# Celery stuff
|
|
||||||
celerybeat-schedule
|
|
||||||
celerybeat.pid
|
|
||||||
|
|
||||||
# SageMath parsed files
|
|
||||||
*.sage.py
|
|
||||||
|
|
||||||
# Environments
|
|
||||||
.env
|
|
||||||
.venv
|
|
||||||
env/
|
|
||||||
venv/
|
|
||||||
ENV/
|
|
||||||
env.bak/
|
|
||||||
venv.bak/
|
|
||||||
|
|
||||||
# Spyder project settings
|
|
||||||
.spyderproject
|
|
||||||
.spyproject
|
|
||||||
|
|
||||||
# Rope project settings
|
|
||||||
.ropeproject
|
|
||||||
|
|
||||||
# mkdocs documentation
|
|
||||||
/site
|
|
||||||
|
|
||||||
# mypy
|
|
||||||
.mypy_cache/
|
|
||||||
.dmypy.json
|
|
||||||
dmypy.json
|
|
||||||
|
|
||||||
# Pyre type checker
|
|
||||||
.pyre/
|
|
|
@ -1,14 +0,0 @@
|
||||||
from django.contrib import admin
|
|
||||||
from django.contrib.auth.admin import UserAdmin
|
|
||||||
|
|
||||||
from authentication.models import ExtendedUser
|
|
||||||
|
|
||||||
|
|
||||||
class ExtendedUserAdmin(UserAdmin):
|
|
||||||
list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff', 'is_superuser')
|
|
||||||
search_fields = ('username', 'email', 'first_name', 'last_name')
|
|
||||||
ordering = ('username',)
|
|
||||||
filter_horizontal = ('groups', 'user_permissions', 'permissions')
|
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(ExtendedUser, ExtendedUserAdmin)
|
|
|
@ -1,116 +0,0 @@
|
||||||
from rest_framework import routers, viewsets, serializers, permissions
|
|
||||||
from rest_framework.decorators import api_view, permission_classes, authentication_classes
|
|
||||||
from rest_framework.authtoken.serializers import AuthTokenSerializer
|
|
||||||
from rest_framework.permissions import IsAuthenticated
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from django.contrib.auth import login
|
|
||||||
from django.urls import path
|
|
||||||
from django.dispatch import receiver
|
|
||||||
from django.db.models.signals import post_save
|
|
||||||
from django.contrib.auth.models import Group
|
|
||||||
from knox.models import AuthToken
|
|
||||||
from knox.views import LoginView as KnoxLoginView
|
|
||||||
|
|
||||||
from authentication.models import ExtendedUser
|
|
||||||
|
|
||||||
|
|
||||||
class UserSerializer(serializers.ModelSerializer):
|
|
||||||
permissions = serializers.SerializerMethodField()
|
|
||||||
groups = serializers.SlugRelatedField(many=True, read_only=True, slug_field='name')
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = ExtendedUser
|
|
||||||
fields = ('id', 'username', 'email', 'first_name', 'last_name', 'permissions', 'groups')
|
|
||||||
read_only_fields = ('id', 'username', 'email', 'first_name', 'last_name', 'permissions', 'groups')
|
|
||||||
|
|
||||||
def get_permissions(self, obj):
|
|
||||||
return list(set(obj.get_permissions()))
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=ExtendedUser)
|
|
||||||
def create_auth_token(sender, instance=None, created=False, **kwargs):
|
|
||||||
if created:
|
|
||||||
AuthToken.objects.create(user=instance)
|
|
||||||
|
|
||||||
|
|
||||||
class UserViewSet(viewsets.ModelViewSet):
|
|
||||||
queryset = ExtendedUser.objects.all()
|
|
||||||
serializer_class = UserSerializer
|
|
||||||
|
|
||||||
|
|
||||||
class GroupSerializer(serializers.ModelSerializer):
|
|
||||||
permissions = serializers.SerializerMethodField()
|
|
||||||
members = serializers.SerializerMethodField()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Group
|
|
||||||
fields = ('id', 'name', 'permissions', 'members')
|
|
||||||
|
|
||||||
def get_permissions(self, obj):
|
|
||||||
return ["*:" + p.codename for p in obj.permissions.all()]
|
|
||||||
|
|
||||||
def get_members(self, obj):
|
|
||||||
return [u.username for u in obj.user_set.all()]
|
|
||||||
|
|
||||||
|
|
||||||
class GroupViewSet(viewsets.ModelViewSet):
|
|
||||||
queryset = Group.objects.all()
|
|
||||||
serializer_class = GroupSerializer
|
|
||||||
|
|
||||||
|
|
||||||
@api_view(['GET'])
|
|
||||||
@permission_classes([IsAuthenticated])
|
|
||||||
def selfUser(request):
|
|
||||||
serializer = UserSerializer(request.user)
|
|
||||||
return Response(serializer.data, status=200)
|
|
||||||
|
|
||||||
|
|
||||||
@api_view(['POST'])
|
|
||||||
@permission_classes([])
|
|
||||||
@authentication_classes([])
|
|
||||||
def registerUser(request):
|
|
||||||
try:
|
|
||||||
username = request.data.get('username')
|
|
||||||
password = request.data.get('password')
|
|
||||||
email = request.data.get('email')
|
|
||||||
|
|
||||||
errors = {}
|
|
||||||
if not username:
|
|
||||||
errors['username'] = 'Username is required'
|
|
||||||
if not password:
|
|
||||||
errors['password'] = 'Password is required'
|
|
||||||
if not email:
|
|
||||||
errors['email'] = 'Email is required'
|
|
||||||
if ExtendedUser.objects.filter(email=email).exists():
|
|
||||||
errors['email'] = 'Email already exists'
|
|
||||||
if ExtendedUser.objects.filter(username=username).exists():
|
|
||||||
errors['username'] = 'Username already exists'
|
|
||||||
if errors:
|
|
||||||
return Response({'errors': errors}, status=400)
|
|
||||||
user = ExtendedUser.objects.create_user(username, email, password)
|
|
||||||
return Response({'username': user.username, 'email': user.email}, status=201)
|
|
||||||
except Exception as e:
|
|
||||||
return Response({'errors': str(e)}, status=400)
|
|
||||||
|
|
||||||
|
|
||||||
class LoginView(KnoxLoginView):
|
|
||||||
permission_classes = (permissions.AllowAny,)
|
|
||||||
authentication_classes = ()
|
|
||||||
|
|
||||||
def post(self, request, format=None):
|
|
||||||
serializer = AuthTokenSerializer(data=request.data)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
|
||||||
user = serializer.validated_data['user']
|
|
||||||
login(request, user)
|
|
||||||
return super(LoginView, self).post(request, format=None)
|
|
||||||
|
|
||||||
|
|
||||||
router = routers.SimpleRouter()
|
|
||||||
router.register(r'users', UserViewSet, basename='users')
|
|
||||||
router.register(r'groups', GroupViewSet, basename='groups')
|
|
||||||
|
|
||||||
urlpatterns = router.urls + [
|
|
||||||
path('self/', selfUser),
|
|
||||||
path('login/', LoginView.as_view()),
|
|
||||||
path('register/', registerUser),
|
|
||||||
]
|
|
|
@ -1,67 +0,0 @@
|
||||||
# Generated by Django 4.2.7 on 2023-12-11 21:10
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
import django.contrib.auth.models
|
|
||||||
import django.contrib.auth.validators
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
import django.utils.timezone
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('auth', '0012_alter_user_first_name_max_length'),
|
|
||||||
('inventory', '0001_initial'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='ExtendedUser',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('password', models.CharField(max_length=128, verbose_name='password')),
|
|
||||||
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
|
||||||
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
|
||||||
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
|
||||||
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
|
||||||
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
|
||||||
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
|
|
||||||
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
|
||||||
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
|
||||||
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
|
||||||
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'Extended user',
|
|
||||||
'verbose_name_plural': 'Extended users',
|
|
||||||
},
|
|
||||||
managers=[
|
|
||||||
('objects', django.contrib.auth.models.UserManager()),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='EventPermission',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.event')),
|
|
||||||
('permission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.permission')),
|
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'unique_together': {('user', 'permission', 'event')},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='extendeduser',
|
|
||||||
name='permissions',
|
|
||||||
field=models.ManyToManyField(through='authentication.EventPermission', to='auth.permission'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='extendeduser',
|
|
||||||
name='user_permissions',
|
|
||||||
field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions'),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,45 +0,0 @@
|
||||||
# Generated by Django 4.2.7 on 2023-12-11 21:11
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('inventory', '0001_initial'),
|
|
||||||
('knox', '0008_remove_authtoken_salt'),
|
|
||||||
('authentication', '0001_initial'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='AuthTokenEventPermissions',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.event')),
|
|
||||||
('permission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='authentication.eventpermission')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='ExtendedAuthToken',
|
|
||||||
fields=[
|
|
||||||
('authtoken_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='knox.authtoken')),
|
|
||||||
('permissions', models.ManyToManyField(through='authentication.AuthTokenEventPermissions', to='authentication.eventpermission')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'Extended auth token',
|
|
||||||
'verbose_name_plural': 'Extended auth tokens',
|
|
||||||
},
|
|
||||||
bases=('knox.authtoken',),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='authtokeneventpermissions',
|
|
||||||
name='token',
|
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='authentication.extendedauthtoken'),
|
|
||||||
),
|
|
||||||
migrations.AlterUniqueTogether(
|
|
||||||
name='authtokeneventpermissions',
|
|
||||||
unique_together={('token', 'permission', 'event')},
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,33 +0,0 @@
|
||||||
# Generated by Django 4.2.7 on 2023-11-26 00:16
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations
|
|
||||||
from django.contrib.auth.models import Permission, Group
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
('authentication', '0002_authtokeneventpermissions_extendedauthtoken_and_more'),
|
|
||||||
('tickets', '0001_initial'),
|
|
||||||
]
|
|
||||||
|
|
||||||
def create_groups(apps, schema_editor):
|
|
||||||
admins = Group.objects.create(name='Admin')
|
|
||||||
orga = Group.objects.create(name='Orga')
|
|
||||||
team = Group.objects.create(name='Team')
|
|
||||||
users = Group.objects.create(name='User')
|
|
||||||
admins.permissions.add(*Permission.objects.all())
|
|
||||||
users.permissions.add(*Permission.objects.filter(codename__in=
|
|
||||||
['view_item', 'add_item', 'change_item', 'match_item']))
|
|
||||||
team.permissions.add(*Permission.objects.filter(codename__in=
|
|
||||||
['delete_item', 'view_issuethread', 'add_issuethread',
|
|
||||||
'change_issuethread', 'delete_issuethread', 'send_mail']),
|
|
||||||
*users.permissions.all())
|
|
||||||
orga.permissions.add(*Permission.objects.filter(codename__in=['add_event']),
|
|
||||||
*team.permissions.all())
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RunPython(create_groups),
|
|
||||||
]
|
|
|
@ -1,18 +0,0 @@
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
('authentication', '0003_groups'),
|
|
||||||
]
|
|
||||||
|
|
||||||
def create_legacy_user(apps, schema_editor):
|
|
||||||
ExtendedUser = apps.get_model('authentication', 'ExtendedUser')
|
|
||||||
ExtendedUser.objects.create_user(settings.LEGACY_USER_NAME, 'mail@' + settings.MAIL_DOMAIN,
|
|
||||||
settings.LEGACY_USER_PASSWORD)
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RunPython(create_legacy_user)
|
|
||||||
]
|
|
|
@ -1,26 +0,0 @@
|
||||||
# Generated by Django 4.2.7 on 2023-12-13 16:28
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('inventory', '0001_initial'),
|
|
||||||
('authentication', '0004_legacy_user'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='eventpermission',
|
|
||||||
name='event',
|
|
||||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='inventory.event'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='eventpermission',
|
|
||||||
name='user',
|
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='event_permissions', to=settings.AUTH_USER_MODEL),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,62 +0,0 @@
|
||||||
from django.db import models
|
|
||||||
from django.contrib.auth.models import Permission, AbstractUser
|
|
||||||
from knox.models import AuthToken
|
|
||||||
|
|
||||||
from inventory.models import Event
|
|
||||||
|
|
||||||
|
|
||||||
class ExtendedUser(AbstractUser):
|
|
||||||
permissions = models.ManyToManyField(Permission, through='EventPermission', through_fields=('user', 'permission'))
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = 'Extended user'
|
|
||||||
verbose_name_plural = 'Extended users'
|
|
||||||
|
|
||||||
def get_permissions(self):
|
|
||||||
if self.is_superuser:
|
|
||||||
for permission in Permission.objects.all():
|
|
||||||
yield "*:" + permission.codename
|
|
||||||
for permission in self.user_permissions.all():
|
|
||||||
yield "*:" + permission.codename
|
|
||||||
for group in self.groups.all():
|
|
||||||
for permission in group.permissions.all():
|
|
||||||
yield "*:" + permission.codename
|
|
||||||
for permission in self.event_permissions.all():
|
|
||||||
yield permission.event.slug + ":" + permission.permission.codename
|
|
||||||
|
|
||||||
def has_event_perm(self, event, permission):
|
|
||||||
if self.is_superuser:
|
|
||||||
return True
|
|
||||||
permissions = set(self.get_permissions())
|
|
||||||
if "*:" + permission in permissions:
|
|
||||||
return True
|
|
||||||
if event.slug + ":" + permission in permissions:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class ExtendedAuthToken(AuthToken):
|
|
||||||
permissions = models.ManyToManyField('EventPermission', through='AuthTokenEventPermissions',
|
|
||||||
through_fields=('token', 'permission'))
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = 'Extended auth token'
|
|
||||||
verbose_name_plural = 'Extended auth tokens'
|
|
||||||
|
|
||||||
|
|
||||||
class EventPermission(models.Model):
|
|
||||||
user = models.ForeignKey(ExtendedUser, on_delete=models.CASCADE, related_name='event_permissions')
|
|
||||||
permission = models.ForeignKey(Permission, on_delete=models.CASCADE)
|
|
||||||
event = models.ForeignKey(Event, on_delete=models.CASCADE, null=True, blank=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
unique_together = ('user', 'permission', 'event')
|
|
||||||
|
|
||||||
|
|
||||||
class AuthTokenEventPermissions(models.Model):
|
|
||||||
token = models.ForeignKey(ExtendedAuthToken, on_delete=models.CASCADE)
|
|
||||||
permission = models.ForeignKey(EventPermission, on_delete=models.CASCADE)
|
|
||||||
event = models.ForeignKey(Event, on_delete=models.CASCADE)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
unique_together = ('token', 'permission', 'event')
|
|
|
@ -1,90 +0,0 @@
|
||||||
from django.test import TestCase, Client
|
|
||||||
from django.contrib.auth.models import Permission
|
|
||||||
from knox.models import AuthToken
|
|
||||||
|
|
||||||
from authentication.models import EventPermission, ExtendedUser
|
|
||||||
from inventory.models import Event
|
|
||||||
|
|
||||||
|
|
||||||
class PermissionsTestCase(TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
super().setUp()
|
|
||||||
self.user = ExtendedUser.objects.create_user('testuser', 'test', 'test')
|
|
||||||
self.user.user_permissions.add(*Permission.objects.all())
|
|
||||||
event1 = Event.objects.create(slug='testevent1', name='testevent1')
|
|
||||||
event2 = Event.objects.create(slug='testevent2', name='testevent2')
|
|
||||||
permission1 = Permission.objects.get(codename='view_event')
|
|
||||||
EventPermission.objects.create(user=self.user, permission=permission1, event=event1)
|
|
||||||
EventPermission.objects.create(user=self.user, permission=permission1, event=event2)
|
|
||||||
self.token = AuthToken.objects.create(user=self.user)
|
|
||||||
self.client = Client(headers={'Authorization': 'Token ' + self.token[1]})
|
|
||||||
self.newuser = ExtendedUser.objects.create_user('newuser', 'test', 'test')
|
|
||||||
self.newuser_token = AuthToken.objects.create(user=self.newuser)
|
|
||||||
self.newuser_client = Client(headers={'Authorization': 'Token ' + self.newuser_token[1]})
|
|
||||||
|
|
||||||
def test_user_permissions(self):
|
|
||||||
"""
|
|
||||||
Test that a user can only access their own data.
|
|
||||||
"""
|
|
||||||
response = self.client.get('/api/2/users/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(len(response.json()), 3)
|
|
||||||
self.assertEqual(response.json()[0]['username'], 'legacy_user')
|
|
||||||
self.assertEqual(response.json()[0]['email'], 'mail@localhost')
|
|
||||||
self.assertEqual(response.json()[0]['first_name'], '')
|
|
||||||
self.assertEqual(response.json()[0]['last_name'], '')
|
|
||||||
self.assertEqual(response.json()[0]['id'], 1)
|
|
||||||
self.assertEqual(response.json()[1]['username'], 'testuser')
|
|
||||||
self.assertEqual(response.json()[1]['email'], 'test')
|
|
||||||
self.assertEqual(response.json()[1]['first_name'], '')
|
|
||||||
self.assertEqual(response.json()[1]['last_name'], '')
|
|
||||||
|
|
||||||
def test_user_permission(self):
|
|
||||||
"""
|
|
||||||
Test that a user can only access their own data.
|
|
||||||
"""
|
|
||||||
#ä['add_logentry', 'change_logentry', 'delete_logentry', 'view_logentry', 'add_group', 'change_group',
|
|
||||||
#ä 'delete_group', 'view_group', 'add_permission', 'change_permission', 'delete_permission', 'view_permission',
|
|
||||||
#ä 'add_authtokeneventpermissions', 'change_authtokeneventpermissions', 'delete_authtokeneventpermissions',
|
|
||||||
#ä 'view_authtokeneventpermissions', 'add_eventpermission', 'change_eventpermission', 'delete_eventpermission',
|
|
||||||
#ä 'view_eventpermission', 'add_extendedauthtoken', 'change_extendedauthtoken', 'delete_extendedauthtoken',
|
|
||||||
#ä 'view_extendedauthtoken', 'add_extendeduser', 'change_extendeduser', 'delete_extendeduser',
|
|
||||||
#ä 'view_extendeduser', 'add_contenttype', 'change_contenttype', 'delete_contenttype', 'view_contenttype',
|
|
||||||
#ä 'add_file', 'change_file', 'delete_file', 'view_file', 'add_container', 'change_container', 'delete_container',
|
|
||||||
#ä 'view_container', 'add_event', 'change_event', 'delete_event', 'view_event', 'add_item', 'change_item',
|
|
||||||
#ä 'delete_item', 'match_item', 'view_item', 'add_authtoken', 'change_authtoken', 'delete_authtoken',
|
|
||||||
#ä 'view_authtoken', 'add_email', 'change_email', 'delete_email', 'view_email', 'add_eventaddress',
|
|
||||||
#ä 'change_eventaddress', 'delete_eventaddress', 'view_eventaddress', 'add_systemevent', 'change_systemevent',
|
|
||||||
#ä 'delete_systemevent', 'view_systemevent', 'add_session', 'change_session', 'delete_session', 'view_session',
|
|
||||||
#ä 'add_comment', 'change_comment', 'delete_comment', 'view_comment', 'add_issuethread', 'change_issuethread',
|
|
||||||
#ä 'delete_issuethread', 'send_mail', 'view_issuethread', 'add_statechange', 'change_statechange',
|
|
||||||
#ä 'delete_statechange', 'view_statechange']
|
|
||||||
|
|
||||||
user = ExtendedUser.objects.create_user('testuser2', 'test', 'test')
|
|
||||||
user.event_permissions.create(permission=Permission.objects.get(codename='view_item'), event=Event.objects.get(slug='testevent1'))
|
|
||||||
user.event_permissions.create(permission=Permission.objects.get(codename='view_item'), event=Event.objects.get(slug='testevent2'))
|
|
||||||
user.event_permissions.create(permission=Permission.objects.get(codename='add_item'), event=Event.objects.get(slug='testevent1'))
|
|
||||||
user.save()
|
|
||||||
#self.assertTrue(user.has_perm('inventory.view_event', Event.objects.get(slug='testevent1')))
|
|
||||||
#self.assertTrue(user.has_perm('inventory.view_event', Event.objects.get(slug='testevent2')))
|
|
||||||
#self.assertFalse(user.has_perm('inventory.add_event', Event.objects.get(slug='testevent1')))
|
|
||||||
#self.assertFalse(user.has_perm('inventory.add_event', Event.objects.get(slug='testevent2')))
|
|
||||||
|
|
||||||
def test_item_api_permissions(self):
|
|
||||||
"""
|
|
||||||
Test that a user can only access their own data.
|
|
||||||
"""
|
|
||||||
response = self.client.get('/api/2/testevent1/items/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(len(response.json()), 0)
|
|
||||||
|
|
||||||
response = self.client.get('/api/2/testevent2/items/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(len(response.json()), 0)
|
|
||||||
|
|
||||||
response = self.newuser_client.get('/api/2/testevent1/items/')
|
|
||||||
self.assertEqual(response.status_code, 403)
|
|
||||||
|
|
||||||
response = self.newuser_client.get('/api/2/testevent2/items/')
|
|
||||||
self.assertEqual(response.status_code, 403)
|
|
||||||
|
|
|
@ -1,183 +0,0 @@
|
||||||
from django.test import TestCase, Client
|
|
||||||
from django.contrib.auth.models import Permission, Group
|
|
||||||
|
|
||||||
from knox.models import AuthToken
|
|
||||||
|
|
||||||
from authentication.models import ExtendedUser, EventPermission
|
|
||||||
from core import settings
|
|
||||||
from inventory.models import Event
|
|
||||||
|
|
||||||
|
|
||||||
class UserApiTest(TestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.event = Event.objects.create(name='testevent', slug='testevent')
|
|
||||||
self.group1 = Group.objects.create(name='testgroup1')
|
|
||||||
self.group2 = Group.objects.create(name='testgroup2')
|
|
||||||
self.group1.permissions.add(Permission.objects.get(codename='add_item'))
|
|
||||||
self.group1.permissions.add(Permission.objects.get(codename='view_item'))
|
|
||||||
self.group2.permissions.add(Permission.objects.get(codename='view_event'))
|
|
||||||
self.group2.permissions.add(Permission.objects.get(codename='view_item'))
|
|
||||||
self.user = ExtendedUser.objects.create_user('testuser', 'test', 'test')
|
|
||||||
self.user.user_permissions.add(Permission.objects.get(codename='add_event'))
|
|
||||||
self.user.groups.add(self.group1)
|
|
||||||
self.user.groups.add(self.group2)
|
|
||||||
self.user.save()
|
|
||||||
EventPermission.objects.create(event=self.event, user=self.user,
|
|
||||||
permission=Permission.objects.get(codename='delete_item'))
|
|
||||||
self.user.save()
|
|
||||||
self.token = AuthToken.objects.create(user=self.user)
|
|
||||||
self.client = Client(headers={'Authorization': 'Token ' + self.token[1]})
|
|
||||||
|
|
||||||
def test_users(self):
|
|
||||||
response = self.client.get('/api/2/users/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(len(response.json()), 2)
|
|
||||||
self.assertEqual(response.json()[0]['username'], settings.LEGACY_USER_NAME)
|
|
||||||
self.assertEqual(response.json()[0]['email'], 'mail@' + settings.MAIL_DOMAIN)
|
|
||||||
self.assertEqual(response.json()[0]['first_name'], '')
|
|
||||||
self.assertEqual(response.json()[0]['last_name'], '')
|
|
||||||
self.assertEqual(response.json()[0]['id'], 1)
|
|
||||||
self.assertEqual(response.json()[0]['groups'], [])
|
|
||||||
self.assertEqual(response.json()[1]['username'], 'testuser')
|
|
||||||
self.assertEqual(response.json()[1]['email'], 'test')
|
|
||||||
self.assertEqual(response.json()[1]['first_name'], '')
|
|
||||||
self.assertEqual(response.json()[1]['last_name'], '')
|
|
||||||
self.assertEqual(response.json()[1]['id'], 2)
|
|
||||||
self.assertEqual(response.json()[1]['groups'], ['testgroup1', 'testgroup2'])
|
|
||||||
|
|
||||||
def test_self_user(self):
|
|
||||||
response = self.client.get('/api/2/self/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(response.json()['username'], 'testuser')
|
|
||||||
self.assertEqual(response.json()['email'], 'test')
|
|
||||||
self.assertEqual(response.json()['first_name'], '')
|
|
||||||
self.assertEqual(response.json()['last_name'], '')
|
|
||||||
permissions = response.json()['permissions']
|
|
||||||
self.assertEqual(len(permissions), 5)
|
|
||||||
self.assertTrue('*:add_item' in permissions)
|
|
||||||
self.assertTrue('*:view_item' in permissions)
|
|
||||||
self.assertTrue('*:view_event' in permissions)
|
|
||||||
self.assertTrue('testevent:delete_item' in permissions)
|
|
||||||
self.assertTrue('*:add_event' in permissions)
|
|
||||||
|
|
||||||
def test_register_user(self):
|
|
||||||
anonymous = Client()
|
|
||||||
response = anonymous.post('/api/2/register/', {'username': 'testuser2', 'password': 'test', 'email': 'test2'},
|
|
||||||
content_type='application/json')
|
|
||||||
self.assertEqual(response.status_code, 201)
|
|
||||||
self.assertEqual(response.json()['username'], 'testuser2')
|
|
||||||
self.assertEqual(response.json()['email'], 'test2')
|
|
||||||
self.assertEqual(len(ExtendedUser.objects.all()), 3)
|
|
||||||
self.assertEqual(ExtendedUser.objects.get(username='testuser2').email, 'test2')
|
|
||||||
self.assertTrue(ExtendedUser.objects.get(username='testuser2').check_password('test'))
|
|
||||||
|
|
||||||
def test_register_user_duplicate(self):
|
|
||||||
anonymous = Client()
|
|
||||||
response = anonymous.post('/api/2/register/', {'username': 'testuser', 'password': 'test', 'email': 'test2'},
|
|
||||||
content_type='application/json')
|
|
||||||
self.assertEqual(response.status_code, 400)
|
|
||||||
self.assertEqual(response.json()['errors']['username'], 'Username already exists')
|
|
||||||
self.assertEqual(len(ExtendedUser.objects.all()), 2)
|
|
||||||
|
|
||||||
def test_register_user_no_username(self):
|
|
||||||
anonymous = Client()
|
|
||||||
response = anonymous.post('/api/2/register/', {'password': 'test', 'email': 'test2'},
|
|
||||||
content_type='application/json')
|
|
||||||
self.assertEqual(response.status_code, 400)
|
|
||||||
self.assertEqual(response.json()['errors']['username'], 'Username is required')
|
|
||||||
self.assertEqual(len(ExtendedUser.objects.all()), 2)
|
|
||||||
|
|
||||||
def test_register_user_no_password(self):
|
|
||||||
anonymous = Client()
|
|
||||||
response = anonymous.post('/api/2/register/', {'username': 'testuser2', 'email': 'test2'},
|
|
||||||
content_type='application/json')
|
|
||||||
self.assertEqual(response.status_code, 400)
|
|
||||||
self.assertEqual(response.json()['errors']['password'], 'Password is required')
|
|
||||||
self.assertEqual(len(ExtendedUser.objects.all()), 2)
|
|
||||||
|
|
||||||
def test_register_user_no_email(self):
|
|
||||||
anonymous = Client()
|
|
||||||
response = anonymous.post('/api/2/register/', {'username': 'testuser2', 'password': 'test'},
|
|
||||||
content_type='application/json')
|
|
||||||
self.assertEqual(response.status_code, 400)
|
|
||||||
self.assertEqual(response.json()['errors']['email'], 'Email is required')
|
|
||||||
self.assertEqual(len(ExtendedUser.objects.all()), 2)
|
|
||||||
|
|
||||||
def test_register_user_duplicate_email(self):
|
|
||||||
anonymous = Client()
|
|
||||||
response = anonymous.post('/api/2/register/', {'username': 'testuser2', 'password': 'test', 'email': 'test'},
|
|
||||||
content_type='application/json')
|
|
||||||
self.assertEqual(response.status_code, 400)
|
|
||||||
self.assertEqual(response.json()['errors']['email'], 'Email already exists')
|
|
||||||
self.assertEqual(len(ExtendedUser.objects.all()), 2)
|
|
||||||
|
|
||||||
def test_get_token(self):
|
|
||||||
anonymous = Client()
|
|
||||||
response = anonymous.post('/api/2/login/', {'username': 'testuser', 'password': 'test'},
|
|
||||||
content_type='application/json')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertTrue('token' in response.json())
|
|
||||||
|
|
||||||
def test_legacy_user(self):
|
|
||||||
response = self.client.get('/api/2/users/1/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(response.json()['username'], settings.LEGACY_USER_NAME)
|
|
||||||
self.assertEqual(response.json()['email'], 'mail@' + settings.MAIL_DOMAIN)
|
|
||||||
self.assertEqual(response.json()['first_name'], '')
|
|
||||||
self.assertEqual(response.json()['last_name'], '')
|
|
||||||
self.assertEqual(response.json()['id'], 1)
|
|
||||||
|
|
||||||
def test_get_legacy_user_token(self):
|
|
||||||
anonymous = Client()
|
|
||||||
response = anonymous.post('/api/2/login/', {
|
|
||||||
'username': settings.LEGACY_USER_NAME, 'password': settings.LEGACY_USER_PASSWORD},
|
|
||||||
content_type='application/json')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertTrue('token' in response.json())
|
|
||||||
|
|
||||||
|
|
||||||
class GroupApiTest(TestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.event = Event.objects.create(name='testevent', slug='testevent')
|
|
||||||
# Admin, Orga, Team, User are created by default
|
|
||||||
self.group1 = Group.objects.create(name='testgroup1')
|
|
||||||
self.group2 = Group.objects.create(name='testgroup2')
|
|
||||||
self.group1.permissions.add(Permission.objects.get(codename='add_item'))
|
|
||||||
self.group1.permissions.add(Permission.objects.get(codename='view_item'))
|
|
||||||
self.group2.permissions.add(Permission.objects.get(codename='view_event'))
|
|
||||||
self.group2.permissions.add(Permission.objects.get(codename='view_item'))
|
|
||||||
self.user = ExtendedUser.objects.create_user('testuser', 'test', 'test')
|
|
||||||
self.user.user_permissions.add(Permission.objects.get(codename='add_event'))
|
|
||||||
self.user.groups.add(self.group1)
|
|
||||||
self.user.groups.add(self.group2)
|
|
||||||
self.user.save()
|
|
||||||
EventPermission.objects.create(event=self.event, user=self.user,
|
|
||||||
permission=Permission.objects.get(codename='delete_item'))
|
|
||||||
self.user.save()
|
|
||||||
self.token = AuthToken.objects.create(user=self.user)
|
|
||||||
self.client = Client(headers={'Authorization': 'Token ' + self.token[1]})
|
|
||||||
|
|
||||||
def test_groups(self):
|
|
||||||
response = self.client.get('/api/2/groups/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(len(response.json()), 6)
|
|
||||||
self.assertEqual(response.json()[0]['name'], 'Admin')
|
|
||||||
self.assertEqual(response.json()[1]['name'], 'Orga')
|
|
||||||
self.assertEqual(response.json()[2]['name'], 'Team')
|
|
||||||
self.assertEqual(response.json()[3]['name'], 'User')
|
|
||||||
self.assertEqual(response.json()[4]['name'], 'testgroup1')
|
|
||||||
self.assertEqual(response.json()[5]['name'], 'testgroup2')
|
|
||||||
|
|
||||||
def test_group(self):
|
|
||||||
response = self.client.get('/api/2/groups/5/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(response.json()['name'], 'testgroup1')
|
|
||||||
permissions = response.json()['permissions']
|
|
||||||
self.assertEqual(len(permissions), 2)
|
|
||||||
self.assertTrue('*:add_item' in permissions)
|
|
||||||
self.assertTrue('*:view_item' in permissions)
|
|
||||||
members = response.json()['members']
|
|
||||||
self.assertEqual(len(members), 1)
|
|
||||||
self.assertEqual(members[0], 'testuser')
|
|
|
@ -1,66 +0,0 @@
|
||||||
"""
|
|
||||||
ASGI config for core project.
|
|
||||||
|
|
||||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
|
||||||
|
|
||||||
For more information on this file, see
|
|
||||||
https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from channels.auth import AuthMiddlewareStack
|
|
||||||
from channels.routing import ProtocolTypeRouter, URLRouter
|
|
||||||
from channels.security.websocket import AllowedHostsOriginValidator
|
|
||||||
from django.core.asgi import get_asgi_application
|
|
||||||
from notify_sessions.routing import websocket_urlpatterns
|
|
||||||
|
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
|
|
||||||
|
|
||||||
django_asgi_app = get_asgi_application()
|
|
||||||
|
|
||||||
|
|
||||||
class TokenAuthMiddleware:
|
|
||||||
"""
|
|
||||||
Token authorization middleware for Django Channels 2
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, inner):
|
|
||||||
self.inner = inner
|
|
||||||
|
|
||||||
def __call__(self, scope):
|
|
||||||
import base64
|
|
||||||
headers = dict(scope['headers'])
|
|
||||||
if b'authorization' in headers:
|
|
||||||
try:
|
|
||||||
token_name, token_key = headers[b'authorization'].decode().split()
|
|
||||||
if token_name == 'Basic':
|
|
||||||
b64 = base64.b64decode(token_key)
|
|
||||||
user = b64.decode().split(':')[0]
|
|
||||||
password = b64.decode().split(':')[1]
|
|
||||||
print(user, password)
|
|
||||||
else:
|
|
||||||
print("Token name is not Basic")
|
|
||||||
scope['user'] = None
|
|
||||||
except:
|
|
||||||
print("Token is not valid")
|
|
||||||
scope['user'] = None
|
|
||||||
else:
|
|
||||||
print("Token is not in headers")
|
|
||||||
scope['user'] = None
|
|
||||||
|
|
||||||
|
|
||||||
TokenAuthMiddlewareStack = lambda inner: TokenAuthMiddleware(AuthMiddlewareStack(inner))
|
|
||||||
|
|
||||||
websocket_asgi_app = AllowedHostsOriginValidator(
|
|
||||||
AuthMiddlewareStack(
|
|
||||||
URLRouter(
|
|
||||||
websocket_urlpatterns
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
application = ProtocolTypeRouter({
|
|
||||||
"http": django_asgi_app,
|
|
||||||
"websocket": websocket_asgi_app,
|
|
||||||
})
|
|
|
@ -1,29 +0,0 @@
|
||||||
import asyncio
|
|
||||||
import logging
|
|
||||||
import signal
|
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
|
|
||||||
|
|
||||||
def create_task(coro):
|
|
||||||
global loop
|
|
||||||
loop.create_task(coro)
|
|
||||||
|
|
||||||
|
|
||||||
async def shutdown(sig, loop):
|
|
||||||
log = logging.getLogger()
|
|
||||||
log.info(f"Received exit signal {sig.name}...")
|
|
||||||
tasks = [t for t in asyncio.all_tasks() if t is not
|
|
||||||
asyncio.current_task()]
|
|
||||||
[task.cancel() for task in tasks]
|
|
||||||
log.info(f"Cancelling {len(tasks)} outstanding tasks")
|
|
||||||
await asyncio.wait_for(loop.shutdown_asyncgens(), timeout=10)
|
|
||||||
loop.stop()
|
|
||||||
log.info("Shutdown complete.")
|
|
||||||
|
|
||||||
|
|
||||||
def init_loop():
|
|
||||||
global loop
|
|
||||||
loop.add_signal_handler(signal.SIGTERM, lambda: asyncio.create_task(shutdown(signal.SIGTERM, loop)))
|
|
||||||
loop.add_signal_handler(signal.SIGINT, lambda: asyncio.create_task(shutdown(signal.SIGINT, loop)))
|
|
||||||
return loop
|
|
|
@ -1,223 +0,0 @@
|
||||||
"""
|
|
||||||
Django settings for core project.
|
|
||||||
|
|
||||||
Generated by 'django-admin startproject' using Django 4.2.7.
|
|
||||||
|
|
||||||
For more information on this file, see
|
|
||||||
https://docs.djangoproject.com/en/4.2/topics/settings/
|
|
||||||
|
|
||||||
For the full list of settings and their values, see
|
|
||||||
https://docs.djangoproject.com/en/4.2/ref/settings/
|
|
||||||
"""
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
import dotenv
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
def truthy_str(s):
|
|
||||||
return s.lower() in ['true', '1', 't', 'y', 'yes', 'yeah', 'yup', 'certainly', 'sure', 'positive', 'uh-huh', '👍']
|
|
||||||
|
|
||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
|
||||||
|
|
||||||
dotenv.load_dotenv(BASE_DIR / '.env')
|
|
||||||
|
|
||||||
# Quick-start development settings - unsuitable for production
|
|
||||||
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
|
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
|
||||||
SECRET_KEY = os.getenv('DJANGO_SECRET_KEY', 'django-insecure-tm*$w_14iqbiy-!7(8#ba7j+_@(7@rf2&a^!=shs&$03b%2*rv')
|
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
|
||||||
DEBUG = truthy_str(os.getenv('DEBUG_MODE_ACTIVE', 'False'))
|
|
||||||
|
|
||||||
ALLOWED_HOSTS = [os.getenv('HTTP_HOST', 'localhost')]
|
|
||||||
|
|
||||||
MAIL_DOMAIN = os.getenv('MAIL_DOMAIN', 'localhost')
|
|
||||||
|
|
||||||
CSRF_TRUSTED_ORIGINS = ["https://" + host for host in ALLOWED_HOSTS]
|
|
||||||
|
|
||||||
LEGACY_USER_NAME = os.getenv('LEGACY_API_USER', 'legacy_user')
|
|
||||||
LEGACY_USER_PASSWORD = os.getenv('LEGACY_API_PASSWORD', 'legacy_password')
|
|
||||||
|
|
||||||
SYSTEM3_VERSION = "0.0.0-dev.0"
|
|
||||||
|
|
||||||
ACTIVE_SPAM_TRAINING = truthy_str(os.getenv('ACTIVE_SPAM_TRAINING', 'False'))
|
|
||||||
|
|
||||||
# Application definition
|
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
|
||||||
'django.contrib.admin',
|
|
||||||
'django.contrib.auth',
|
|
||||||
'django.contrib.contenttypes',
|
|
||||||
'django.contrib.sessions',
|
|
||||||
'django.contrib.messages',
|
|
||||||
'django.contrib.staticfiles',
|
|
||||||
'django_extensions',
|
|
||||||
'django_prometheus',
|
|
||||||
'rest_framework',
|
|
||||||
'knox',
|
|
||||||
'drf_yasg',
|
|
||||||
'channels',
|
|
||||||
'authentication',
|
|
||||||
'files',
|
|
||||||
'tickets',
|
|
||||||
'inventory',
|
|
||||||
'mail',
|
|
||||||
'notify_sessions',
|
|
||||||
]
|
|
||||||
|
|
||||||
REST_FRAMEWORK = {
|
|
||||||
'TEST_REQUEST_DEFAULT_FORMAT': 'json',
|
|
||||||
'DEFAULT_AUTHENTICATION_CLASSES': ('knox.auth.TokenAuthentication',),
|
|
||||||
'DEFAULT_PERMISSION_CLASSES': (
|
|
||||||
'rest_framework.permissions.IsAuthenticated', 'rest_framework.permissions.DjangoModelPermissions'),
|
|
||||||
}
|
|
||||||
|
|
||||||
AUTH_USER_MODEL = 'authentication.ExtendedUser'
|
|
||||||
|
|
||||||
SWAGGER_SETTINGS = {
|
|
||||||
'SECURITY_DEFINITIONS': {
|
|
||||||
'api_key': {
|
|
||||||
'type': 'apiKey',
|
|
||||||
'in': 'header',
|
|
||||||
'name': 'Authorization'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'USE_SESSION_AUTH': False,
|
|
||||||
'JSON_EDITOR': True,
|
|
||||||
'DEFAULT_INFO': 'core.urls.openapi_info',
|
|
||||||
}
|
|
||||||
|
|
||||||
MIDDLEWARE = [
|
|
||||||
'django_prometheus.middleware.PrometheusBeforeMiddleware',
|
|
||||||
'django.middleware.security.SecurityMiddleware',
|
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
|
||||||
'django.middleware.common.CommonMiddleware',
|
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
|
||||||
'django_prometheus.middleware.PrometheusAfterMiddleware',
|
|
||||||
]
|
|
||||||
|
|
||||||
ROOT_URLCONF = 'core.urls'
|
|
||||||
|
|
||||||
TEMPLATES = [
|
|
||||||
{
|
|
||||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
|
||||||
'DIRS': [BASE_DIR / 'templates'],
|
|
||||||
'APP_DIRS': True,
|
|
||||||
'OPTIONS': {
|
|
||||||
'context_processors': [
|
|
||||||
'django.template.context_processors.debug',
|
|
||||||
'django.template.context_processors.request',
|
|
||||||
'django.contrib.auth.context_processors.auth',
|
|
||||||
'django.contrib.messages.context_processors.messages',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
WSGI_APPLICATION = 'core.wsgi.application'
|
|
||||||
|
|
||||||
# Database
|
|
||||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
|
|
||||||
|
|
||||||
if 'test' in sys.argv:
|
|
||||||
DATABASES = {
|
|
||||||
'default': {
|
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
|
||||||
'NAME': ':memory:',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
DATABASES = {
|
|
||||||
'default': {
|
|
||||||
'ENGINE': 'django.db.backends.mysql',
|
|
||||||
'HOST': os.getenv('DB_HOST', 'localhost'),
|
|
||||||
'PORT': os.getenv('DB_PORT', '3306'),
|
|
||||||
'NAME': os.getenv('DB_NAME', 'system3'),
|
|
||||||
'USER': os.getenv('DB_USER', 'system3'),
|
|
||||||
'PASSWORD': os.getenv('DB_PASSWORD', 'system3'),
|
|
||||||
'OPTIONS': {
|
|
||||||
'charset': 'utf8mb4'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Password validation
|
|
||||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
|
|
||||||
|
|
||||||
AUTH_PASSWORD_VALIDATORS = [
|
|
||||||
{
|
|
||||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
# Internationalization
|
|
||||||
# https://docs.djangoproject.com/en/4.2/topics/i18n/
|
|
||||||
|
|
||||||
LANGUAGE_CODE = 'en-us'
|
|
||||||
|
|
||||||
TIME_ZONE = 'UTC'
|
|
||||||
|
|
||||||
USE_I18N = True
|
|
||||||
|
|
||||||
USE_TZ = True
|
|
||||||
|
|
||||||
# Static files (CSS, JavaScript, Images)
|
|
||||||
# https://docs.djangoproject.com/en/4.2/howto/static-files/
|
|
||||||
|
|
||||||
STATIC_ROOT = os.getenv('STATIC_ROOT', 'staticfiles')
|
|
||||||
STATIC_URL = '/static/'
|
|
||||||
|
|
||||||
MEDIA_ROOT = os.getenv('MEDIA_ROOT', 'userfiles')
|
|
||||||
MEDIA_URL = '/media/'
|
|
||||||
|
|
||||||
STORAGES = {
|
|
||||||
'default': {
|
|
||||||
'BACKEND': 'django.core.files.storage.FileSystemStorage',
|
|
||||||
'OPTIONS': {
|
|
||||||
'base_url': MEDIA_URL,
|
|
||||||
'location': BASE_DIR / MEDIA_ROOT
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'staticfiles': {
|
|
||||||
'BACKEND': 'django.core.files.storage.FileSystemStorage',
|
|
||||||
'OPTIONS': {
|
|
||||||
'base_url': STATIC_URL,
|
|
||||||
'location': BASE_DIR / STATIC_ROOT
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
DATA_UPLOAD_MAX_MEMORY_SIZE = 1024 * 1024 * 128 # 128 MB
|
|
||||||
|
|
||||||
# Default primary key field type
|
|
||||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
|
|
||||||
|
|
||||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
|
||||||
|
|
||||||
CHANNEL_LAYERS = {
|
|
||||||
'default': {
|
|
||||||
'BACKEND': 'channels_redis.core.RedisChannelLayer',
|
|
||||||
'CONFIG': {
|
|
||||||
'hosts': [(os.getenv('REDIS_HOST', 'localhost'), 6379)],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
PROMETHEUS_METRIC_NAMESPACE = 'c3lf'
|
|
||||||
|
|
||||||
TEST_RUNNER = 'core.test_runner.FastTestRunner'
|
|
|
@ -1,33 +0,0 @@
|
||||||
from django.conf import settings
|
|
||||||
from django.test.runner import DiscoverRunner
|
|
||||||
|
|
||||||
|
|
||||||
class FastTestRunner(DiscoverRunner):
|
|
||||||
def setup_test_environment(self):
|
|
||||||
super(FastTestRunner, self).setup_test_environment()
|
|
||||||
# Don't write files
|
|
||||||
settings.STORAGES = {
|
|
||||||
'default': {
|
|
||||||
'BACKEND': 'django.core.files.storage.InMemoryStorage',
|
|
||||||
'OPTIONS': {
|
|
||||||
'base_url': '/media/',
|
|
||||||
'location': '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
# Bonus: Use a faster password hasher. This REALLY helps.
|
|
||||||
settings.PASSWORD_HASHERS = (
|
|
||||||
'django.contrib.auth.hashers.MD5PasswordHasher',
|
|
||||||
)
|
|
||||||
|
|
||||||
settings.CHANNEL_LAYERS = {
|
|
||||||
'default': {
|
|
||||||
'BACKEND': 'channels.layers.InMemoryChannelLayer'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
settings.DATABASES = {
|
|
||||||
'default': {
|
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
|
||||||
'NAME': ':memory:',
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,36 +0,0 @@
|
||||||
"""
|
|
||||||
URL configuration for core project.
|
|
||||||
|
|
||||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
|
||||||
https://docs.djangoproject.com/en/4.2/topics/http/urls/
|
|
||||||
Examples:
|
|
||||||
Function views
|
|
||||||
1. Add an import: from my_app import views
|
|
||||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
|
||||||
Class-based views
|
|
||||||
1. Add an import: from other_app.views import Home
|
|
||||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
|
||||||
Including another URLconf
|
|
||||||
1. Import the include() function: from django.urls import include, path
|
|
||||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
|
||||||
"""
|
|
||||||
from django.contrib import admin
|
|
||||||
from django.urls import path, include
|
|
||||||
|
|
||||||
from .version import get_info
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
path('djangoadmin/', admin.site.urls),
|
|
||||||
path('api/1/', include('inventory.api_v1')),
|
|
||||||
path('api/1/', include('files.api_v1')),
|
|
||||||
path('api/1/', include('files.media_v1')),
|
|
||||||
path('api/2/', include('inventory.api_v2')),
|
|
||||||
path('api/2/', include('files.api_v2')),
|
|
||||||
path('media/2/', include('files.media_v2')),
|
|
||||||
path('api/2/', include('tickets.api_v2')),
|
|
||||||
path('api/2/', include('mail.api_v2')),
|
|
||||||
path('api/2/', include('notify_sessions.api_v2')),
|
|
||||||
path('api/2/', include('authentication.api_v2')),
|
|
||||||
path('api/', get_info),
|
|
||||||
path('', include('django_prometheus.urls')),
|
|
||||||
]
|
|
|
@ -1,15 +0,0 @@
|
||||||
from rest_framework.decorators import api_view, permission_classes, authentication_classes
|
|
||||||
from rest_framework.response import Response
|
|
||||||
|
|
||||||
from .settings import SYSTEM3_VERSION
|
|
||||||
|
|
||||||
|
|
||||||
@api_view(['GET'])
|
|
||||||
@permission_classes([])
|
|
||||||
@authentication_classes([])
|
|
||||||
def get_info(request):
|
|
||||||
return Response({
|
|
||||||
"framework_version": SYSTEM3_VERSION,
|
|
||||||
"api_min_version": "1.0",
|
|
||||||
"api_max_version": "1.0",
|
|
||||||
})
|
|
|
@ -1,10 +0,0 @@
|
||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
from files.models import File
|
|
||||||
|
|
||||||
|
|
||||||
class FileAdmin(admin.ModelAdmin):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(File, FileAdmin)
|
|
|
@ -1,27 +0,0 @@
|
||||||
from rest_framework import serializers, viewsets, routers
|
|
||||||
|
|
||||||
from files.models import File
|
|
||||||
|
|
||||||
|
|
||||||
class FileSerializer(serializers.ModelSerializer):
|
|
||||||
data = serializers.CharField(max_length=1000000, write_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = File
|
|
||||||
fields = ['hash', 'data']
|
|
||||||
read_only_fields = ['hash']
|
|
||||||
|
|
||||||
|
|
||||||
class FileViewSet(viewsets.ModelViewSet):
|
|
||||||
serializer_class = FileSerializer
|
|
||||||
queryset = File.objects.all()
|
|
||||||
lookup_field = 'hash'
|
|
||||||
permission_classes = []
|
|
||||||
authentication_classes = []
|
|
||||||
|
|
||||||
|
|
||||||
router = routers.SimpleRouter(trailing_slash=False)
|
|
||||||
router.register(r'files', FileViewSet, basename='files')
|
|
||||||
router.register(r'file', FileViewSet, basename='files')
|
|
||||||
|
|
||||||
urlpatterns = router.urls
|
|
|
@ -1,24 +0,0 @@
|
||||||
from rest_framework import serializers, viewsets, routers
|
|
||||||
|
|
||||||
from files.models import File
|
|
||||||
|
|
||||||
|
|
||||||
class FileSerializer(serializers.ModelSerializer):
|
|
||||||
data = serializers.CharField(max_length=1000000, write_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = File
|
|
||||||
fields = ['hash', 'data']
|
|
||||||
read_only_fields = ['hash']
|
|
||||||
|
|
||||||
|
|
||||||
class FileViewSet(viewsets.ModelViewSet):
|
|
||||||
serializer_class = FileSerializer
|
|
||||||
queryset = File.objects.all()
|
|
||||||
lookup_field = 'hash'
|
|
||||||
|
|
||||||
|
|
||||||
router = routers.SimpleRouter()
|
|
||||||
router.register(r'files', FileViewSet, basename='files')
|
|
||||||
|
|
||||||
urlpatterns = router.urls
|
|
|
@ -1,65 +0,0 @@
|
||||||
import os
|
|
||||||
from django.http import HttpResponse
|
|
||||||
from django.urls import path
|
|
||||||
from drf_yasg.utils import swagger_auto_schema
|
|
||||||
from rest_framework import status
|
|
||||||
from rest_framework.decorators import api_view, permission_classes, authentication_classes
|
|
||||||
from rest_framework.response import Response
|
|
||||||
|
|
||||||
from core.settings import MEDIA_ROOT
|
|
||||||
from files.models import File
|
|
||||||
|
|
||||||
|
|
||||||
@swagger_auto_schema(method='GET', auto_schema=None)
|
|
||||||
@api_view(['GET'])
|
|
||||||
@permission_classes([])
|
|
||||||
@authentication_classes([])
|
|
||||||
def media_urls(request, hash):
|
|
||||||
try:
|
|
||||||
file = File.objects.get(hash=hash)
|
|
||||||
hash_path = file.file
|
|
||||||
return HttpResponse(status=status.HTTP_200_OK,
|
|
||||||
content_type=file.mime_type,
|
|
||||||
headers={
|
|
||||||
'X-Accel-Redirect': f'/redirect_media/{hash_path}',
|
|
||||||
'Access-Control-Allow-Origin': '*',
|
|
||||||
}) # TODO Expires and Cache-Control
|
|
||||||
|
|
||||||
except File.DoesNotExist:
|
|
||||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
|
||||||
|
|
||||||
|
|
||||||
@swagger_auto_schema(method='GET', auto_schema=None)
|
|
||||||
@api_view(['GET'])
|
|
||||||
@permission_classes([])
|
|
||||||
@authentication_classes([])
|
|
||||||
def thumbnail_urls(request, hash):
|
|
||||||
size = 200
|
|
||||||
try:
|
|
||||||
file = File.objects.get(hash=hash)
|
|
||||||
hash_path = file.file
|
|
||||||
if not os.path.exists(MEDIA_ROOT + f'/thumbnails/{size}/{hash_path}'):
|
|
||||||
from PIL import Image
|
|
||||||
image = Image.open(file.file)
|
|
||||||
image.thumbnail((size, size))
|
|
||||||
rgb_image = image.convert('RGB')
|
|
||||||
thumb_dir = os.path.dirname(MEDIA_ROOT + f'/thumbnails/{size}/{hash_path}')
|
|
||||||
if not os.path.exists(thumb_dir):
|
|
||||||
os.makedirs(thumb_dir)
|
|
||||||
rgb_image.save(MEDIA_ROOT + f'/thumbnails/{size}/{hash_path}', 'jpeg', quality=90)
|
|
||||||
|
|
||||||
return HttpResponse(status=status.HTTP_200_OK,
|
|
||||||
content_type="image/jpeg",
|
|
||||||
headers={
|
|
||||||
'X-Accel-Redirect': f'/redirect_thumbnail/{size}/{hash_path}',
|
|
||||||
'Access-Control-Allow-Origin': '*',
|
|
||||||
}) # TODO Expires and Cache-Control
|
|
||||||
|
|
||||||
except File.DoesNotExist:
|
|
||||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
path('thumbs/<path:hash>', thumbnail_urls),
|
|
||||||
path('images/<path:hash>', media_urls),
|
|
||||||
]
|
|
|
@ -1,92 +0,0 @@
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
import os
|
|
||||||
from django.http import HttpResponse
|
|
||||||
from django.urls import path
|
|
||||||
from drf_yasg.utils import swagger_auto_schema
|
|
||||||
from rest_framework import status
|
|
||||||
from rest_framework.decorators import api_view, permission_classes
|
|
||||||
from rest_framework.permissions import IsAuthenticated
|
|
||||||
from rest_framework.response import Response
|
|
||||||
|
|
||||||
from core.settings import MEDIA_ROOT
|
|
||||||
from files.models import File
|
|
||||||
from mail.models import EmailAttachment
|
|
||||||
|
|
||||||
|
|
||||||
@swagger_auto_schema(method='GET', auto_schema=None)
|
|
||||||
@api_view(['GET'])
|
|
||||||
@permission_classes([IsAuthenticated])
|
|
||||||
def media_urls(request, hash):
|
|
||||||
try:
|
|
||||||
if request.META.get('HTTP_IF_NONE_MATCH') and request.META.get('HTTP_IF_NONE_MATCH') == hash:
|
|
||||||
return HttpResponse(status=status.HTTP_304_NOT_MODIFIED)
|
|
||||||
|
|
||||||
file = File.objects.filter(hash=hash).first()
|
|
||||||
attachment = EmailAttachment.objects.filter(hash=hash).first()
|
|
||||||
file = file if file else attachment
|
|
||||||
if not file:
|
|
||||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
|
||||||
hash_path = file.file
|
|
||||||
return HttpResponse(status=status.HTTP_200_OK,
|
|
||||||
content_type=file.mime_type,
|
|
||||||
headers={
|
|
||||||
'X-Accel-Redirect': f'/redirect_media/{hash_path}',
|
|
||||||
'Access-Control-Allow-Origin': '*',
|
|
||||||
'Cache-Control': 'max-age=31536000, private, immutable',
|
|
||||||
'Expires': datetime.utcnow() + timedelta(days=365),
|
|
||||||
'Age': 0,
|
|
||||||
'ETag': file.hash,
|
|
||||||
})
|
|
||||||
except File.DoesNotExist:
|
|
||||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
|
||||||
except EmailAttachment.DoesNotExist:
|
|
||||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
|
||||||
|
|
||||||
|
|
||||||
@swagger_auto_schema(method='GET', auto_schema=None)
|
|
||||||
@api_view(['GET'])
|
|
||||||
@permission_classes([IsAuthenticated])
|
|
||||||
def thumbnail_urls(request, size, hash):
|
|
||||||
if size not in [32, 64, 256]:
|
|
||||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
|
||||||
if request.META.get('HTTP_IF_NONE_MATCH') and request.META.get('HTTP_IF_NONE_MATCH') == hash + "_" + str(size):
|
|
||||||
return HttpResponse(status=status.HTTP_304_NOT_MODIFIED)
|
|
||||||
try:
|
|
||||||
file = File.objects.filter(hash=hash).first()
|
|
||||||
attachment = EmailAttachment.objects.filter(hash=hash).first()
|
|
||||||
file = file if file else attachment
|
|
||||||
if not file:
|
|
||||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
|
||||||
hash_path = file.file
|
|
||||||
if not os.path.exists(MEDIA_ROOT + f'/thumbnails/{size}/{hash_path}'):
|
|
||||||
from PIL import Image
|
|
||||||
image = Image.open(file.file)
|
|
||||||
image.thumbnail((size, size))
|
|
||||||
rgb_image = image.convert('RGB')
|
|
||||||
thumb_dir = os.path.dirname(MEDIA_ROOT + f'/thumbnails/{size}/{hash_path}')
|
|
||||||
if not os.path.exists(thumb_dir):
|
|
||||||
os.makedirs(thumb_dir)
|
|
||||||
rgb_image.save(MEDIA_ROOT + f'/thumbnails/{size}/{hash_path}', 'jpeg', quality=90)
|
|
||||||
|
|
||||||
return HttpResponse(status=status.HTTP_200_OK,
|
|
||||||
content_type="image/jpeg",
|
|
||||||
headers={
|
|
||||||
'X-Accel-Redirect': f'/redirect_thumbnail/{size}/{hash_path}',
|
|
||||||
'Access-Control-Allow-Origin': '*',
|
|
||||||
'Cache-Control': 'max-age=31536000, private, immutable',
|
|
||||||
'Expires': datetime.utcnow() + timedelta(days=365),
|
|
||||||
'Age': 0,
|
|
||||||
'ETag': file.hash + "_" + str(size),
|
|
||||||
})
|
|
||||||
|
|
||||||
except File.DoesNotExist:
|
|
||||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
|
||||||
except EmailAttachment.DoesNotExist:
|
|
||||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
path('<int:size>/<path:hash>/', thumbnail_urls),
|
|
||||||
path('<path:hash>/', media_urls),
|
|
||||||
]
|
|
|
@ -1,30 +0,0 @@
|
||||||
# Generated by Django 4.2.7 on 2023-12-09 02:13
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
import files.models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('inventory', '0001_initial'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='File',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('created_at', models.DateTimeField(blank=True, null=True)),
|
|
||||||
('updated_at', models.DateTimeField(blank=True, null=True)),
|
|
||||||
('deleted_at', models.DateTimeField(blank=True, null=True)),
|
|
||||||
('file', models.ImageField(upload_to=files.models.hash_upload)),
|
|
||||||
('mime_type', models.CharField(max_length=255)),
|
|
||||||
('hash', models.CharField(max_length=64, unique=True)),
|
|
||||||
('item', models.ForeignKey(blank=True, db_column='iid', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='files', to='inventory.item')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,19 +0,0 @@
|
||||||
# Generated by Django 4.2.7 on 2024-01-10 19:04
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import files.models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('files', '0001_initial'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='file',
|
|
||||||
name='file',
|
|
||||||
field=models.FileField(upload_to=files.models.hash_upload),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,95 +0,0 @@
|
||||||
from django.core.files.base import ContentFile
|
|
||||||
from django.db import models, IntegrityError
|
|
||||||
from django_softdelete.models import SoftDeleteModel
|
|
||||||
|
|
||||||
from inventory.models import Item
|
|
||||||
|
|
||||||
|
|
||||||
def hash_upload(instance, filename):
|
|
||||||
return f"{instance.hash[:2]}/{instance.hash[2:4]}/{instance.hash[4:6]}/{instance.hash[6:]}"
|
|
||||||
|
|
||||||
|
|
||||||
class FileManager(models.Manager):
|
|
||||||
def get_or_create(self, **kwargs):
|
|
||||||
if 'data' in kwargs and type(kwargs['data']) == str:
|
|
||||||
import base64
|
|
||||||
from hashlib import sha256
|
|
||||||
raw = kwargs['data']
|
|
||||||
if not raw.startswith('data:'):
|
|
||||||
raise ValueError('data must be a base64 encoded string or file and hash must be provided')
|
|
||||||
raw = raw.split(';base64,')
|
|
||||||
if len(raw) != 2:
|
|
||||||
raise ValueError('data must be a base64 encoded string or file and hash must be provided')
|
|
||||||
mime_type = raw[0].split(':')[1]
|
|
||||||
content = base64.b64decode(raw[1], validate=True)
|
|
||||||
kwargs.pop('data')
|
|
||||||
content_hash = sha256(content).hexdigest()
|
|
||||||
kwargs['file'] = ContentFile(content, content_hash)
|
|
||||||
kwargs['hash'] = content_hash
|
|
||||||
kwargs['mime_type'] = mime_type
|
|
||||||
elif 'file' in kwargs and 'hash' in kwargs and type(kwargs['file']) == ContentFile and 'mime_type' in kwargs:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
raise ValueError('data must be a base64 encoded string or file and hash must be provided')
|
|
||||||
try:
|
|
||||||
return self.get(hash=kwargs['hash']), False
|
|
||||||
except self.model.DoesNotExist:
|
|
||||||
obj = super().create(**kwargs)
|
|
||||||
obj.file.save(content=kwargs['file'], name=kwargs['hash'])
|
|
||||||
return obj, True
|
|
||||||
|
|
||||||
def create(self, **kwargs):
|
|
||||||
if 'data' in kwargs and type(kwargs['data']) == str:
|
|
||||||
import base64
|
|
||||||
from hashlib import sha256
|
|
||||||
raw = kwargs['data']
|
|
||||||
if not raw.startswith('data:'):
|
|
||||||
raise ValueError('data must be a base64 encoded string or file and hash must be provided')
|
|
||||||
raw = raw.split(';base64,')
|
|
||||||
if len(raw) != 2:
|
|
||||||
raise ValueError('data must be a base64 encoded string or file and hash must be provided')
|
|
||||||
mime_type = raw[0].split(':')[1]
|
|
||||||
content = base64.b64decode(raw[1], validate=True)
|
|
||||||
kwargs.pop('data')
|
|
||||||
content_hash = sha256(content).hexdigest()
|
|
||||||
kwargs['file'] = ContentFile(content, content_hash)
|
|
||||||
kwargs['hash'] = content_hash
|
|
||||||
kwargs['mime_type'] = mime_type
|
|
||||||
elif 'file' in kwargs and 'hash' in kwargs and type(kwargs['file']) == ContentFile and 'mime_type' in kwargs:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
raise ValueError('data must be a base64 encoded string or file and hash must be provided')
|
|
||||||
if not self.filter(hash=kwargs['hash']).exists():
|
|
||||||
obj = super().create(**kwargs)
|
|
||||||
obj.file.save(content=kwargs['file'], name=kwargs['hash'])
|
|
||||||
return obj
|
|
||||||
else:
|
|
||||||
raise IntegrityError('File with this hash already exists')
|
|
||||||
|
|
||||||
|
|
||||||
class AbstractFile(models.Model):
|
|
||||||
created_at = models.DateTimeField(blank=True, null=True)
|
|
||||||
updated_at = models.DateTimeField(blank=True, null=True)
|
|
||||||
deleted_at = models.DateTimeField(blank=True, null=True)
|
|
||||||
file = models.FileField(upload_to=hash_upload)
|
|
||||||
mime_type = models.CharField(max_length=255, null=False, blank=False)
|
|
||||||
hash = models.CharField(max_length=64, null=False, blank=False, unique=True)
|
|
||||||
|
|
||||||
objects = FileManager()
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
from django.utils import timezone
|
|
||||||
if not self.created_at:
|
|
||||||
self.created_at = timezone.now()
|
|
||||||
self.updated_at = timezone.now()
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
abstract = True
|
|
||||||
|
|
||||||
|
|
||||||
class File(AbstractFile):
|
|
||||||
item = models.ForeignKey(Item, models.CASCADE, db_column='iid', null=True, blank=True, related_name='files')
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.hash
|
|
|
@ -1,68 +0,0 @@
|
||||||
from django.test import TestCase, Client
|
|
||||||
from django.core.files.base import ContentFile
|
|
||||||
|
|
||||||
from files.models import File
|
|
||||||
from inventory.models import Event, Container, Item
|
|
||||||
|
|
||||||
client = Client()
|
|
||||||
|
|
||||||
|
|
||||||
class FileTestCase(TestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super().setUp()
|
|
||||||
self.event = Event.objects.create(slug='EVENT', name='Event')
|
|
||||||
self.box = Container.objects.create(name='BOX')
|
|
||||||
|
|
||||||
def test_create_file_raw(self):
|
|
||||||
from hashlib import sha256
|
|
||||||
content = b"foo"
|
|
||||||
chash = sha256(content).hexdigest()
|
|
||||||
item = Item.objects.create(container=self.box, event=self.event, description='1')
|
|
||||||
file = File.objects.create(file=ContentFile(b"foo"), mime_type='text/plain', hash=chash, item=item)
|
|
||||||
file.save()
|
|
||||||
self.assertEqual(1, len(File.objects.all()))
|
|
||||||
self.assertEqual(content, File.objects.all()[0].file.read())
|
|
||||||
self.assertEqual(chash, File.objects.all()[0].hash)
|
|
||||||
|
|
||||||
def test_list_files(self):
|
|
||||||
import base64
|
|
||||||
|
|
||||||
item = File.objects.create(data="data:text/plain;base64," + base64.b64encode(b"foo").decode('utf-8'))
|
|
||||||
response = client.get('/api/1/files')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(response.json()[0]['hash'], item.hash)
|
|
||||||
self.assertEqual(len(response.json()[0]['hash']), 64)
|
|
||||||
self.assertEqual(len(File.objects.all()), 1)
|
|
||||||
self.assertEqual(File.objects.all()[0].file.read(), b"foo")
|
|
||||||
|
|
||||||
def test_one_file(self):
|
|
||||||
import base64
|
|
||||||
item = File.objects.create(data="data:text/plain;base64," + base64.b64encode(b"foo").decode('utf-8'))
|
|
||||||
response = client.get(f'/api/1/file/{item.hash}')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(response.json()['hash'], item.hash)
|
|
||||||
self.assertEqual(len(response.json()['hash']), 64)
|
|
||||||
self.assertEqual(len(File.objects.all()), 1)
|
|
||||||
self.assertEqual(File.objects.all()[0].file.read(), b"foo")
|
|
||||||
|
|
||||||
def test_create_file(self):
|
|
||||||
import base64
|
|
||||||
Item.objects.create(container=self.box, event=self.event, description='1')
|
|
||||||
item = Item.objects.create(container=self.box, event=self.event, description='2')
|
|
||||||
response = client.post('/api/1/file',
|
|
||||||
{'data': "data:text/plain;base64," + base64.b64encode(b"foo").decode('utf-8')},
|
|
||||||
content_type='application/json')
|
|
||||||
self.assertEqual(response.status_code, 201)
|
|
||||||
self.assertEqual(len(response.json()['hash']), 64)
|
|
||||||
self.assertEqual(len(File.objects.all()), 1)
|
|
||||||
self.assertEqual(File.objects.all()[0].file.read(), b"foo")
|
|
||||||
|
|
||||||
def test_delete_file(self):
|
|
||||||
import base64
|
|
||||||
item = Item.objects.create(container=self.box, event=self.event, description='1')
|
|
||||||
File.objects.create(item=item, data="data:text/plain;base64," + base64.b64encode(b"foo").decode('utf-8'))
|
|
||||||
file = File.objects.create(item=item, data="data:text/plain;base64," + base64.b64encode(b"bar").decode('utf-8'))
|
|
||||||
self.assertEqual(len(File.objects.all()), 2)
|
|
||||||
response = client.delete(f'/api/1/file/{file.hash}')
|
|
||||||
self.assertEqual(response.status_code, 204)
|
|
|
@ -1,55 +0,0 @@
|
||||||
from django.test import TestCase, Client
|
|
||||||
from django.contrib.auth.models import Permission
|
|
||||||
|
|
||||||
from authentication.models import ExtendedUser
|
|
||||||
from files.models import File
|
|
||||||
from inventory.models import Event, Container, Item
|
|
||||||
from knox.models import AuthToken
|
|
||||||
|
|
||||||
|
|
||||||
class FileTestCase(TestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super().setUp()
|
|
||||||
self.user = ExtendedUser.objects.create_user('testuser', 'test', 'test')
|
|
||||||
self.user.user_permissions.add(*Permission.objects.all())
|
|
||||||
self.user.save()
|
|
||||||
self.token = AuthToken.objects.create(user=self.user)
|
|
||||||
self.client = Client(headers={'Authorization': 'Token ' + self.token[1]})
|
|
||||||
self.event = Event.objects.create(slug='EVENT', name='Event')
|
|
||||||
self.box = Container.objects.create(name='BOX')
|
|
||||||
|
|
||||||
def test_list_files(self):
|
|
||||||
import base64
|
|
||||||
item = File.objects.create(data="data:text/plain;base64," + base64.b64encode(b"foo").decode('utf-8'))
|
|
||||||
response = self.client.get('/api/2/files/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(response.json()[0]['hash'], item.hash)
|
|
||||||
self.assertEqual(len(response.json()[0]['hash']), 64)
|
|
||||||
|
|
||||||
def test_one_file(self):
|
|
||||||
import base64
|
|
||||||
item = File.objects.create(data="data:text/plain;base64," + base64.b64encode(b"foo").decode('utf-8'))
|
|
||||||
response = self.client.get(f'/api/2/files/{item.hash}/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(response.json()['hash'], item.hash)
|
|
||||||
self.assertEqual(len(response.json()['hash']), 64)
|
|
||||||
|
|
||||||
def test_create_file(self):
|
|
||||||
import base64
|
|
||||||
Item.objects.create(container=self.box, event=self.event, description='1')
|
|
||||||
item = Item.objects.create(container=self.box, event=self.event, description='2')
|
|
||||||
response = self.client.post('/api/2/files/',
|
|
||||||
{'data': "data:text/plain;base64," + base64.b64encode(b"foo").decode('utf-8')},
|
|
||||||
content_type='application/json')
|
|
||||||
self.assertEqual(response.status_code, 201)
|
|
||||||
self.assertEqual(len(response.json()['hash']), 64)
|
|
||||||
|
|
||||||
def test_delete_file(self):
|
|
||||||
import base64
|
|
||||||
item = Item.objects.create(container=self.box, event=self.event, description='1')
|
|
||||||
File.objects.create(item=item, data="data:text/plain;base64," + base64.b64encode(b"foo").decode('utf-8'))
|
|
||||||
file = File.objects.create(item=item, data="data:text/plain;base64," + base64.b64encode(b"bar").decode('utf-8'))
|
|
||||||
self.assertEqual(len(File.objects.all()), 2)
|
|
||||||
response = self.client.delete(f'/api/2/files/{file.hash}/')
|
|
||||||
self.assertEqual(response.status_code, 204)
|
|
|
@ -1,30 +0,0 @@
|
||||||
import asyncio
|
|
||||||
import logging
|
|
||||||
import signal
|
|
||||||
|
|
||||||
loop = None
|
|
||||||
|
|
||||||
|
|
||||||
def create_task(coro):
|
|
||||||
global loop
|
|
||||||
loop.create_task(coro)
|
|
||||||
|
|
||||||
|
|
||||||
async def shutdown(sig, loop):
|
|
||||||
log = logging.getLogger()
|
|
||||||
log.info(f"Received exit signal {sig.name}...")
|
|
||||||
tasks = [t for t in asyncio.all_tasks() if t is not
|
|
||||||
asyncio.current_task()]
|
|
||||||
[task.cancel() for task in tasks]
|
|
||||||
log.info(f"Cancelling {len(tasks)} outstanding tasks")
|
|
||||||
await asyncio.wait_for(loop.shutdown_asyncgens(), timeout=10)
|
|
||||||
loop.stop()
|
|
||||||
log.info("Shutdown complete.")
|
|
||||||
|
|
||||||
|
|
||||||
def init_loop():
|
|
||||||
global loop
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
loop.add_signal_handler(signal.SIGTERM, lambda: asyncio.create_task(shutdown(signal.SIGTERM, loop)))
|
|
||||||
loop.add_signal_handler(signal.SIGINT, lambda: asyncio.create_task(shutdown(signal.SIGINT, loop)))
|
|
||||||
return loop
|
|
|
@ -1,24 +0,0 @@
|
||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
from inventory.models import Item, Container, Event
|
|
||||||
|
|
||||||
|
|
||||||
class ItemAdmin(admin.ModelAdmin):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(Item, ItemAdmin)
|
|
||||||
|
|
||||||
|
|
||||||
class ContainerAdmin(admin.ModelAdmin):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(Container, ContainerAdmin)
|
|
||||||
|
|
||||||
|
|
||||||
class EventAdmin(admin.ModelAdmin):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(Event, EventAdmin)
|
|
|
@ -1,150 +0,0 @@
|
||||||
from django.utils import timezone
|
|
||||||
from django.urls import re_path
|
|
||||||
from rest_framework import routers, viewsets, serializers
|
|
||||||
from rest_framework.decorators import api_view, permission_classes, authentication_classes
|
|
||||||
from rest_framework.response import Response
|
|
||||||
|
|
||||||
from files.models import File
|
|
||||||
from inventory.models import Event, Container, Item
|
|
||||||
from inventory.serializers import EventSerializer, ContainerSerializer
|
|
||||||
|
|
||||||
|
|
||||||
class EventViewSet(viewsets.ModelViewSet):
|
|
||||||
serializer_class = EventSerializer
|
|
||||||
queryset = Event.objects.all()
|
|
||||||
permission_classes = []
|
|
||||||
authentication_classes = []
|
|
||||||
|
|
||||||
|
|
||||||
class ContainerViewSet(viewsets.ModelViewSet):
|
|
||||||
serializer_class = ContainerSerializer
|
|
||||||
queryset = Container.objects.all()
|
|
||||||
permission_classes = []
|
|
||||||
authentication_classes = []
|
|
||||||
|
|
||||||
|
|
||||||
class ItemSerializer(serializers.ModelSerializer):
|
|
||||||
dataImage = serializers.CharField(write_only=True, required=False)
|
|
||||||
cid = serializers.SerializerMethodField()
|
|
||||||
box = serializers.SerializerMethodField()
|
|
||||||
file = serializers.SerializerMethodField()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Item
|
|
||||||
fields = ['cid', 'box', 'uid', 'description', 'file', 'dataImage']
|
|
||||||
read_only_fields = ['uid']
|
|
||||||
|
|
||||||
def get_cid(self, instance):
|
|
||||||
return instance.container.cid
|
|
||||||
|
|
||||||
def get_box(self, instance):
|
|
||||||
return instance.container.name
|
|
||||||
|
|
||||||
def get_file(self, instance):
|
|
||||||
if len(instance.files.all()) > 0:
|
|
||||||
return instance.files.all().order_by('-created_at')[0].hash
|
|
||||||
return None
|
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
|
||||||
if 'cid' in data:
|
|
||||||
container = Container.objects.get(cid=data['cid'])
|
|
||||||
internal = super().to_internal_value(data)
|
|
||||||
internal['container'] = container
|
|
||||||
return internal
|
|
||||||
return super().to_internal_value(data)
|
|
||||||
|
|
||||||
def validate(self, attrs):
|
|
||||||
return super().validate(attrs)
|
|
||||||
|
|
||||||
def create(self, validated_data):
|
|
||||||
if 'dataImage' in validated_data:
|
|
||||||
file = File.objects.create(data=validated_data['dataImage'])
|
|
||||||
validated_data.pop('dataImage')
|
|
||||||
item = Item.objects.create(**validated_data)
|
|
||||||
item.files.set([file])
|
|
||||||
return item
|
|
||||||
return Item.objects.create(**validated_data)
|
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
|
||||||
if 'returned' in validated_data:
|
|
||||||
if validated_data['returned']:
|
|
||||||
validated_data['returned_at'] = timezone.now()
|
|
||||||
validated_data.pop('returned')
|
|
||||||
if 'dataImage' in validated_data:
|
|
||||||
file = File.objects.create(data=validated_data['dataImage'])
|
|
||||||
validated_data.pop('dataImage')
|
|
||||||
instance.files.add(file)
|
|
||||||
return super().update(instance, validated_data)
|
|
||||||
|
|
||||||
|
|
||||||
@api_view(['GET'])
|
|
||||||
@permission_classes([])
|
|
||||||
@authentication_classes([])
|
|
||||||
def search_items(request, event_slug, query):
|
|
||||||
try:
|
|
||||||
event = Event.objects.get(slug=event_slug)
|
|
||||||
query_tokens = query.split(' ')
|
|
||||||
q = Item.objects.filter(event=event)
|
|
||||||
for token in query_tokens:
|
|
||||||
if token:
|
|
||||||
q = q.filter(description__icontains=token)
|
|
||||||
return Response(ItemSerializer(q, many=True).data)
|
|
||||||
except Event.DoesNotExist:
|
|
||||||
return Response(status=404)
|
|
||||||
|
|
||||||
|
|
||||||
@api_view(['GET', 'POST'])
|
|
||||||
@permission_classes([])
|
|
||||||
@authentication_classes([])
|
|
||||||
def item(request, event_slug):
|
|
||||||
try:
|
|
||||||
event = Event.objects.get(slug=event_slug)
|
|
||||||
if request.method == 'GET':
|
|
||||||
return Response(ItemSerializer(Item.objects.filter(event=event), many=True).data)
|
|
||||||
elif request.method == 'POST':
|
|
||||||
validated_data = ItemSerializer(data=request.data)
|
|
||||||
if validated_data.is_valid():
|
|
||||||
validated_data.save(event=event)
|
|
||||||
return Response(validated_data.data, status=201)
|
|
||||||
except Event.DoesNotExist:
|
|
||||||
return Response(status=404)
|
|
||||||
|
|
||||||
|
|
||||||
@api_view(['GET', 'PUT', 'DELETE'])
|
|
||||||
@permission_classes([])
|
|
||||||
@authentication_classes([])
|
|
||||||
def item_by_id(request, event_slug, id):
|
|
||||||
try:
|
|
||||||
event = Event.objects.get(slug=event_slug)
|
|
||||||
item = Item.objects.get(event=event, uid=id)
|
|
||||||
if request.method == 'GET':
|
|
||||||
return Response(ItemSerializer(item).data)
|
|
||||||
elif request.method == 'PUT':
|
|
||||||
validated_data = ItemSerializer(item, data=request.data)
|
|
||||||
if validated_data.is_valid():
|
|
||||||
validated_data.save()
|
|
||||||
return Response(validated_data.data)
|
|
||||||
elif request.method == 'DELETE':
|
|
||||||
item.delete()
|
|
||||||
return Response(status=204)
|
|
||||||
except Item.DoesNotExist:
|
|
||||||
return Response(status=404)
|
|
||||||
except Event.DoesNotExist:
|
|
||||||
return Response(status=404)
|
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
re_path('events/?$', EventViewSet.as_view({'get': 'list', 'post': 'create'})),
|
|
||||||
re_path('events/(?P<pk>[0-9]+)/?$',
|
|
||||||
EventViewSet.as_view({'get': 'retrieve', 'put': 'update', 'delete': 'destroy'})),
|
|
||||||
re_path('boxes/?$', ContainerViewSet.as_view({'get': 'list', 'post': 'create'})),
|
|
||||||
re_path('boxes/(?P<pk>[0-9]+)/?$',
|
|
||||||
ContainerViewSet.as_view({'get': 'retrieve', 'put': 'update', 'delete': 'destroy'})),
|
|
||||||
re_path('box/?$', ContainerViewSet.as_view({'get': 'list', 'post': 'create'})),
|
|
||||||
re_path('box/(?P<pk>[0-9]+)/?$',
|
|
||||||
ContainerViewSet.as_view({'get': 'retrieve', 'put': 'update', 'delete': 'destroy'})),
|
|
||||||
re_path('(?P<event_slug>[a-zA-Z0-9]+)/items/?$', item),
|
|
||||||
re_path('(?P<event_slug>[a-zA-Z0-9]+)/items/(?P<query>[^/]+)/?$', search_items),
|
|
||||||
re_path('(?P<event_slug>[a-zA-Z0-9]+)/item/?$', item),
|
|
||||||
re_path('(?P<event_slug>[a-zA-Z0-9]+)/item/(?P<id>[0-9]+)/?$', item_by_id),
|
|
||||||
]
|
|
|
@ -1,106 +0,0 @@
|
||||||
from django.urls import path
|
|
||||||
from django.contrib.auth.decorators import permission_required
|
|
||||||
from rest_framework import routers, viewsets
|
|
||||||
from rest_framework.decorators import api_view, permission_classes
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework.permissions import IsAuthenticated
|
|
||||||
|
|
||||||
from inventory.models import Event, Container, Item
|
|
||||||
from inventory.serializers import EventSerializer, ContainerSerializer, ItemSerializer
|
|
||||||
|
|
||||||
|
|
||||||
class EventViewSet(viewsets.ModelViewSet):
|
|
||||||
serializer_class = EventSerializer
|
|
||||||
queryset = Event.objects.all()
|
|
||||||
permission_classes = []
|
|
||||||
|
|
||||||
|
|
||||||
class ContainerViewSet(viewsets.ModelViewSet):
|
|
||||||
serializer_class = ContainerSerializer
|
|
||||||
queryset = Container.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
@api_view(['GET'])
|
|
||||||
@permission_classes([IsAuthenticated])
|
|
||||||
@permission_required('view_item', raise_exception=True)
|
|
||||||
def search_items(request, event_slug, query):
|
|
||||||
try:
|
|
||||||
event = Event.objects.get(slug=event_slug)
|
|
||||||
query_tokens = query.split(' ')
|
|
||||||
q = Item.objects.filter(event=event)
|
|
||||||
for token in query_tokens:
|
|
||||||
if token:
|
|
||||||
q = q.filter(description__icontains=token)
|
|
||||||
return Response(ItemSerializer(q, many=True).data)
|
|
||||||
except Event.DoesNotExist:
|
|
||||||
return Response(status=404)
|
|
||||||
|
|
||||||
|
|
||||||
@api_view(['GET', 'POST'])
|
|
||||||
@permission_classes([IsAuthenticated])
|
|
||||||
def item(request, event_slug):
|
|
||||||
try:
|
|
||||||
event = Event.objects.get(slug=event_slug)
|
|
||||||
if request.method == 'GET':
|
|
||||||
if not request.user.has_event_perm(event, 'view_item'):
|
|
||||||
return Response(status=403)
|
|
||||||
return Response(ItemSerializer(Item.objects.filter(event=event), many=True).data)
|
|
||||||
elif request.method == 'POST':
|
|
||||||
if not request.user.has_event_perm(event, 'add_item'):
|
|
||||||
return Response(status=403)
|
|
||||||
validated_data = ItemSerializer(data=request.data)
|
|
||||||
if validated_data.is_valid():
|
|
||||||
validated_data.save(event=event)
|
|
||||||
return Response(validated_data.data, status=201)
|
|
||||||
except Event.DoesNotExist:
|
|
||||||
return Response(status=404)
|
|
||||||
|
|
||||||
|
|
||||||
@api_view(['GET', 'PUT', 'DELETE', 'PATCH'])
|
|
||||||
@permission_classes([IsAuthenticated])
|
|
||||||
def item_by_id(request, event_slug, id):
|
|
||||||
try:
|
|
||||||
event = Event.objects.get(slug=event_slug)
|
|
||||||
item = Item.objects.get(event=event, uid=id)
|
|
||||||
if request.method == 'GET':
|
|
||||||
if not request.user.has_event_perm(event, 'view_item'):
|
|
||||||
return Response(status=403)
|
|
||||||
return Response(ItemSerializer(item).data)
|
|
||||||
elif request.method == 'PUT':
|
|
||||||
if not request.user.has_event_perm(event, 'change_item'):
|
|
||||||
return Response(status=403)
|
|
||||||
validated_data = ItemSerializer(item, data=request.data)
|
|
||||||
if validated_data.is_valid():
|
|
||||||
validated_data.save()
|
|
||||||
return Response(validated_data.data)
|
|
||||||
return Response(validated_data.errors, status=400)
|
|
||||||
elif request.method == 'PATCH':
|
|
||||||
if not request.user.has_event_perm(event, 'change_item'):
|
|
||||||
return Response(status=403)
|
|
||||||
validated_data = ItemSerializer(item, data=request.data, partial=True)
|
|
||||||
if validated_data.is_valid():
|
|
||||||
validated_data.save()
|
|
||||||
return Response(validated_data.data)
|
|
||||||
return Response(validated_data.errors, status=400)
|
|
||||||
elif request.method == 'DELETE':
|
|
||||||
if not request.user.has_event_perm(event, 'delete_item'):
|
|
||||||
return Response(status=403)
|
|
||||||
item.delete()
|
|
||||||
return Response(status=204)
|
|
||||||
except Item.DoesNotExist:
|
|
||||||
return Response(status=404)
|
|
||||||
except Event.DoesNotExist:
|
|
||||||
return Response(status=404)
|
|
||||||
|
|
||||||
|
|
||||||
router = routers.SimpleRouter()
|
|
||||||
router.register(r'events', EventViewSet, basename='events')
|
|
||||||
router.register(r'boxes', ContainerViewSet, basename='boxes')
|
|
||||||
router.register(r'box', ContainerViewSet, basename='boxes')
|
|
||||||
|
|
||||||
urlpatterns = router.urls + [
|
|
||||||
path('<event_slug>/items/', item),
|
|
||||||
path('<event_slug>/items/<query>/', search_items),
|
|
||||||
path('<event_slug>/item/', item),
|
|
||||||
path('<event_slug>/item/<id>/', item_by_id),
|
|
||||||
]
|
|
|
@ -1,54 +0,0 @@
|
||||||
# Generated by Django 4.2.7 on 2023-11-18 11:28
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Container',
|
|
||||||
fields=[
|
|
||||||
('cid', models.AutoField(primary_key=True, serialize=False)),
|
|
||||||
('name', models.CharField(max_length=255)),
|
|
||||||
('created_at', models.DateTimeField(blank=True, null=True)),
|
|
||||||
('updated_at', models.DateTimeField(blank=True, null=True)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Event',
|
|
||||||
fields=[
|
|
||||||
('eid', models.AutoField(primary_key=True, serialize=False)),
|
|
||||||
('name', models.CharField(max_length=255)),
|
|
||||||
('slug', models.CharField(max_length=255, unique=True)),
|
|
||||||
('start', models.DateTimeField(blank=True, null=True)),
|
|
||||||
('end', models.DateTimeField(blank=True, null=True)),
|
|
||||||
('pre_start', models.DateTimeField(blank=True, null=True)),
|
|
||||||
('post_end', models.DateTimeField(blank=True, null=True)),
|
|
||||||
('created_at', models.DateTimeField(blank=True, null=True)),
|
|
||||||
('updated_at', models.DateTimeField(blank=True, null=True)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Item',
|
|
||||||
fields=[
|
|
||||||
('iid', models.AutoField(primary_key=True, serialize=False)),
|
|
||||||
('uid', models.IntegerField()),
|
|
||||||
('description', models.TextField()),
|
|
||||||
('returned_at', models.DateTimeField(blank=True, null=True)),
|
|
||||||
('created_at', models.DateTimeField(blank=True, null=True)),
|
|
||||||
('updated_at', models.DateTimeField(blank=True, null=True)),
|
|
||||||
('container', models.ForeignKey(db_column='cid', on_delete=django.db.models.deletion.CASCADE, to='inventory.container')),
|
|
||||||
('event', models.ForeignKey(db_column='eid', on_delete=django.db.models.deletion.CASCADE, to='inventory.event')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'unique_together': {('uid', 'event')},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,33 +0,0 @@
|
||||||
# Generated by Django 4.2.7 on 2023-11-20 11:23
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('inventory', '0001_initial'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='container',
|
|
||||||
name='deleted_at',
|
|
||||||
field=models.DateTimeField(blank=True, null=True),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='container',
|
|
||||||
name='is_deleted',
|
|
||||||
field=models.BooleanField(default=False),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='item',
|
|
||||||
name='deleted_at',
|
|
||||||
field=models.DateTimeField(blank=True, null=True),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='item',
|
|
||||||
name='is_deleted',
|
|
||||||
field=models.BooleanField(default=False),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,17 +0,0 @@
|
||||||
# Generated by Django 4.2.7 on 2024-01-07 18:46
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('inventory', '0002_container_deleted_at_container_is_deleted_and_more'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterModelOptions(
|
|
||||||
name='item',
|
|
||||||
options={'permissions': [('match_item', 'Can match item')]},
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,23 +0,0 @@
|
||||||
# Generated by Django 4.2.7 on 2024-01-22 16:02
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('inventory', '0003_alter_item_options'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='event',
|
|
||||||
name='created_at',
|
|
||||||
field=models.DateTimeField(auto_now_add=True, null=True),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='item',
|
|
||||||
name='created_at',
|
|
||||||
field=models.DateTimeField(auto_now_add=True, null=True),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,64 +0,0 @@
|
||||||
from django.core.files.base import ContentFile
|
|
||||||
from django.db import models, IntegrityError
|
|
||||||
from django_softdelete.models import SoftDeleteModel, SoftDeleteManager
|
|
||||||
|
|
||||||
|
|
||||||
class ItemManager(SoftDeleteManager):
|
|
||||||
|
|
||||||
def create(self, **kwargs):
|
|
||||||
if 'uid' in kwargs:
|
|
||||||
raise ValueError('uid must not be set manually')
|
|
||||||
uid = Item.all_objects.filter(event=kwargs['event']).count() + 1
|
|
||||||
kwargs['uid'] = uid
|
|
||||||
return super().create(**kwargs)
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
return super().get_queryset().filter(returned_at__isnull=True)
|
|
||||||
|
|
||||||
|
|
||||||
class Item(SoftDeleteModel):
|
|
||||||
iid = models.AutoField(primary_key=True)
|
|
||||||
uid = models.IntegerField()
|
|
||||||
description = models.TextField()
|
|
||||||
event = models.ForeignKey('Event', models.CASCADE, db_column='eid')
|
|
||||||
container = models.ForeignKey('Container', models.CASCADE, db_column='cid')
|
|
||||||
returned_at = models.DateTimeField(blank=True, null=True)
|
|
||||||
created_at = models.DateTimeField(null=True, auto_now_add=True)
|
|
||||||
updated_at = models.DateTimeField(blank=True, null=True)
|
|
||||||
|
|
||||||
objects = ItemManager()
|
|
||||||
all_objects = models.Manager()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
unique_together = (('uid', 'event'),)
|
|
||||||
permissions = [
|
|
||||||
('match_item', 'Can match item')
|
|
||||||
]
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return '[' + str(self.uid) + ']' + self.description
|
|
||||||
|
|
||||||
|
|
||||||
class Container(SoftDeleteModel):
|
|
||||||
cid = models.AutoField(primary_key=True)
|
|
||||||
name = models.CharField(max_length=255)
|
|
||||||
created_at = models.DateTimeField(blank=True, null=True)
|
|
||||||
updated_at = models.DateTimeField(blank=True, null=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return '[' + str(self.cid) + ']' + self.name
|
|
||||||
|
|
||||||
|
|
||||||
class Event(models.Model):
|
|
||||||
eid = models.AutoField(primary_key=True)
|
|
||||||
name = models.CharField(max_length=255)
|
|
||||||
slug = models.CharField(max_length=255, unique=True)
|
|
||||||
start = models.DateTimeField(blank=True, null=True)
|
|
||||||
end = models.DateTimeField(blank=True, null=True)
|
|
||||||
pre_start = models.DateTimeField(blank=True, null=True)
|
|
||||||
post_end = models.DateTimeField(blank=True, null=True)
|
|
||||||
created_at = models.DateTimeField(null=True, auto_now_add=True)
|
|
||||||
updated_at = models.DateTimeField(blank=True, null=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return '[' + str(self.slug) + ']' + self.name
|
|
|
@ -1,97 +0,0 @@
|
||||||
from django.utils import timezone
|
|
||||||
from rest_framework import serializers
|
|
||||||
|
|
||||||
from files.models import File
|
|
||||||
from inventory.models import Event, Container, Item
|
|
||||||
from mail.models import EventAddress
|
|
||||||
|
|
||||||
|
|
||||||
class EventAdressSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = EventAddress
|
|
||||||
fields = ['address']
|
|
||||||
|
|
||||||
|
|
||||||
class EventSerializer(serializers.ModelSerializer):
|
|
||||||
addresses = EventAdressSerializer(many=True, required=False)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Event
|
|
||||||
fields = ['eid', 'slug', 'name', 'start', 'end', 'pre_start', 'post_end', 'addresses']
|
|
||||||
read_only_fields = ['eid']
|
|
||||||
|
|
||||||
|
|
||||||
class ContainerSerializer(serializers.ModelSerializer):
|
|
||||||
itemCount = serializers.SerializerMethodField()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Container
|
|
||||||
fields = ['cid', 'name', 'itemCount']
|
|
||||||
read_only_fields = ['cid', 'itemCount']
|
|
||||||
|
|
||||||
def get_itemCount(self, instance):
|
|
||||||
return Item.objects.filter(container=instance.cid).count()
|
|
||||||
|
|
||||||
|
|
||||||
class ItemSerializer(serializers.ModelSerializer):
|
|
||||||
dataImage = serializers.CharField(write_only=True, required=False)
|
|
||||||
cid = serializers.SerializerMethodField()
|
|
||||||
box = serializers.SerializerMethodField()
|
|
||||||
file = serializers.SerializerMethodField()
|
|
||||||
returned = serializers.SerializerMethodField(required=False)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Item
|
|
||||||
fields = ['cid', 'box', 'uid', 'description', 'file', 'dataImage', 'returned']
|
|
||||||
read_only_fields = ['uid']
|
|
||||||
|
|
||||||
def get_cid(self, instance):
|
|
||||||
return instance.container.cid
|
|
||||||
|
|
||||||
def get_box(self, instance):
|
|
||||||
return instance.container.name
|
|
||||||
|
|
||||||
def get_file(self, instance):
|
|
||||||
if len(instance.files.all()) > 0:
|
|
||||||
return instance.files.all().order_by('-created_at')[0].hash
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_returned(self, instance):
|
|
||||||
return instance.returned_at is not None
|
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
|
||||||
container = None
|
|
||||||
returned = False
|
|
||||||
if 'cid' in data:
|
|
||||||
container = Container.objects.get(cid=data['cid'])
|
|
||||||
if 'returned' in data:
|
|
||||||
returned = data['returned']
|
|
||||||
internal = super().to_internal_value(data)
|
|
||||||
if container:
|
|
||||||
internal['container'] = container
|
|
||||||
if returned:
|
|
||||||
internal['returned_at'] = timezone.now()
|
|
||||||
return internal
|
|
||||||
|
|
||||||
def validate(self, attrs):
|
|
||||||
return super().validate(attrs)
|
|
||||||
|
|
||||||
def create(self, validated_data):
|
|
||||||
if 'dataImage' in validated_data:
|
|
||||||
file = File.objects.create(data=validated_data['dataImage'])
|
|
||||||
validated_data.pop('dataImage')
|
|
||||||
item = Item.objects.create(**validated_data)
|
|
||||||
item.files.set([file])
|
|
||||||
return item
|
|
||||||
return Item.objects.create(**validated_data)
|
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
|
||||||
if 'returned' in validated_data:
|
|
||||||
if validated_data['returned']:
|
|
||||||
validated_data['returned_at'] = timezone.now()
|
|
||||||
validated_data.pop('returned')
|
|
||||||
if 'dataImage' in validated_data:
|
|
||||||
file = File.objects.create(data=validated_data['dataImage'])
|
|
||||||
validated_data.pop('dataImage')
|
|
||||||
instance.files.add(file)
|
|
||||||
return super().update(instance, validated_data)
|
|
|
@ -1,34 +0,0 @@
|
||||||
from django.test import TestCase, Client
|
|
||||||
|
|
||||||
client = Client()
|
|
||||||
|
|
||||||
|
|
||||||
class ApiTest(TestCase):
|
|
||||||
|
|
||||||
def test_root(self):
|
|
||||||
from core.settings import SYSTEM3_VERSION
|
|
||||||
response = client.get('/api/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(response.json()["framework_version"], SYSTEM3_VERSION)
|
|
||||||
|
|
||||||
def test_events(self):
|
|
||||||
response = client.get('/api/1/events')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(response.json(), [])
|
|
||||||
|
|
||||||
def test_containers(self):
|
|
||||||
response = client.get('/api/1/boxes')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(response.json(), [])
|
|
||||||
|
|
||||||
def test_files(self):
|
|
||||||
response = client.get('/api/1/files')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(response.json(), [])
|
|
||||||
|
|
||||||
def test_items(self):
|
|
||||||
from inventory.models import Event
|
|
||||||
Event.objects.create(slug='TEST1', name='Event')
|
|
||||||
response = client.get('/api/1/TEST1/items')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(response.json(), [])
|
|
|
@ -1,59 +0,0 @@
|
||||||
from django.test import TestCase, Client
|
|
||||||
from inventory.models import Container
|
|
||||||
|
|
||||||
client = Client()
|
|
||||||
|
|
||||||
|
|
||||||
class ContainerTestCase(TestCase):
|
|
||||||
|
|
||||||
def test_empty(self):
|
|
||||||
response = client.get('/api/1/boxes')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(response.json(), [])
|
|
||||||
|
|
||||||
def test_members(self):
|
|
||||||
Container.objects.create(name='BOX')
|
|
||||||
response = client.get('/api/1/boxes')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(len(response.json()), 1)
|
|
||||||
self.assertEqual(response.json()[0]['cid'], 1)
|
|
||||||
self.assertEqual(response.json()[0]['name'], 'BOX')
|
|
||||||
self.assertEqual(response.json()[0]['itemCount'], 0)
|
|
||||||
|
|
||||||
def test_multi_members(self):
|
|
||||||
Container.objects.create(name='BOX 1')
|
|
||||||
Container.objects.create(name='BOX 2')
|
|
||||||
Container.objects.create(name='BOX 3')
|
|
||||||
response = client.get('/api/1/boxes')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(len(response.json()), 3)
|
|
||||||
|
|
||||||
def test_create_container(self):
|
|
||||||
response = client.post('/api/1/box', {'name': 'BOX'})
|
|
||||||
self.assertEqual(response.status_code, 201)
|
|
||||||
self.assertEqual(response.json()['cid'], 1)
|
|
||||||
self.assertEqual(response.json()['name'], 'BOX')
|
|
||||||
self.assertEqual(response.json()['itemCount'], 0)
|
|
||||||
self.assertEqual(len(Container.objects.all()), 1)
|
|
||||||
self.assertEqual(Container.objects.all()[0].cid, 1)
|
|
||||||
self.assertEqual(Container.objects.all()[0].name, 'BOX')
|
|
||||||
|
|
||||||
def test_update_container(self):
|
|
||||||
from rest_framework.test import APIClient
|
|
||||||
box = Container.objects.create(name='BOX 1')
|
|
||||||
response = APIClient().put(f'/api/1/box/{box.cid}', {'name': 'BOX 2'})
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(response.json()['cid'], 1)
|
|
||||||
self.assertEqual(response.json()['name'], 'BOX 2')
|
|
||||||
self.assertEqual(response.json()['itemCount'], 0)
|
|
||||||
self.assertEqual(len(Container.objects.all()), 1)
|
|
||||||
self.assertEqual(Container.objects.all()[0].cid, 1)
|
|
||||||
self.assertEqual(Container.objects.all()[0].name, 'BOX 2')
|
|
||||||
|
|
||||||
def test_delete_container(self):
|
|
||||||
box = Container.objects.create(name='BOX 1')
|
|
||||||
Container.objects.create(name='BOX 2')
|
|
||||||
self.assertEqual(len(Container.objects.all()), 2)
|
|
||||||
response = client.delete(f'/api/1/box/{box.cid}')
|
|
||||||
self.assertEqual(response.status_code, 204)
|
|
||||||
self.assertEqual(len(Container.objects.all()), 1)
|
|
|
@ -1,56 +0,0 @@
|
||||||
from django.test import TestCase, Client
|
|
||||||
from inventory.models import Event
|
|
||||||
|
|
||||||
client = Client()
|
|
||||||
|
|
||||||
|
|
||||||
class EventTestCase(TestCase):
|
|
||||||
|
|
||||||
def test_empty(self):
|
|
||||||
response = client.get('/api/1/events')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(response.json(), [])
|
|
||||||
|
|
||||||
def test_members(self):
|
|
||||||
Event.objects.create(slug='EVENT', name='Event')
|
|
||||||
response = client.get('/api/1/events')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(len(response.json()), 1)
|
|
||||||
self.assertEqual(response.json()[0]['slug'], 'EVENT')
|
|
||||||
self.assertEqual(response.json()[0]['name'], 'Event')
|
|
||||||
|
|
||||||
def test_multi_members(self):
|
|
||||||
Event.objects.create(slug='EVENT1', name='Event 1')
|
|
||||||
Event.objects.create(slug='EVENT2', name='Event 2')
|
|
||||||
Event.objects.create(slug='EVENT3', name='Event 3')
|
|
||||||
response = client.get('/api/1/events')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(len(response.json()), 3)
|
|
||||||
|
|
||||||
def test_create_event(self):
|
|
||||||
response = client.post('/api/1/events', {'slug': 'EVENT', 'name': 'Event'})
|
|
||||||
self.assertEqual(response.status_code, 201)
|
|
||||||
self.assertEqual(response.json()['slug'], 'EVENT')
|
|
||||||
self.assertEqual(response.json()['name'], 'Event')
|
|
||||||
self.assertEqual(len(Event.objects.all()), 1)
|
|
||||||
self.assertEqual(Event.objects.all()[0].slug, 'EVENT')
|
|
||||||
self.assertEqual(Event.objects.all()[0].name, 'Event')
|
|
||||||
|
|
||||||
def test_update_event(self):
|
|
||||||
from rest_framework.test import APIClient
|
|
||||||
event = Event.objects.create(slug='EVENT1', name='Event 1')
|
|
||||||
response = APIClient().put(f'/api/1/events/{event.eid}', {'slug': 'EVENT2', 'name': 'Event 2 new'})
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(response.json()['slug'], 'EVENT2')
|
|
||||||
self.assertEqual(response.json()['name'], 'Event 2 new')
|
|
||||||
self.assertEqual(len(Event.objects.all()), 1)
|
|
||||||
self.assertEqual(Event.objects.all()[0].slug, 'EVENT2')
|
|
||||||
self.assertEqual(Event.objects.all()[0].name, 'Event 2 new')
|
|
||||||
|
|
||||||
def test_remove_event(self):
|
|
||||||
event = Event.objects.create(slug='EVENT1', name='Event 1')
|
|
||||||
Event.objects.create(slug='EVENT2', name='Event 2')
|
|
||||||
self.assertEqual(len(Event.objects.all()), 2)
|
|
||||||
response = client.delete(f'/api/1/events/{event.eid}')
|
|
||||||
self.assertEqual(response.status_code, 204)
|
|
||||||
self.assertEqual(len(Event.objects.all()), 1)
|
|
|
@ -1,133 +0,0 @@
|
||||||
from django.test import TestCase, Client
|
|
||||||
|
|
||||||
from files.models import File
|
|
||||||
from inventory.models import Event, Container, Item
|
|
||||||
|
|
||||||
client = Client()
|
|
||||||
|
|
||||||
|
|
||||||
class ItemTestCase(TestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super().setUp()
|
|
||||||
self.event = Event.objects.create(slug='EVENT', name='Event')
|
|
||||||
self.box = Container.objects.create(name='BOX')
|
|
||||||
|
|
||||||
def test_empty(self):
|
|
||||||
response = client.get(f'/api/1/{self.event.slug}/item')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(response.content, b'[]')
|
|
||||||
|
|
||||||
def test_members(self):
|
|
||||||
item = Item.objects.create(container=self.box, event=self.event, description='1')
|
|
||||||
response = client.get(f'/api/1/{self.event.slug}/item')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(response.json(),
|
|
||||||
[{'uid': 1, 'description': '1', 'box': 'BOX', 'cid': self.box.cid, 'file': None}])
|
|
||||||
|
|
||||||
def test_members_with_file(self):
|
|
||||||
import base64
|
|
||||||
item = Item.objects.create(container=self.box, event=self.event, description='1')
|
|
||||||
file = File.objects.create(item=item, data="data:text/plain;base64," + base64.b64encode(b"foo").decode('utf-8'))
|
|
||||||
response = client.get(f'/api/1/{self.event.slug}/item')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(response.json(),
|
|
||||||
[{'uid': 1, 'description': '1', 'box': 'BOX', 'cid': self.box.cid, 'file': file.hash}])
|
|
||||||
|
|
||||||
def test_multi_members(self):
|
|
||||||
Item.objects.create(container=self.box, event=self.event, description='1')
|
|
||||||
Item.objects.create(container=self.box, event=self.event, description='2')
|
|
||||||
Item.objects.create(container=self.box, event=self.event, description='3')
|
|
||||||
response = client.get(f'/api/1/{self.event.slug}/item')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(len(response.json()), 3)
|
|
||||||
|
|
||||||
def test_create_item(self):
|
|
||||||
response = client.post(f'/api/1/{self.event.slug}/item', {'cid': self.box.cid, 'description': '1'})
|
|
||||||
self.assertEqual(response.status_code, 201)
|
|
||||||
self.assertEqual(response.json(),
|
|
||||||
{'uid': 1, 'description': '1', 'box': 'BOX', 'cid': self.box.cid, 'file': None})
|
|
||||||
self.assertEqual(len(Item.objects.all()), 1)
|
|
||||||
self.assertEqual(Item.objects.all()[0].uid, 1)
|
|
||||||
self.assertEqual(Item.objects.all()[0].description, '1')
|
|
||||||
self.assertEqual(Item.objects.all()[0].container.cid, self.box.cid)
|
|
||||||
|
|
||||||
def test_create_item_with_file(self):
|
|
||||||
import base64
|
|
||||||
response = client.post(f'/api/1/{self.event.slug}/item',
|
|
||||||
{'cid': self.box.cid, 'description': '1',
|
|
||||||
'dataImage': "data:text/plain;base64," + base64.b64encode(b"foo").decode(
|
|
||||||
'utf-8')}, content_type='application/json')
|
|
||||||
self.assertEqual(response.status_code, 201)
|
|
||||||
self.assertEqual(response.json()['uid'], 1)
|
|
||||||
self.assertEqual(response.json()['description'], '1')
|
|
||||||
self.assertEqual(response.json()['box'], 'BOX')
|
|
||||||
self.assertEqual(response.json()['cid'], self.box.cid)
|
|
||||||
self.assertEqual(len(response.json()['file']), 64)
|
|
||||||
self.assertEqual(len(Item.objects.all()), 1)
|
|
||||||
self.assertEqual(Item.objects.all()[0].uid, 1)
|
|
||||||
self.assertEqual(Item.objects.all()[0].description, '1')
|
|
||||||
self.assertEqual(Item.objects.all()[0].container.cid, self.box.cid)
|
|
||||||
self.assertEqual(len(File.objects.all()), 1)
|
|
||||||
|
|
||||||
def test_update_item(self):
|
|
||||||
item = Item.objects.create(container=self.box, event=self.event, description='1')
|
|
||||||
response = client.put(f'/api/1/{self.event.slug}/item/{item.uid}', {'description': '2'},
|
|
||||||
content_type='application/json')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(response.json(),
|
|
||||||
{'uid': 1, 'description': '2', 'box': 'BOX', 'cid': self.box.cid, 'file': None})
|
|
||||||
self.assertEqual(len(Item.objects.all()), 1)
|
|
||||||
self.assertEqual(Item.objects.all()[0].uid, 1)
|
|
||||||
self.assertEqual(Item.objects.all()[0].description, '2')
|
|
||||||
self.assertEqual(Item.objects.all()[0].container.cid, self.box.cid)
|
|
||||||
|
|
||||||
def test_update_item_with_file(self):
|
|
||||||
import base64
|
|
||||||
item = Item.objects.create(container=self.box, event=self.event, description='1')
|
|
||||||
response = client.put(f'/api/1/{self.event.slug}/item/{item.uid}',
|
|
||||||
{'description': '2',
|
|
||||||
'dataImage': "data:text/plain;base64," + base64.b64encode(b"foo").decode('utf-8')},
|
|
||||||
content_type='application/json')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(response.json()['uid'], 1)
|
|
||||||
self.assertEqual(response.json()['description'], '2')
|
|
||||||
self.assertEqual(response.json()['box'], 'BOX')
|
|
||||||
self.assertEqual(response.json()['cid'], self.box.cid)
|
|
||||||
self.assertEqual(len(response.json()['file']), 64)
|
|
||||||
self.assertEqual(len(Item.objects.all()), 1)
|
|
||||||
self.assertEqual(Item.objects.all()[0].uid, 1)
|
|
||||||
self.assertEqual(Item.objects.all()[0].description, '2')
|
|
||||||
self.assertEqual(Item.objects.all()[0].container.cid, self.box.cid)
|
|
||||||
self.assertEqual(len(File.objects.all()), 1)
|
|
||||||
|
|
||||||
def test_delete_item(self):
|
|
||||||
item = Item.objects.create(container=self.box, event=self.event, description='1')
|
|
||||||
Item.objects.create(container=self.box, event=self.event, description='2')
|
|
||||||
self.assertEqual(len(Item.objects.all()), 2)
|
|
||||||
response = client.delete(f'/api/1/{self.event.slug}/item/{item.uid}')
|
|
||||||
self.assertEqual(response.status_code, 204)
|
|
||||||
self.assertEqual(len(Item.objects.all()), 1)
|
|
||||||
|
|
||||||
def test_delete_item2(self):
|
|
||||||
Item.objects.create(container=self.box, event=self.event, description='1')
|
|
||||||
item2 = Item.objects.create(container=self.box, event=self.event, description='2')
|
|
||||||
self.assertEqual(len(Item.objects.all()), 2)
|
|
||||||
response = client.delete(f'/api/1/{self.event.slug}/item/{item2.uid}')
|
|
||||||
self.assertEqual(response.status_code, 204)
|
|
||||||
self.assertEqual(len(Item.objects.all()), 1)
|
|
||||||
item3 = Item.objects.create(container=self.box, event=self.event, description='3')
|
|
||||||
self.assertEqual(item3.uid, 3)
|
|
||||||
self.assertEqual(len(Item.objects.all()), 2)
|
|
||||||
|
|
||||||
def test_item_count(self):
|
|
||||||
Item.objects.create(container=self.box, event=self.event, description='1')
|
|
||||||
Item.objects.create(container=self.box, event=self.event, description='2')
|
|
||||||
response = client.get('/api/1/boxes')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(len(response.json()), 1)
|
|
||||||
self.assertEqual(response.json()[0]['itemCount'], 2)
|
|
||||||
|
|
||||||
def test_item_nonexistent(self):
|
|
||||||
response = client.get(f'/api/1/NOEVENT/item')
|
|
||||||
self.assertEqual(response.status_code, 404)
|
|
|
@ -1,44 +0,0 @@
|
||||||
from django.test import TestCase, Client
|
|
||||||
from django.contrib.auth.models import Permission
|
|
||||||
from knox.models import AuthToken
|
|
||||||
|
|
||||||
from authentication.models import ExtendedUser
|
|
||||||
|
|
||||||
|
|
||||||
class ApiTest(TestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super().setUp()
|
|
||||||
self.user = ExtendedUser.objects.create_user('testuser', 'test', 'test')
|
|
||||||
self.user.user_permissions.add(*Permission.objects.all())
|
|
||||||
self.user.save()
|
|
||||||
self.token = AuthToken.objects.create(user=self.user)
|
|
||||||
self.client = Client(headers={'Authorization': 'Token ' + self.token[1]})
|
|
||||||
|
|
||||||
def test_root(self):
|
|
||||||
from core.settings import SYSTEM3_VERSION
|
|
||||||
response = self.client.get('/api/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(response.json()["framework_version"], SYSTEM3_VERSION)
|
|
||||||
|
|
||||||
def test_events(self):
|
|
||||||
response = self.client.get('/api/2/events/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(response.json(), [])
|
|
||||||
|
|
||||||
def test_containers(self):
|
|
||||||
response = self.client.get('/api/2/boxes/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(response.json(), [])
|
|
||||||
|
|
||||||
def test_files(self):
|
|
||||||
response = self.client.get('/api/2/files/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(response.json(), [])
|
|
||||||
|
|
||||||
def test_items(self):
|
|
||||||
from inventory.models import Event
|
|
||||||
Event.objects.create(slug='TEST1', name='Event')
|
|
||||||
response = self.client.get('/api/2/TEST1/items/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(response.json(), [])
|
|
|
@ -1,66 +0,0 @@
|
||||||
from django.test import TestCase, Client
|
|
||||||
from django.contrib.auth.models import Permission
|
|
||||||
from knox.models import AuthToken
|
|
||||||
|
|
||||||
from authentication.models import ExtendedUser
|
|
||||||
from inventory.models import Container
|
|
||||||
|
|
||||||
|
|
||||||
class ContainerTestCase(TestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.user = ExtendedUser.objects.create_user('testuser', 'test', 'test')
|
|
||||||
self.user.user_permissions.add(*Permission.objects.all())
|
|
||||||
self.token = AuthToken.objects.create(user=self.user)
|
|
||||||
self.client = Client(headers={'Authorization': 'Token ' + self.token[1]})
|
|
||||||
|
|
||||||
def test_empty(self):
|
|
||||||
response = self.client.get('/api/2/boxes/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(response.json(), [])
|
|
||||||
|
|
||||||
def test_members(self):
|
|
||||||
Container.objects.create(name='BOX')
|
|
||||||
response = self.client.get('/api/2/boxes/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(len(response.json()), 1)
|
|
||||||
self.assertEqual(response.json()[0]['cid'], 1)
|
|
||||||
self.assertEqual(response.json()[0]['name'], 'BOX')
|
|
||||||
self.assertEqual(response.json()[0]['itemCount'], 0)
|
|
||||||
|
|
||||||
def test_multi_members(self):
|
|
||||||
Container.objects.create(name='BOX 1')
|
|
||||||
Container.objects.create(name='BOX 2')
|
|
||||||
Container.objects.create(name='BOX 3')
|
|
||||||
response = self.client.get('/api/2/boxes/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(len(response.json()), 3)
|
|
||||||
|
|
||||||
def test_create_container(self):
|
|
||||||
response = self.client.post('/api/2/box/', {'name': 'BOX'})
|
|
||||||
self.assertEqual(response.status_code, 201)
|
|
||||||
self.assertEqual(response.json()['cid'], 1)
|
|
||||||
self.assertEqual(response.json()['name'], 'BOX')
|
|
||||||
self.assertEqual(response.json()['itemCount'], 0)
|
|
||||||
self.assertEqual(len(Container.objects.all()), 1)
|
|
||||||
self.assertEqual(Container.objects.all()[0].cid, 1)
|
|
||||||
self.assertEqual(Container.objects.all()[0].name, 'BOX')
|
|
||||||
|
|
||||||
def test_update_container(self):
|
|
||||||
box = Container.objects.create(name='BOX 1')
|
|
||||||
response = self.client.put(f'/api/2/box/{box.cid}/', {'name': 'BOX 2'}, content_type='application/json')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(response.json()['cid'], 1)
|
|
||||||
self.assertEqual(response.json()['name'], 'BOX 2')
|
|
||||||
self.assertEqual(response.json()['itemCount'], 0)
|
|
||||||
self.assertEqual(len(Container.objects.all()), 1)
|
|
||||||
self.assertEqual(Container.objects.all()[0].cid, 1)
|
|
||||||
self.assertEqual(Container.objects.all()[0].name, 'BOX 2')
|
|
||||||
|
|
||||||
def test_delete_container(self):
|
|
||||||
box = Container.objects.create(name='BOX 1')
|
|
||||||
Container.objects.create(name='BOX 2')
|
|
||||||
self.assertEqual(len(Container.objects.all()), 2)
|
|
||||||
response = self.client.delete(f'/api/2/box/{box.cid}/')
|
|
||||||
self.assertEqual(response.status_code, 204)
|
|
||||||
self.assertEqual(len(Container.objects.all()), 1)
|
|
|
@ -1,68 +0,0 @@
|
||||||
from django.test import TestCase, Client
|
|
||||||
from inventory.models import Event
|
|
||||||
|
|
||||||
client = Client()
|
|
||||||
|
|
||||||
|
|
||||||
class EventTestCase(TestCase):
|
|
||||||
|
|
||||||
def test_empty(self):
|
|
||||||
response = client.get('/api/2/events/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(response.json(), [])
|
|
||||||
|
|
||||||
def test_members(self):
|
|
||||||
Event.objects.create(slug='EVENT', name='Event')
|
|
||||||
response = client.get('/api/2/events/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(len(response.json()), 1)
|
|
||||||
self.assertEqual(response.json()[0]['slug'], 'EVENT')
|
|
||||||
self.assertEqual(response.json()[0]['name'], 'Event')
|
|
||||||
|
|
||||||
def test_multi_members(self):
|
|
||||||
Event.objects.create(slug='EVENT1', name='Event 1')
|
|
||||||
Event.objects.create(slug='EVENT2', name='Event 2')
|
|
||||||
Event.objects.create(slug='EVENT3', name='Event 3')
|
|
||||||
response = client.get('/api/2/events/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(len(response.json()), 3)
|
|
||||||
|
|
||||||
def test_create_event(self):
|
|
||||||
response = client.post('/api/2/events/', {'slug': 'EVENT', 'name': 'Event'})
|
|
||||||
self.assertEqual(response.status_code, 201)
|
|
||||||
self.assertEqual(response.json()['slug'], 'EVENT')
|
|
||||||
self.assertEqual(response.json()['name'], 'Event')
|
|
||||||
self.assertEqual(len(Event.objects.all()), 1)
|
|
||||||
self.assertEqual(Event.objects.all()[0].slug, 'EVENT')
|
|
||||||
self.assertEqual(Event.objects.all()[0].name, 'Event')
|
|
||||||
|
|
||||||
def test_update_event(self):
|
|
||||||
from rest_framework.test import APIClient
|
|
||||||
event = Event.objects.create(slug='EVENT1', name='Event 1')
|
|
||||||
response = APIClient().put(f'/api/2/events/{event.eid}/', {'slug': 'EVENT2', 'name': 'Event 2 new'})
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(response.json()['slug'], 'EVENT2')
|
|
||||||
self.assertEqual(response.json()['name'], 'Event 2 new')
|
|
||||||
self.assertEqual(len(Event.objects.all()), 1)
|
|
||||||
self.assertEqual(Event.objects.all()[0].slug, 'EVENT2')
|
|
||||||
self.assertEqual(Event.objects.all()[0].name, 'Event 2 new')
|
|
||||||
|
|
||||||
def test_remove_event(self):
|
|
||||||
event = Event.objects.create(slug='EVENT1', name='Event 1')
|
|
||||||
Event.objects.create(slug='EVENT2', name='Event 2')
|
|
||||||
self.assertEqual(len(Event.objects.all()), 2)
|
|
||||||
response = client.delete(f'/api/2/events/{event.eid}/')
|
|
||||||
self.assertEqual(response.status_code, 204)
|
|
||||||
self.assertEqual(len(Event.objects.all()), 1)
|
|
||||||
|
|
||||||
def test_items2(self):
|
|
||||||
from mail.models import EventAddress
|
|
||||||
event1 = Event.objects.create(slug='TEST1', name='Event')
|
|
||||||
EventAddress.objects.create(event=Event.objects.get(slug='TEST1'), address='foo@bar.baz')
|
|
||||||
response = self.client.get('/api/2/events/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(1, len(response.json()))
|
|
||||||
self.assertEqual('TEST1', response.json()[0]['slug'])
|
|
||||||
self.assertEqual('Event', response.json()[0]['name'])
|
|
||||||
self.assertEqual(1, len(response.json()[0]['addresses']))
|
|
||||||
|
|
|
@ -1,171 +0,0 @@
|
||||||
from django.utils import timezone
|
|
||||||
from django.test import TestCase, Client
|
|
||||||
from django.contrib.auth.models import Permission
|
|
||||||
from knox.models import AuthToken
|
|
||||||
|
|
||||||
from authentication.models import ExtendedUser
|
|
||||||
from files.models import File
|
|
||||||
from inventory.models import Event, Container, Item
|
|
||||||
|
|
||||||
|
|
||||||
class ItemTestCase(TestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super().setUp()
|
|
||||||
self.event = Event.objects.create(slug='EVENT', name='Event')
|
|
||||||
self.box = Container.objects.create(name='BOX')
|
|
||||||
self.user = ExtendedUser.objects.create_user('testuser', 'test', 'test')
|
|
||||||
self.user.user_permissions.add(*Permission.objects.all())
|
|
||||||
self.token = AuthToken.objects.create(user=self.user)
|
|
||||||
self.client = Client(headers={'Authorization': 'Token ' + self.token[1]})
|
|
||||||
|
|
||||||
def test_empty(self):
|
|
||||||
response = self.client.get(f'/api/2/{self.event.slug}/item/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(response.content, b'[]')
|
|
||||||
|
|
||||||
def test_members(self):
|
|
||||||
item = Item.objects.create(container=self.box, event=self.event, description='1')
|
|
||||||
response = self.client.get(f'/api/2/{self.event.slug}/item/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(response.json(),
|
|
||||||
[{'uid': 1, 'description': '1', 'box': 'BOX', 'cid': self.box.cid, 'file': None,
|
|
||||||
'returned': False}])
|
|
||||||
|
|
||||||
def test_members_with_file(self):
|
|
||||||
import base64
|
|
||||||
item = Item.objects.create(container=self.box, event=self.event, description='1')
|
|
||||||
file = File.objects.create(item=item, data="data:text/plain;base64," + base64.b64encode(b"foo").decode('utf-8'))
|
|
||||||
response = self.client.get(f'/api/2/{self.event.slug}/item/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(response.json(),
|
|
||||||
[{'uid': 1, 'description': '1', 'box': 'BOX', 'cid': self.box.cid, 'file': file.hash,
|
|
||||||
'returned': False}])
|
|
||||||
|
|
||||||
def test_multi_members(self):
|
|
||||||
Item.objects.create(container=self.box, event=self.event, description='1')
|
|
||||||
Item.objects.create(container=self.box, event=self.event, description='2')
|
|
||||||
Item.objects.create(container=self.box, event=self.event, description='3')
|
|
||||||
response = self.client.get(f'/api/2/{self.event.slug}/item/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(len(response.json()), 3)
|
|
||||||
|
|
||||||
def test_create_item(self):
|
|
||||||
response = self.client.post(f'/api/2/{self.event.slug}/item/', {'cid': self.box.cid, 'description': '1'})
|
|
||||||
self.assertEqual(response.status_code, 201)
|
|
||||||
self.assertEqual(response.json(),
|
|
||||||
{'uid': 1, 'description': '1', 'box': 'BOX', 'cid': self.box.cid, 'file': None,
|
|
||||||
'returned': False})
|
|
||||||
self.assertEqual(len(Item.objects.all()), 1)
|
|
||||||
self.assertEqual(Item.objects.all()[0].uid, 1)
|
|
||||||
self.assertEqual(Item.objects.all()[0].description, '1')
|
|
||||||
self.assertEqual(Item.objects.all()[0].container.cid, self.box.cid)
|
|
||||||
|
|
||||||
def test_create_item_with_file(self):
|
|
||||||
import base64
|
|
||||||
response = self.client.post(f'/api/2/{self.event.slug}/item/',
|
|
||||||
{'cid': self.box.cid, 'description': '1',
|
|
||||||
'dataImage': "data:text/plain;base64," + base64.b64encode(b"foo").decode(
|
|
||||||
'utf-8')}, content_type='application/json')
|
|
||||||
self.assertEqual(response.status_code, 201)
|
|
||||||
self.assertEqual(response.json()['uid'], 1)
|
|
||||||
self.assertEqual(response.json()['description'], '1')
|
|
||||||
self.assertEqual(response.json()['box'], 'BOX')
|
|
||||||
self.assertEqual(response.json()['cid'], self.box.cid)
|
|
||||||
self.assertEqual(len(response.json()['file']), 64)
|
|
||||||
self.assertEqual(len(Item.objects.all()), 1)
|
|
||||||
self.assertEqual(Item.objects.all()[0].uid, 1)
|
|
||||||
self.assertEqual(Item.objects.all()[0].description, '1')
|
|
||||||
self.assertEqual(Item.objects.all()[0].container.cid, self.box.cid)
|
|
||||||
self.assertEqual(len(File.objects.all()), 1)
|
|
||||||
|
|
||||||
def test_update_item(self):
|
|
||||||
item = Item.objects.create(container=self.box, event=self.event, description='1')
|
|
||||||
response = self.client.put(f'/api/2/{self.event.slug}/item/{item.uid}/', {'description': '2'},
|
|
||||||
content_type='application/json')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(response.json(),
|
|
||||||
{'uid': 1, 'description': '2', 'box': 'BOX', 'cid': self.box.cid, 'file': None,
|
|
||||||
'returned': False})
|
|
||||||
self.assertEqual(len(Item.objects.all()), 1)
|
|
||||||
self.assertEqual(Item.objects.all()[0].uid, 1)
|
|
||||||
self.assertEqual(Item.objects.all()[0].description, '2')
|
|
||||||
self.assertEqual(Item.objects.all()[0].container.cid, self.box.cid)
|
|
||||||
|
|
||||||
def test_update_item_with_file(self):
|
|
||||||
import base64
|
|
||||||
item = Item.objects.create(container=self.box, event=self.event, description='1')
|
|
||||||
response = self.client.put(f'/api/2/{self.event.slug}/item/{item.uid}/',
|
|
||||||
{'description': '2',
|
|
||||||
'dataImage': "data:text/plain;base64," + base64.b64encode(b"foo").decode('utf-8')},
|
|
||||||
content_type='application/json')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(response.json()['uid'], 1)
|
|
||||||
self.assertEqual(response.json()['description'], '2')
|
|
||||||
self.assertEqual(response.json()['box'], 'BOX')
|
|
||||||
self.assertEqual(response.json()['cid'], self.box.cid)
|
|
||||||
self.assertEqual(len(response.json()['file']), 64)
|
|
||||||
self.assertEqual(len(Item.objects.all()), 1)
|
|
||||||
self.assertEqual(Item.objects.all()[0].uid, 1)
|
|
||||||
self.assertEqual(Item.objects.all()[0].description, '2')
|
|
||||||
self.assertEqual(Item.objects.all()[0].container.cid, self.box.cid)
|
|
||||||
self.assertEqual(len(File.objects.all()), 1)
|
|
||||||
|
|
||||||
def test_delete_item(self):
|
|
||||||
item = Item.objects.create(container=self.box, event=self.event, description='1')
|
|
||||||
Item.objects.create(container=self.box, event=self.event, description='2')
|
|
||||||
self.assertEqual(len(Item.objects.all()), 2)
|
|
||||||
response = self.client.delete(f'/api/2/{self.event.slug}/item/{item.uid}/')
|
|
||||||
self.assertEqual(response.status_code, 204)
|
|
||||||
self.assertEqual(len(Item.objects.all()), 1)
|
|
||||||
|
|
||||||
def test_delete_item2(self):
|
|
||||||
Item.objects.create(container=self.box, event=self.event, description='1')
|
|
||||||
item2 = Item.objects.create(container=self.box, event=self.event, description='2')
|
|
||||||
self.assertEqual(len(Item.objects.all()), 2)
|
|
||||||
response = self.client.delete(f'/api/2/{self.event.slug}/item/{item2.uid}/')
|
|
||||||
self.assertEqual(response.status_code, 204)
|
|
||||||
self.assertEqual(len(Item.objects.all()), 1)
|
|
||||||
item3 = Item.objects.create(container=self.box, event=self.event, description='3')
|
|
||||||
self.assertEqual(item3.uid, 3)
|
|
||||||
self.assertEqual(len(Item.objects.all()), 2)
|
|
||||||
|
|
||||||
def test_item_count(self):
|
|
||||||
Item.objects.create(container=self.box, event=self.event, description='1')
|
|
||||||
Item.objects.create(container=self.box, event=self.event, description='2')
|
|
||||||
response = self.client.get('/api/2/boxes/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(len(response.json()), 1)
|
|
||||||
self.assertEqual(response.json()[0]['itemCount'], 2)
|
|
||||||
|
|
||||||
def test_item_nonexistent(self):
|
|
||||||
response = self.client.get(f'/api/2/NOEVENT/item/')
|
|
||||||
self.assertEqual(response.status_code, 404)
|
|
||||||
|
|
||||||
def test_item_return(self):
|
|
||||||
item = Item.objects.create(container=self.box, event=self.event, description='1')
|
|
||||||
self.assertEqual(item.returned_at, None)
|
|
||||||
response = self.client.get(f'/api/2/{self.event.slug}/item/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(len(response.json()), 1)
|
|
||||||
response = self.client.patch(f'/api/2/{self.event.slug}/item/{item.uid}/', {'returned': True},
|
|
||||||
content_type='application/json')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
item.refresh_from_db()
|
|
||||||
self.assertNotEqual(item.returned_at, None)
|
|
||||||
response = self.client.get(f'/api/2/{self.event.slug}/item/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(len(response.json()), 0)
|
|
||||||
|
|
||||||
def test_item_show_not_returned(self):
|
|
||||||
item1 = Item.objects.create(container=self.box, event=self.event, description='1')
|
|
||||||
item2 = Item.objects.create(container=self.box, event=self.event, description='2')
|
|
||||||
response = self.client.get(f'/api/2/{self.event.slug}/item/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(len(response.json()), 2)
|
|
||||||
item2.returned_at = timezone.now()
|
|
||||||
item2.save()
|
|
||||||
response = self.client.get(f'/api/2/{self.event.slug}/item/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertEqual(len(response.json()), 1)
|
|
||||||
self.assertEqual(response.json()[0]['uid'], item1.uid)
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue