Compare commits
No commits in common. "system2/backend" and "testing" have entirely different histories.
system2/ba
...
testing
265 changed files with 12930 additions and 2491 deletions
|
@ -1,15 +0,0 @@
|
|||
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
19
.env.example
|
@ -1,19 +0,0 @@
|
|||
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
|
35
.forgejo/issue_template/bug.yml
Normal file
35
.forgejo/issue_template/bug.yml
Normal file
|
@ -0,0 +1,35 @@
|
|||
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
|
27
.forgejo/issue_template/feature.yml
Normal file
27
.forgejo/issue_template/feature.yml
Normal file
|
@ -0,0 +1,27 @@
|
|||
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
|
60
.forgejo/workflows/deploy_staging.yml
Normal file
60
.forgejo/workflows/deploy_staging.yml
Normal file
|
@ -0,0 +1,60 @@
|
|||
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
|
21
.forgejo/workflows/test.yml
Normal file
21
.forgejo/workflows/test.yml
Normal file
|
@ -0,0 +1,21 @@
|
|||
on:
|
||||
pull_request:
|
||||
push:
|
||||
|
||||
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
|
15
.gitignore
vendored
15
.gitignore
vendored
|
@ -1,15 +1,8 @@
|
|||
/vendor
|
||||
|
||||
/.idea
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
.env
|
||||
.local
|
||||
/public/docs
|
||||
/public/staticimages
|
||||
/public/thumbnails
|
||||
/resources/docs
|
||||
|
||||
composer.lock
|
||||
composer.phar
|
||||
|
||||
.phpunit.result.cache
|
||||
staticfiles/
|
||||
userfiles/
|
||||
*.db
|
|
@ -1,9 +0,0 @@
|
|||
php:
|
||||
preset: laravel
|
||||
enabled:
|
||||
- alpha_ordered_imports
|
||||
disabled:
|
||||
- length_ordered_imports
|
||||
- unused_use
|
||||
js: true
|
||||
css: true
|
|
@ -1,29 +0,0 @@
|
|||
<?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)
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
<?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();
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
<?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'];
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
abstract class Event
|
||||
{
|
||||
use SerializesModels;
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
class ExampleEvent extends Event
|
||||
{
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
<?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
53
app/File.php
|
@ -1,53 +0,0 @@
|
|||
<?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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Laravel\Lumen\Routing\Controller as BaseController;
|
||||
|
||||
class Controller extends BaseController
|
||||
{
|
||||
//
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
|
@ -1,83 +0,0 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
<?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
65
app/Item.php
|
@ -1,65 +0,0 @@
|
|||
<?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();
|
||||
}
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
<?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()
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
<?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;
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
<?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)
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
<?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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
<?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
32
app/User.php
|
@ -1,32 +0,0 @@
|
|||
<?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
35
artisan
|
@ -1,35 +0,0 @@
|
|||
#!/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
33
backup.sh
|
@ -1,33 +0,0 @@
|
|||
#!/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
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
<?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;
|
|
@ -1,44 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
|
@ -1,245 +0,0 @@
|
|||
<?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,
|
||||
];
|
|
@ -1,42 +0,0 @@
|
|||
<?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'
|
||||
];
|
||||
|
||||
?>
|
|
@ -1,6 +0,0 @@
|
|||
DROP DATABASE IF EXISTS lostfound;
|
||||
CREATE DATABASE lostfound;
|
||||
|
||||
CREATE OR REPLACE USER lostfound IDENTIFIED BY 'lostfound';
|
||||
|
||||
GRANT ALL privileges ON `lostfound`.* TO 'lostfound';
|
|
@ -1,9 +0,0 @@
|
|||
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;
|
|
@ -1,7 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
cd /app
|
||||
echo "executing COMPOSER UPDATE"
|
||||
composer update
|
||||
echo "executing DATABASE MIGRATE"
|
||||
php artisan migrate --force
|
|
@ -1,15 +0,0 @@
|
|||
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;
|
||||
|
||||
|
||||
}
|
14
core/.coveragerc
Normal file
14
core/.coveragerc
Normal file
|
@ -0,0 +1,14 @@
|
|||
[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
Normal file
129
core/.gitignore
vendored
Normal file
|
@ -0,0 +1,129 @@
|
|||
# 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/
|
14
core/authentication/admin.py
Normal file
14
core/authentication/admin.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
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)
|
89
core/authentication/api_v2.py
Normal file
89
core/authentication/api_v2.py
Normal file
|
@ -0,0 +1,89 @@
|
|||
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
|
||||
from authentication.serializers import UserSerializer, GroupSerializer
|
||||
|
||||
|
||||
class UserViewSet(viewsets.ModelViewSet):
|
||||
queryset = ExtendedUser.objects.all()
|
||||
serializer_class = UserSerializer
|
||||
|
||||
|
||||
class GroupViewSet(viewsets.ModelViewSet):
|
||||
queryset = Group.objects.all()
|
||||
serializer_class = GroupSerializer
|
||||
|
||||
|
||||
@receiver(post_save, sender=ExtendedUser)
|
||||
def create_auth_token(sender, instance=None, created=False, **kwargs):
|
||||
if created:
|
||||
AuthToken.objects.create(user=instance)
|
||||
|
||||
|
||||
@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),
|
||||
]
|
67
core/authentication/migrations/0001_initial.py
Normal file
67
core/authentication/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,67 @@
|
|||
# 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'),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,45 @@
|
|||
# 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')},
|
||||
),
|
||||
]
|
33
core/authentication/migrations/0003_groups.py
Normal file
33
core/authentication/migrations/0003_groups.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
# 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),
|
||||
]
|
18
core/authentication/migrations/0004_legacy_user.py
Normal file
18
core/authentication/migrations/0004_legacy_user.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
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)
|
||||
]
|
|
@ -0,0 +1,26 @@
|
|||
# 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),
|
||||
),
|
||||
]
|
62
core/authentication/models.py
Normal file
62
core/authentication/models.py
Normal file
|
@ -0,0 +1,62 @@
|
|||
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')
|
32
core/authentication/serializers.py
Normal file
32
core/authentication/serializers.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
from rest_framework import serializers
|
||||
from django.contrib.auth.models import Group
|
||||
|
||||
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()))
|
||||
|
||||
|
||||
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()]
|
0
core/authentication/tests/v2/__init__.py
Normal file
0
core/authentication/tests/v2/__init__.py
Normal file
90
core/authentication/tests/v2/test_permissions.py
Normal file
90
core/authentication/tests/v2/test_permissions.py
Normal file
|
@ -0,0 +1,90 @@
|
|||
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)
|
||||
|
183
core/authentication/tests/v2/test_users.py
Normal file
183
core/authentication/tests/v2/test_users.py
Normal file
|
@ -0,0 +1,183 @@
|
|||
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')
|
0
core/core/__init__.py
Normal file
0
core/core/__init__.py
Normal file
66
core/core/asgi.py
Normal file
66
core/core/asgi.py
Normal file
|
@ -0,0 +1,66 @@
|
|||
"""
|
||||
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,
|
||||
})
|
29
core/core/globals.py
Normal file
29
core/core/globals.py
Normal file
|
@ -0,0 +1,29 @@
|
|||
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
|
228
core/core/settings.py
Normal file
228
core/core/settings.py
Normal file
|
@ -0,0 +1,228 @@
|
|||
"""
|
||||
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'))
|
||||
|
||||
PRIMARY_HOST = os.getenv('HTTP_HOST', 'localhost')
|
||||
|
||||
ALLOWED_HOSTS = [PRIMARY_HOST]
|
||||
|
||||
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',
|
||||
'init_command': "SET sql_mode='STRICT_TRANS_TABLES'"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# 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'
|
33
core/core/test_runner.py
Normal file
33
core/core/test_runner.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
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:',
|
||||
}
|
||||
}
|
33
core/core/urls.py
Normal file
33
core/core/urls.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
"""
|
||||
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/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')),
|
||||
]
|
15
core/core/version.py
Normal file
15
core/core/version.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
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",
|
||||
})
|
0
core/files/__init__.py
Normal file
0
core/files/__init__.py
Normal file
10
core/files/admin.py
Normal file
10
core/files/admin.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from files.models import File
|
||||
|
||||
|
||||
class FileAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
|
||||
|
||||
admin.site.register(File, FileAdmin)
|
24
core/files/api_v2.py
Normal file
24
core/files/api_v2.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
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
|
92
core/files/media_v2.py
Normal file
92
core/files/media_v2.py
Normal file
|
@ -0,0 +1,92 @@
|
|||
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),
|
||||
]
|
30
core/files/migrations/0001_initial.py
Normal file
30
core/files/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
# 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')),
|
||||
],
|
||||
),
|
||||
]
|
19
core/files/migrations/0002_alter_file_file.py
Normal file
19
core/files/migrations/0002_alter_file_file.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
# 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),
|
||||
),
|
||||
]
|
24
core/files/migrations/0003_ensure_creation_date.py
Normal file
24
core/files/migrations/0003_ensure_creation_date.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
# Generated by Django 4.2.7 on 2024-11-21 22:40
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('files', '0002_alter_file_file'),
|
||||
]
|
||||
|
||||
def set_creation_date(apps, schema_editor):
|
||||
File = apps.get_model('files', 'File')
|
||||
for file in File.objects.all():
|
||||
if file.created_at is None:
|
||||
if not file.item.created_at is None:
|
||||
file.created_at = file.item.created_at
|
||||
else:
|
||||
file.created_at = max(File.objects.filter(
|
||||
id__lt=file.id, created_at__isnull=False).values_list('created_at', flat=True))
|
||||
file.save()
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(set_creation_date),
|
||||
]
|
0
core/files/migrations/__init__.py
Normal file
0
core/files/migrations/__init__.py
Normal file
95
core/files/models.py
Normal file
95
core/files/models.py
Normal file
|
@ -0,0 +1,95 @@
|
|||
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
|
0
core/files/tests/__init__.py
Normal file
0
core/files/tests/__init__.py
Normal file
0
core/files/tests/v2/__init__.py
Normal file
0
core/files/tests/v2/__init__.py
Normal file
55
core/files/tests/v2/test_files.py
Normal file
55
core/files/tests/v2/test_files.py
Normal file
|
@ -0,0 +1,55 @@
|
|||
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)
|
30
core/helper.py
Normal file
30
core/helper.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
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
|
0
core/inventory/__init__.py
Normal file
0
core/inventory/__init__.py
Normal file
38
core/inventory/admin.py
Normal file
38
core/inventory/admin.py
Normal file
|
@ -0,0 +1,38 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from inventory.models import Item, Container, ItemPlacement, Comment, 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)
|
||||
|
||||
|
||||
class ItemPlacementAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
|
||||
|
||||
admin.site.register(ItemPlacement, ItemPlacementAdmin)
|
||||
|
||||
|
||||
class CommentAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
|
||||
|
||||
admin.site.register(Comment, CommentAdmin)
|
121
core/inventory/api_v2.py
Normal file
121
core/inventory/api_v2.py
Normal file
|
@ -0,0 +1,121 @@
|
|||
from django.urls import re_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, SearchResultSerializer
|
||||
|
||||
from base64 import b64decode
|
||||
|
||||
|
||||
class EventViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = EventSerializer
|
||||
queryset = Event.objects.all()
|
||||
permission_classes = []
|
||||
|
||||
|
||||
class ContainerViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = ContainerSerializer
|
||||
queryset = Container.objects.all()
|
||||
|
||||
|
||||
def filter_items(items, query):
|
||||
query_tokens = query.split(' ')
|
||||
for item in items:
|
||||
value = 0
|
||||
for token in query_tokens:
|
||||
if token in item.description:
|
||||
value += 1
|
||||
if value > 0:
|
||||
yield {'search_score': value, 'item': item}
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def search_items(request, event_slug, query):
|
||||
try:
|
||||
event = Event.objects.get(slug=event_slug)
|
||||
if not request.user.has_event_perm(event, 'view_item'):
|
||||
return Response(status=403)
|
||||
items = filter_items(Item.objects.filter(event=event), b64decode(query).decode('utf-8'))
|
||||
return Response(SearchResultSerializer(items, many=True).data)
|
||||
except Event.DoesNotExist:
|
||||
return Response(status=404)
|
||||
|
||||
|
||||
@api_view(['GET', 'POST'])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def item(request, event_slug):
|
||||
try:
|
||||
event = None
|
||||
if event_slug != 'none':
|
||||
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)
|
||||
return Response(status=400)
|
||||
except Event.DoesNotExist:
|
||||
return Response(status=404)
|
||||
except KeyError:
|
||||
return Response(status=400)
|
||||
|
||||
|
||||
@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, id=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 + [
|
||||
re_path(r'^(?P<event_slug>[\w-]+)/items/$', item, name='item'),
|
||||
re_path(r'^(?P<event_slug>[\w-]+)/items/(?P<query>[-A-Za-z0-9+/]*={0,3})/$', search_items, name='search_items'),
|
||||
re_path(r'^(?P<event_slug>[\w-]+)/item/$', item, name='item'),
|
||||
re_path(r'^(?P<event_slug>[\w-]+)/item/(?P<id>\d+)/$', item_by_id, name='item_by_id'),
|
||||
]
|
54
core/inventory/migrations/0001_initial.py
Normal file
54
core/inventory/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,54 @@
|
|||
# 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')},
|
||||
},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,33 @@
|
|||
# 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),
|
||||
),
|
||||
]
|
17
core/inventory/migrations/0003_alter_item_options.py
Normal file
17
core/inventory/migrations/0003_alter_item_options.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
# 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')]},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,23 @@
|
|||
# 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),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,52 @@
|
|||
# Generated by Django 4.2.7 on 2024-11-19 22:56
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0004_alter_event_created_at_alter_item_created_at'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='container',
|
||||
old_name='cid',
|
||||
new_name='id',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='event',
|
||||
old_name='eid',
|
||||
new_name='id',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='item',
|
||||
old_name='iid',
|
||||
new_name='id',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='item',
|
||||
old_name='uid',
|
||||
new_name='uid_deprecated',
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='item',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='item',
|
||||
name='container',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.container'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='item',
|
||||
name='event',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.event'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='item',
|
||||
unique_together={('uid_deprecated', 'event')},
|
||||
),
|
||||
]
|
17
core/inventory/migrations/0006_alter_event_table.py
Normal file
17
core/inventory/migrations/0006_alter_event_table.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 4.2.7 on 2024-11-20 01:39
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0005_rename_cid_container_id_rename_eid_event_id_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelTable(
|
||||
name='event',
|
||||
table='common_event',
|
||||
),
|
||||
]
|
|
@ -0,0 +1,52 @@
|
|||
# Generated by Django 4.2.7 on 2024-11-23 15:27
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('inventory', '0006_alter_event_table'),
|
||||
]
|
||||
|
||||
def set_initial_container(apps, schema_editor):
|
||||
Item = apps.get_model('inventory', 'Item')
|
||||
for item in Item.objects.all():
|
||||
item.container_history.get_or_create(container=item.container_old)
|
||||
item.save()
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='item',
|
||||
old_name='container',
|
||||
new_name='container_old',
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ItemPlacement',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('timestamp', models.DateTimeField(auto_now_add=True)),
|
||||
('container',
|
||||
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='item_history',
|
||||
to='inventory.container')),
|
||||
('item',
|
||||
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='container_history',
|
||||
to='inventory.item')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Comment',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('comment', models.TextField()),
|
||||
('timestamp', models.DateTimeField(auto_now_add=True)),
|
||||
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments',
|
||||
to='inventory.item')),
|
||||
],
|
||||
),
|
||||
migrations.RunPython(set_initial_container),
|
||||
migrations.RemoveField(
|
||||
model_name='item',
|
||||
name='container_old',
|
||||
),
|
||||
]
|
0
core/inventory/migrations/__init__.py
Normal file
0
core/inventory/migrations/__init__.py
Normal file
113
core/inventory/models.py
Normal file
113
core/inventory/models.py
Normal file
|
@ -0,0 +1,113 @@
|
|||
from itertools import groupby
|
||||
|
||||
from django.db import models
|
||||
from django_softdelete.models import SoftDeleteModel, SoftDeleteManager
|
||||
|
||||
|
||||
class ItemManager(SoftDeleteManager):
|
||||
|
||||
def create(self, **kwargs):
|
||||
container = kwargs.pop('container')
|
||||
if 'uid_deprecated' in kwargs:
|
||||
raise ValueError('uid_deprecated must not be set manually')
|
||||
uid_deprecated = Item.all_objects.filter(event=kwargs['event']).count() + 1
|
||||
kwargs['uid_deprecated'] = uid_deprecated
|
||||
item = super().create(**kwargs)
|
||||
item.container = container
|
||||
return item
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().filter(returned_at__isnull=True)
|
||||
|
||||
|
||||
class Item(SoftDeleteModel):
|
||||
id = models.AutoField(primary_key=True)
|
||||
uid_deprecated = models.IntegerField()
|
||||
description = models.TextField()
|
||||
event = models.ForeignKey('Event', models.CASCADE)
|
||||
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)
|
||||
|
||||
@property
|
||||
def container(self):
|
||||
try:
|
||||
return self.container_history.order_by('-timestamp').first().container
|
||||
except AttributeError:
|
||||
return None
|
||||
|
||||
@container.setter
|
||||
def container(self, value):
|
||||
if self.container == value:
|
||||
return
|
||||
self.container_history.create(container=value)
|
||||
|
||||
@property
|
||||
def related_issues(self):
|
||||
groups = groupby(self.issue_relation_changes.all(), lambda rel: rel.issue_thread.id)
|
||||
return [sorted(v, key=lambda r: r.timestamp)[0].issue_thread for k, v in groups]
|
||||
|
||||
objects = ItemManager()
|
||||
all_objects = models.Manager()
|
||||
|
||||
class Meta:
|
||||
unique_together = (('uid_deprecated', 'event'),)
|
||||
permissions = [
|
||||
('match_item', 'Can match item')
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return '[' + str(self.id) + ']' + self.description
|
||||
|
||||
|
||||
class Container(SoftDeleteModel):
|
||||
id = 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)
|
||||
|
||||
@property
|
||||
def items(self):
|
||||
try:
|
||||
history = self.item_history.order_by('-timestamp').all()
|
||||
return [v for k, v in groupby(history, key=lambda item: item.item.id)]
|
||||
except AttributeError:
|
||||
return []
|
||||
|
||||
def __str__(self):
|
||||
return '[' + str(self.id) + ']' + self.name
|
||||
|
||||
|
||||
class ItemPlacement(models.Model):
|
||||
id = models.AutoField(primary_key=True)
|
||||
item = models.ForeignKey('Item', models.CASCADE, related_name='container_history')
|
||||
container = models.ForeignKey('Container', models.CASCADE, related_name='item_history')
|
||||
timestamp = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
|
||||
class Comment(models.Model):
|
||||
id = models.AutoField(primary_key=True)
|
||||
item = models.ForeignKey(Item, on_delete=models.CASCADE, related_name='comments')
|
||||
comment = models.TextField()
|
||||
timestamp = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.item) + ' comment #' + str(self.id)
|
||||
|
||||
|
||||
class Event(models.Model):
|
||||
id = 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
|
||||
|
||||
class Meta:
|
||||
db_table = 'common_event'
|
120
core/inventory/serializers.py
Normal file
120
core/inventory/serializers.py
Normal file
|
@ -0,0 +1,120 @@
|
|||
from django.utils import timezone
|
||||
from rest_framework import serializers
|
||||
from rest_framework.relations import SlugRelatedField
|
||||
|
||||
from files.models import File
|
||||
from inventory.models import Event, Container, Item
|
||||
from inventory.shared_serializers import BasicItemSerializer
|
||||
from mail.models import EventAddress
|
||||
from tickets.shared_serializers import BasicIssueSerializer
|
||||
|
||||
|
||||
class EventSerializer(serializers.ModelSerializer):
|
||||
addresses = SlugRelatedField(many=True, slug_field='address', queryset=EventAddress.objects.all())
|
||||
|
||||
class Meta:
|
||||
model = Event
|
||||
fields = ['id', 'slug', 'name', 'start', 'end', 'pre_start', 'post_end', 'addresses']
|
||||
read_only_fields = ['id']
|
||||
|
||||
def to_internal_value(self, data):
|
||||
data = data.copy()
|
||||
addresses = data.pop('addresses', None)
|
||||
dict = super().to_internal_value(data)
|
||||
if addresses:
|
||||
dict['addresses'] = [EventAddress.objects.get_or_create(address=x)[0] for x in addresses]
|
||||
return dict
|
||||
|
||||
|
||||
class ContainerSerializer(serializers.ModelSerializer):
|
||||
itemCount = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Container
|
||||
fields = ['id', 'name', 'itemCount']
|
||||
read_only_fields = ['id', 'itemCount']
|
||||
|
||||
def get_itemCount(self, instance):
|
||||
return len(instance.items)
|
||||
|
||||
|
||||
class ItemSerializer(BasicItemSerializer):
|
||||
timeline = serializers.SerializerMethodField()
|
||||
dataImage = serializers.CharField(write_only=True, required=False)
|
||||
related_issues = BasicIssueSerializer(many=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Item
|
||||
fields = ['cid', 'box', 'id', 'description', 'file', 'dataImage', 'returned', 'event', 'related_issues',
|
||||
'timeline']
|
||||
read_only_fields = ['id']
|
||||
|
||||
def to_internal_value(self, data):
|
||||
container = None
|
||||
returned = False
|
||||
if 'cid' in data:
|
||||
container = Container.objects.get(id=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):
|
||||
if not 'container' in attrs and not self.partial:
|
||||
raise serializers.ValidationError("This field cannot be empty.")
|
||||
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)
|
||||
|
||||
@staticmethod
|
||||
def get_timeline(obj):
|
||||
timeline = []
|
||||
for comment in obj.comments.all():
|
||||
timeline.append({
|
||||
'type': 'comment',
|
||||
'id': comment.id,
|
||||
'timestamp': comment.timestamp,
|
||||
'comment': comment.comment,
|
||||
})
|
||||
for relation in (obj.issue_relation_changes.all()):
|
||||
timeline.append({
|
||||
'type': 'issue_relation',
|
||||
'id': relation.id,
|
||||
'status': relation.status,
|
||||
'timestamp': relation.timestamp,
|
||||
'issue_thread': BasicIssueSerializer(relation.issue_thread).data,
|
||||
})
|
||||
return sorted(timeline, key=lambda x: x['timestamp'])
|
||||
|
||||
|
||||
class SearchResultSerializer(serializers.Serializer):
|
||||
search_score = serializers.IntegerField()
|
||||
item = ItemSerializer()
|
||||
|
||||
def to_representation(self, instance):
|
||||
return {**ItemSerializer(instance['item']).data, 'search_score': instance['search_score']}
|
||||
|
||||
class Meta:
|
||||
model = Item
|
31
core/inventory/shared_serializers.py
Normal file
31
core/inventory/shared_serializers.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from inventory.models import Event, Item
|
||||
|
||||
|
||||
class BasicItemSerializer(serializers.ModelSerializer):
|
||||
cid = serializers.SerializerMethodField()
|
||||
box = serializers.SerializerMethodField()
|
||||
file = serializers.SerializerMethodField()
|
||||
returned = serializers.SerializerMethodField(required=False)
|
||||
event = serializers.SlugRelatedField(slug_field='slug', queryset=Event.objects.all(),
|
||||
allow_null=True, required=False)
|
||||
|
||||
class Meta:
|
||||
model = Item
|
||||
fields = ['cid', 'box', 'id', 'description', 'file', 'returned', 'event']
|
||||
read_only_fields = ['id']
|
||||
|
||||
def get_cid(self, instance):
|
||||
return instance.container.id if instance.container else None
|
||||
|
||||
def get_box(self, instance):
|
||||
return instance.container.name if instance.container else None
|
||||
|
||||
def get_file(self, instance):
|
||||
if len(instance.files.all()) > 0:
|
||||
return sorted(instance.files.all(), key=lambda x: x.created_at, reverse=True)[0].hash
|
||||
return None
|
||||
|
||||
def get_returned(self, instance):
|
||||
return instance.returned_at is not None
|
0
core/inventory/tests/__init__.py
Normal file
0
core/inventory/tests/__init__.py
Normal file
0
core/inventory/tests/v2/__init__.py
Normal file
0
core/inventory/tests/v2/__init__.py
Normal file
44
core/inventory/tests/v2/test_api.py
Normal file
44
core/inventory/tests/v2/test_api.py
Normal file
|
@ -0,0 +1,44 @@
|
|||
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(), [])
|
66
core/inventory/tests/v2/test_containers.py
Normal file
66
core/inventory/tests/v2/test_containers.py
Normal file
|
@ -0,0 +1,66 @@
|
|||
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]['id'], 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()['id'], 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].id, 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.id}/', {'name': 'BOX 2'}, content_type='application/json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json()['id'], 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].id, 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.id}/')
|
||||
self.assertEqual(response.status_code, 204)
|
||||
self.assertEqual(len(Container.objects.all()), 1)
|
94
core/inventory/tests/v2/test_events.py
Normal file
94
core/inventory/tests/v2/test_events.py
Normal file
|
@ -0,0 +1,94 @@
|
|||
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.id}/', {'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_update_event(self):
|
||||
from rest_framework.test import APIClient
|
||||
event = Event.objects.create(slug='EVENT1', name='Event 1')
|
||||
response = APIClient().patch(f'/api/2/events/{event.id}/', {'addresses': ['foo@bar.baz', 'foo1@bar.baz']})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json()['slug'], 'EVENT1')
|
||||
self.assertEqual(response.json()['name'], 'Event 1')
|
||||
self.assertEqual(2, len(response.json()['addresses']))
|
||||
self.assertEqual('foo@bar.baz', response.json()['addresses'][0])
|
||||
self.assertEqual('foo1@bar.baz', response.json()['addresses'][1])
|
||||
self.assertEqual(len(Event.objects.all()), 1)
|
||||
self.assertEqual(Event.objects.all()[0].slug, 'EVENT1')
|
||||
self.assertEqual(Event.objects.all()[0].name, 'Event 1')
|
||||
|
||||
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.id}/')
|
||||
self.assertEqual(response.status_code, 204)
|
||||
self.assertEqual(len(Event.objects.all()), 1)
|
||||
|
||||
def test_event_with_address(self):
|
||||
from mail.models import EventAddress
|
||||
event1 = Event.objects.create(slug='TEST1', name='Event')
|
||||
EventAddress.objects.create(event=event1, 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']))
|
||||
|
||||
def test_items_remove_addresss(self):
|
||||
from mail.models import EventAddress
|
||||
from rest_framework.test import APIClient
|
||||
event1 = Event.objects.create(slug='TEST1', name='Event')
|
||||
EventAddress.objects.create(event=event1, address='foo@bar.baz')
|
||||
EventAddress.objects.create(event=event1, address='fo1o@bar.baz')
|
||||
response = APIClient().patch(f'/api/2/events/{event1.id}/', {'addresses': ['foo1@bar.baz']})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual('TEST1', response.json()['slug'])
|
||||
self.assertEqual('Event', response.json()['name'])
|
||||
self.assertEqual(1, len(response.json()['addresses']))
|
||||
self.assertEqual('foo1@bar.baz', response.json()['addresses'][0])
|
327
core/inventory/tests/v2/test_items.py
Normal file
327
core/inventory/tests/v2/test_items.py
Normal file
|
@ -0,0 +1,327 @@
|
|||
from datetime import datetime, timedelta
|
||||
|
||||
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, Comment
|
||||
|
||||
from base64 import b64encode
|
||||
|
||||
from tickets.models import IssueThread, ItemRelation
|
||||
|
||||
|
||||
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]})
|
||||
self.issue = IssueThread.objects.create(
|
||||
name="test issue",
|
||||
event=self.event,
|
||||
)
|
||||
|
||||
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_and_timeline(self):
|
||||
now = datetime.now()
|
||||
item = Item.objects.create(container=self.box, event=self.event, description='1')
|
||||
comment = Comment.objects.create(
|
||||
item=item,
|
||||
comment="test",
|
||||
timestamp=now + timedelta(seconds=3),
|
||||
)
|
||||
match = ItemRelation.objects.create(
|
||||
issue_thread=self.issue,
|
||||
item = item,
|
||||
timestamp=now + timedelta(seconds=5),
|
||||
)
|
||||
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]['id'], item.id)
|
||||
self.assertEqual(response.json()[0]['description'], '1')
|
||||
self.assertEqual(response.json()[0]['box'], 'BOX')
|
||||
self.assertEqual(response.json()[0]['cid'], self.box.id)
|
||||
self.assertEqual(response.json()[0]['file'], None)
|
||||
self.assertEqual(response.json()[0]['returned'], False)
|
||||
self.assertEqual(response.json()[0]['event'], self.event.slug)
|
||||
self.assertEqual(len(response.json()[0]['timeline']), 2)
|
||||
self.assertEqual(response.json()[0]['timeline'][0]['type'], 'comment')
|
||||
self.assertEqual(response.json()[0]['timeline'][1]['type'], 'issue_relation')
|
||||
self.assertEqual(response.json()[0]['timeline'][0]['id'], comment.id)
|
||||
self.assertEqual(response.json()[0]['timeline'][1]['id'], match.id)
|
||||
self.assertEqual(response.json()[0]['timeline'][0]['comment'], 'test')
|
||||
self.assertEqual(response.json()[0]['timeline'][0]['timestamp'],
|
||||
comment.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'))
|
||||
self.assertEqual(response.json()[0]['timeline'][1]['status'], 'possible')
|
||||
self.assertEqual(response.json()[0]['timeline'][1]['timestamp'],
|
||||
match.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'))
|
||||
self.assertEqual(response.json()[0]['timeline'][1]['issue_thread']['name'], "test issue")
|
||||
self.assertEqual(response.json()[0]['timeline'][1]['issue_thread']['event'], "EVENT")
|
||||
self.assertEqual(response.json()[0]['timeline'][1]['issue_thread']['state'], "pending_new")
|
||||
self.assertEqual(len(response.json()[0]['related_issues']), 1)
|
||||
self.assertEqual(response.json()[0]['related_issues'][0]['name'], "test issue")
|
||||
self.assertEqual(response.json()[0]['related_issues'][0]['event'], "EVENT")
|
||||
self.assertEqual(response.json()[0]['related_issues'][0]['state'], "pending_new")
|
||||
|
||||
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(len(response.json()), 1)
|
||||
self.assertEqual(response.json()[0]['id'], item.id)
|
||||
self.assertEqual(response.json()[0]['description'], '1')
|
||||
self.assertEqual(response.json()[0]['box'], 'BOX')
|
||||
self.assertEqual(response.json()[0]['cid'], self.box.id)
|
||||
self.assertEqual(response.json()[0]['file'], file.hash)
|
||||
self.assertEqual(response.json()[0]['returned'], False)
|
||||
self.assertEqual(response.json()[0]['event'], self.event.slug)
|
||||
self.assertEqual(len(response.json()[0]['related_issues']), 0)
|
||||
|
||||
def test_members_with_two_file(self):
|
||||
import base64
|
||||
item = Item.objects.create(container=self.box, event=self.event, description='1')
|
||||
file1 = File.objects.create(item=item, data="data:text/plain;base64," + base64.b64encode(b"foo").decode('utf-8'))
|
||||
file2 = File.objects.create(item=item, data="data:text/plain;base64," + base64.b64encode(b"bar").decode('utf-8'))
|
||||
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]['id'], item.id)
|
||||
self.assertEqual(response.json()[0]['description'], '1')
|
||||
self.assertEqual(response.json()[0]['box'], 'BOX')
|
||||
self.assertEqual(response.json()[0]['cid'], self.box.id)
|
||||
self.assertEqual(response.json()[0]['file'], file2.hash)
|
||||
self.assertEqual(response.json()[0]['returned'], False)
|
||||
self.assertEqual(response.json()[0]['event'], self.event.slug)
|
||||
self.assertEqual(len(response.json()[0]['related_issues']), 0)
|
||||
|
||||
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.id, 'description': '1'})
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertEqual(response.json()['id'], 1)
|
||||
self.assertEqual(response.json()['description'], '1')
|
||||
self.assertEqual(response.json()['box'], 'BOX')
|
||||
self.assertEqual(response.json()['cid'], self.box.id)
|
||||
self.assertEqual(response.json()['file'], None)
|
||||
self.assertEqual(response.json()['returned'], False)
|
||||
self.assertEqual(response.json()['event'], self.event.slug)
|
||||
self.assertEqual(len(response.json()['related_issues']), 0)
|
||||
self.assertEqual(len(Item.objects.all()), 1)
|
||||
self.assertEqual(Item.objects.all()[0].id, 1)
|
||||
self.assertEqual(Item.objects.all()[0].description, '1')
|
||||
self.assertEqual(Item.objects.all()[0].container.id, self.box.id)
|
||||
|
||||
def test_create_item_without_container(self):
|
||||
response = self.client.post(f'/api/2/{self.event.slug}/item/', {'description': '1'})
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_create_item_without_description(self):
|
||||
response = self.client.post(f'/api/2/{self.event.slug}/item/', {'cid': self.box.id})
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_create_item_with_file(self):
|
||||
import base64
|
||||
response = self.client.post(f'/api/2/{self.event.slug}/item/',
|
||||
{'cid': self.box.id, '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()['id'], 1)
|
||||
self.assertEqual(response.json()['description'], '1')
|
||||
self.assertEqual(response.json()['box'], 'BOX')
|
||||
self.assertEqual(response.json()['id'], self.box.id)
|
||||
self.assertEqual(len(response.json()['file']), 64)
|
||||
self.assertEqual(len(Item.objects.all()), 1)
|
||||
self.assertEqual(Item.objects.all()[0].id, 1)
|
||||
self.assertEqual(Item.objects.all()[0].description, '1')
|
||||
self.assertEqual(Item.objects.all()[0].container.id, self.box.id)
|
||||
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.patch(f'/api/2/{self.event.slug}/item/{item.id}/', {'description': '2'},
|
||||
content_type='application/json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json()['id'], item.id)
|
||||
self.assertEqual(response.json()['description'], '2')
|
||||
self.assertEqual(response.json()['box'], 'BOX')
|
||||
self.assertEqual(response.json()['cid'], self.box.id)
|
||||
self.assertEqual(response.json()['file'], None)
|
||||
self.assertEqual(response.json()['returned'], False)
|
||||
self.assertEqual(response.json()['event'], self.event.slug)
|
||||
self.assertEqual(len(response.json()['related_issues']), 0)
|
||||
self.assertEqual(len(Item.objects.all()), 1)
|
||||
self.assertEqual(Item.objects.all()[0].id, 1)
|
||||
self.assertEqual(Item.objects.all()[0].description, '2')
|
||||
self.assertEqual(Item.objects.all()[0].container.id, self.box.id)
|
||||
|
||||
def test_update_item_with_file(self):
|
||||
import base64
|
||||
item = Item.objects.create(container=self.box, event=self.event, description='1')
|
||||
response = self.client.patch(f'/api/2/{self.event.slug}/item/{item.id}/',
|
||||
{'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()['id'], 1)
|
||||
self.assertEqual(response.json()['description'], '2')
|
||||
self.assertEqual(response.json()['box'], 'BOX')
|
||||
self.assertEqual(response.json()['id'], self.box.id)
|
||||
self.assertEqual(len(response.json()['file']), 64)
|
||||
self.assertEqual(len(Item.objects.all()), 1)
|
||||
self.assertEqual(Item.objects.all()[0].id, 1)
|
||||
self.assertEqual(Item.objects.all()[0].description, '2')
|
||||
self.assertEqual(Item.objects.all()[0].container.id, self.box.id)
|
||||
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.id}/')
|
||||
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.id}/')
|
||||
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.id, 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.id}/', {'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]['id'], item1.id)
|
||||
|
||||
|
||||
class ItemSearchTestCase(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]})
|
||||
self.item1 = Item.objects.create(container=self.box, event=self.event, description='abc def')
|
||||
self.item2 = Item.objects.create(container=self.box, event=self.event, description='def ghi')
|
||||
self.item3 = Item.objects.create(container=self.box, event=self.event, description='jkl mno pqr')
|
||||
self.item4 = Item.objects.create(container=self.box, event=self.event, description='stu vwx')
|
||||
|
||||
def test_search(self):
|
||||
search_query = b64encode(b'abc').decode('utf-8')
|
||||
response = self.client.get(f'/api/2/{self.event.slug}/items/{search_query}/')
|
||||
self.assertEqual(200, response.status_code)
|
||||
self.assertEqual(1, len(response.json()))
|
||||
self.assertEqual(self.item1.id, response.json()[0]['id'])
|
||||
self.assertEqual('abc def', response.json()[0]['description'])
|
||||
self.assertEqual('BOX', response.json()[0]['box'])
|
||||
self.assertEqual(self.box.id, response.json()[0]['cid'])
|
||||
self.assertEqual(1, response.json()[0]['search_score'])
|
||||
|
||||
def test_search2(self):
|
||||
search_query = b64encode(b'def').decode('utf-8')
|
||||
response = self.client.get(f'/api/2/{self.event.slug}/items/{search_query}/')
|
||||
self.assertEqual(200, response.status_code)
|
||||
self.assertEqual(2, len(response.json()))
|
||||
self.assertEqual(self.item1.id, response.json()[0]['id'])
|
||||
self.assertEqual('abc def', response.json()[0]['description'])
|
||||
self.assertEqual('BOX', response.json()[0]['box'])
|
||||
self.assertEqual(self.box.id, response.json()[0]['cid'])
|
||||
self.assertEqual(1, response.json()[0]['search_score'])
|
||||
self.assertEqual(self.item2.id, response.json()[1]['id'])
|
||||
self.assertEqual('def ghi', response.json()[1]['description'])
|
||||
self.assertEqual('BOX', response.json()[1]['box'])
|
||||
self.assertEqual(self.box.id, response.json()[1]['cid'])
|
||||
self.assertEqual(1, response.json()[0]['search_score'])
|
||||
|
||||
def test_search3(self):
|
||||
search_query = b64encode(b'jkl').decode('utf-8')
|
||||
response = self.client.get(f'/api/2/{self.event.slug}/items/{search_query}/')
|
||||
self.assertEqual(200, response.status_code)
|
||||
self.assertEqual(1, len(response.json()))
|
||||
self.assertEqual(self.item3.id, response.json()[0]['id'])
|
||||
self.assertEqual('jkl mno pqr', response.json()[0]['description'])
|
||||
self.assertEqual('BOX', response.json()[0]['box'])
|
||||
self.assertEqual(self.box.id, response.json()[0]['cid'])
|
||||
self.assertEqual(1, response.json()[0]['search_score'])
|
||||
|
||||
def test_search4(self):
|
||||
search_query = b64encode(b'abc def').decode('utf-8')
|
||||
response = self.client.get(f'/api/2/{self.event.slug}/items/{search_query}/')
|
||||
self.assertEqual(200, response.status_code)
|
||||
self.assertEqual(2, len(response.json()))
|
||||
self.assertEqual(self.item1.id, response.json()[0]['id'])
|
||||
self.assertEqual('abc def', response.json()[0]['description'])
|
||||
self.assertEqual('BOX', response.json()[0]['box'])
|
||||
self.assertEqual(self.box.id, response.json()[0]['cid'])
|
||||
self.assertEqual(2, response.json()[0]['search_score'])
|
||||
self.assertEqual(self.item2.id, response.json()[1]['id'])
|
||||
self.assertEqual('def ghi', response.json()[1]['description'])
|
||||
self.assertEqual('BOX', response.json()[1]['box'])
|
||||
self.assertEqual(self.box.id, response.json()[1]['cid'])
|
||||
self.assertEqual(1, response.json()[1]['search_score'])
|
||||
|
0
core/mail/__init__.py
Normal file
0
core/mail/__init__.py
Normal file
17
core/mail/admin.py
Normal file
17
core/mail/admin.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from mail.models import Email, EventAddress
|
||||
|
||||
|
||||
class EmailAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
|
||||
|
||||
admin.site.register(Email, EmailAdmin)
|
||||
|
||||
|
||||
class EventAddressAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
|
||||
|
||||
admin.site.register(EventAddress, EventAddressAdmin)
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue