Compare commits

...

No commits in common. "live" and "system2/backend" have entirely different histories.

237 changed files with 2491 additions and 10515 deletions

15
.editorconfig Normal file
View file

@ -0,0 +1,15 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.yml]
indent_size = 2

19
.env.example Normal file
View file

@ -0,0 +1,19 @@
APP_NAME="Lost & Found Backend"
APP_ENV=local
APP_KEY=
APP_DEBUG=false
APP_URL=http://localhost
APP_TIMEZONE=Europe/Berlin
LOG_CHANNEL=stack
LOG_SLACK_WEBHOOK_URL=
DB_CONNECTION=mysql
DB_HOST=dbserver
DB_PORT=3306
DB_DATABASE=lostfound
DB_USERNAME=lostfound
DB_PASSWORD=lostfound
CACHE_DRIVER=file
QUEUE_CONNECTION=sync

15
.gitignore vendored
View file

@ -1,8 +1,15 @@
/vendor
/.idea
Homestead.json
Homestead.yaml
.env
.local
/public/docs
/public/staticimages
/public/thumbnails
/resources/docs
staticfiles/
userfiles/
*.db
composer.lock
composer.phar
.phpunit.result.cache

9
.styleci.yml Normal file
View file

@ -0,0 +1,9 @@
php:
preset: laravel
enabled:
- alpha_ordered_imports
disabled:
- length_ordered_imports
- unused_use
js: true
css: true

29
app/Console/Kernel.php Normal file
View file

@ -0,0 +1,29 @@
<?php
namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Laravel\Lumen\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
/**
* The Artisan commands provided by your application.
*
* @var array
*/
protected $commands = [
//
];
/**
* Define the application's command schedule.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
protected function schedule(Schedule $schedule)
{
//
}
}

42
app/Container.php Normal file
View file

@ -0,0 +1,42 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\DB;
class Container extends Model
{
use SoftDeletes;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'cid', 'name'
];
protected $primaryKey = 'cid';
/**
* The attributes excluded from the model's JSON form.
*
* @var array
*/
protected $hidden = ['created_at', 'deleted_at', 'updated_at'];
static function all($columns=Array()){
return Container::leftJoin('items','items.cid','=','containers.cid')
->select('containers.cid', 'name', DB::raw('count(items.iid) as itemCount'))
->groupBy('containers.cid', 'name')->get();
}
static function find($id){
return Container::leftJoin('items','items.cid','=','containers.cid')
->select('containers.cid', 'name', DB::raw('count(items.iid) as itemCount'))
->groupBy('containers.cid', 'name')->where(Container::primaryKey, '=', $id)->first();
}
}

27
app/Event.php Normal file
View file

@ -0,0 +1,27 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Event extends Model
{
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'eid', 'name', 'slug', 'start', 'end', 'pre_start', 'post_end'
];
protected $primaryKey = 'eid';
/**
* The attributes excluded from the model's JSON form.
*
* @var array
*/
protected $hidden = ['created_at','updated_at'];
}

10
app/Events/Event.php Normal file
View file

@ -0,0 +1,10 @@
<?php
namespace App\Events;
use Illuminate\Queue\SerializesModels;
abstract class Event
{
use SerializesModels;
}

View file

@ -0,0 +1,16 @@
<?php
namespace App\Events;
class ExampleEvent extends Event
{
/**
* Create a new event instance.
*
* @return void
*/
public function __construct()
{
//
}
}

View file

@ -0,0 +1,50 @@
<?php
namespace App\Exceptions;
use Exception;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Validation\ValidationException;
use Laravel\Lumen\Exceptions\Handler as ExceptionHandler;
use Symfony\Component\HttpKernel\Exception\HttpException;
class Handler extends ExceptionHandler
{
/**
* A list of the exception types that should not be reported.
*
* @var array
*/
protected $dontReport = [
AuthorizationException::class,
HttpException::class,
ModelNotFoundException::class,
ValidationException::class,
];
/**
* Report or log an exception.
*
* This is a great spot to send exceptions to Sentry, Bugsnag, etc.
*
* @param \Exception $exception
* @return void
*/
public function report(Exception $exception)
{
parent::report($exception);
}
/**
* Render an exception into an HTTP response.
*
* @param \Illuminate\Http\Request $request
* @param \Exception $exception
* @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse
*/
public function render($request, Exception $exception)
{
return parent::render($request, $exception);
}
}

53
app/File.php Normal file
View file

@ -0,0 +1,53 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use TheSeer\Tokenizer\Exception;
class File extends Model
{
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'hash', 'iid'
];
protected $primaryKey = 'hash';
public $incrementing = false;
protected $keyType = 'string';
/**
* The attributes excluded from the model's JSON form.
*
* @var array
*/
protected $hidden = ['created_at','updated_at'];
public static function create(array $attributes = [])
{
if (!isset($attributes['data'])) {
throw new Exception("foo" );
}
$pos = strpos($attributes['data'], ",");
$image = base64_decode(substr($attributes['data'], $pos + 1), true);
if (!$image) {
throw new Exception("foo" );
}
$hash = md5(time());
if (!file_exists('staticimages'))
mkdir('staticimages', 0755, true);
file_put_contents('staticimages/' . $hash, $image);
$attributes['hash'] = $hash;
return static::query()->create($attributes);
}
}

View file

@ -0,0 +1,46 @@
<?php
namespace App\Http\Controllers;
use App\Container;
use Illuminate\Http\Request;
/**
* @group Container management
*
* APIs for creating, deleting, updating and viewing containers
*/
class ContainerController extends Controller
{
public function showAllContainers()
{
return response()->json(Container::all());
}
public function showOneContainer($id)
{
return response()->json(Container::find($id));
}
public function create(Request $request)
{
$container = Container::create($request->all());
return response()->json($container, 201);
}
public function update($id, Request $request)
{
$container = Container::findOrFail($id);
$container->update($request->all());
return response()->json($container, 200);
}
public function delete($id)
{
Container::findOrFail($id)->delete();
return response('Deleted Successfully', 200);
}
}

View file

@ -0,0 +1,10 @@
<?php
namespace App\Http\Controllers;
use Laravel\Lumen\Routing\Controller as BaseController;
class Controller extends BaseController
{
//
}

View file

@ -0,0 +1,41 @@
<?php
namespace App\Http\Controllers;
use App\Event;
use Illuminate\Http\Request;
class EventController extends Controller
{
public function showAllEvents()
{
return response()->json(Event::all());
}
public function showOneEvent($id)
{
return response()->json(Event::find($id));
}
public function create(Request $request)
{
$event = Event::create($request->all());
return response()->json($event, 201);
}
public function update($id, Request $request)
{
$event = Event::findOrFail($id);
$event->update($request->all());
return response()->json($event, 200);
}
public function delete($id)
{
Event::findOrFail($id)->delete();
return response('Deleted Successfully', 200);
}
}

View file

@ -0,0 +1,32 @@
<?php
namespace App\Http\Controllers;
use App\File;
use Illuminate\Http\Request;
class FileController extends Controller
{
public function showAllFiles()
{
return response()->json(File::all());
}
public function showOneFile($id)
{
return response()->json(File::find($id));
}
public function create(Request $request)
{
$file = File::create($request->only(['data','iid']));
return response()->json($file, 201);
}
public function delete($id)
{
File::findOrFail($id)->delete();
return response('Deleted Successfully', 200);
}
}

View file

@ -0,0 +1,83 @@
<?php
namespace App\Http\Controllers;
use App\Container;
use App\File;
use App\Item;
use App\Event;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class ItemController extends Controller
{
public function showAllItems()
{
return response()->json(Item::all());
}
public function showByEvent($event)
{
$eid = Event::where('slug','=',$event)->first()->eid;
$q = Item::byEvent($eid);
return response()->json($q->get());
}
public function searchByEvent($event, $query)
{
$eid = Event::where('slug','=',$event)->first()->eid;
$query_tokens = explode(" ",base64_decode ( $query , true));
$q = Item::byEvent($eid);
foreach ($query_tokens as $token)
if(!empty($token))
$q = $q->where('items.description','like','%'.$token.'%');
return response()->json($q->get());
}
public function showOneItem($event, $id)
{
$eid = Event::where('slug','=',$event)->first()->eid;
return response()->json(Item::byEvent($eid)->where('uid', '=', $id)->first());
}
public function create($event, Request $request)
{
$eid = Event::where('slug','=',$event)->first()->eid;
$newitem = $request->except(['dataImage']);
$newitem['eid'] = "".$eid;
$item = Item::create($newitem);
if($request->get('dataImage')) {
$file = File::create(array('data' => $request->get('dataImage'), 'iid' => $item['iid']));
$item['file'] = $file['hash'];
}
return response()->json($item, 201);
}
public function update($event, $id, Request $request)
{
$eid = Event::where('slug', $event)->first()->eid;
$item = Item::where('eid', $eid)->where('uid', $id)->first();
$item->update($request->except(['file', 'box', 'dataImage']));
if($request->get('returned')===true){
$item->update(['returned_at' => DB::raw( 'current_timestamp' )]);
}
if($request->get('dataImage')) {
$file = File::create(array('data' => $request->get('dataImage'), 'iid' => $item['iid']));
$item['file'] = $file['hash'];
}
return response()->json(Item::find($item['uid']), 200);
}
public function delete($event, $id)
{
$eid = Event::where('slug','=',$event)->first()->eid;
Item::where('eid', $eid)->where('uid', $id)->first()->delete();
return response()->json(array("status"=>'Deleted Successfully'), 200);
}
}

View file

@ -0,0 +1,44 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Contracts\Auth\Factory as Auth;
class Authenticate
{
/**
* The authentication guard factory instance.
*
* @var \Illuminate\Contracts\Auth\Factory
*/
protected $auth;
/**
* Create a new middleware instance.
*
* @param \Illuminate\Contracts\Auth\Factory $auth
* @return void
*/
public function __construct(Auth $auth)
{
$this->auth = $auth;
}
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|null $guard
* @return mixed
*/
public function handle($request, Closure $next, $guard = null)
{
if ($this->auth->guard($guard)->guest()) {
return response('Unauthorized.', 401);
}
return $next($request);
}
}

View file

@ -0,0 +1,20 @@
<?php
namespace App\Http\Middleware;
use Closure;
class ExampleMiddleware
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
return $next($request);
}
}

65
app/Item.php Normal file
View file

@ -0,0 +1,65 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Item extends Model
{
use SoftDeletes;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'iid', 'uid', 'description', 'eid', 'cid', 'returned_at'
];
protected $primaryKey = 'iid';
/**
* The attributes excluded from the model's JSON form.
*
* @var array
*/
protected $hidden = ['created_at','updated_at', 'deleted_at', 'returned_at', 'eid', 'iid'];
public static function restored($callback)
{
}
public static function create(array $attributes = [])
{
$uid = static::query()->withTrashed()->where('eid',$attributes['eid'])->max('uid') + 1;
$attributes['uid'] = $uid;
$item = static::query()->create($attributes);
return Item::find($item->iid);
}
protected static function extended($columns=Array()){
return Item::whereNull('returned_at')
->join('containers','items.cid','=','containers.cid')
->leftJoin('currentfiles','items.iid','=','currentfiles.iid');
}
static function byEvent($eid){
return Item::extended()->where('eid','=',$eid)
->select('items.*','currentfiles.hash as file', 'containers.name as box');
}
static function all($columns=Array()){
return Item::extended($columns)
->select('items.*','currentfiles.hash as file', 'containers.name as box')
->get();
}
static function find($id){
return Item::extended()
->select('items.*','currentfiles.hash as file', 'containers.name as box')
->where('items.iid', '=', $id)->first();
}
}

26
app/Jobs/ExampleJob.php Normal file
View file

@ -0,0 +1,26 @@
<?php
namespace App\Jobs;
class ExampleJob extends Job
{
/**
* Create a new job instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
//
}
}

24
app/Jobs/Job.php Normal file
View file

@ -0,0 +1,24 @@
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
abstract class Job implements ShouldQueue
{
/*
|--------------------------------------------------------------------------
| Queueable Jobs
|--------------------------------------------------------------------------
|
| This job base class provides a central location to place any logic that
| is shared across all of your jobs. The trait included with the class
| provides access to the "queueOn" and "delay" queue helper methods.
|
*/
use InteractsWithQueue, Queueable, SerializesModels;
}

View file

@ -0,0 +1,31 @@
<?php
namespace App\Listeners;
use App\Events\ExampleEvent;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class ExampleListener
{
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* @param ExampleEvent $event
* @return void
*/
public function handle(ExampleEvent $event)
{
//
}
}

View file

@ -0,0 +1,25 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Schema;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
//
}
public function boot()
{
Schema::defaultStringLength(191);
}
}

View file

@ -0,0 +1,39 @@
<?php
namespace App\Providers;
use App\User;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\ServiceProvider;
class AuthServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
//
}
/**
* Boot the authentication services for the application.
*
* @return void
*/
public function boot()
{
// Here you may define how you wish users to be authenticated for your Lumen
// application. The callback which receives the incoming request instance
// should return either a User instance or null. You're free to obtain
// the User instance via an API token or any other method necessary.
$this->app['auth']->viaRequest('api', function ($request) {
if ($request->input('api_token')) {
return User::where('api_token', $request->input('api_token'))->first();
}
});
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace App\Providers;
use Laravel\Lumen\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
{
/**
* The event listener mappings for the application.
*
* @var array
*/
protected $listen = [
'App\Events\ExampleEvent' => [
'App\Listeners\ExampleListener',
],
];
}

32
app/User.php Normal file
View file

@ -0,0 +1,32 @@
<?php
namespace App;
use Illuminate\Auth\Authenticatable;
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Database\Eloquent\Model;
use Laravel\Lumen\Auth\Authorizable;
class User extends Model implements AuthenticatableContract, AuthorizableContract
{
use Authenticatable, Authorizable;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name', 'email',
];
/**
* The attributes excluded from the model's JSON form.
*
* @var array
*/
protected $hidden = [
'password',
];
}

35
artisan Executable file
View file

@ -0,0 +1,35 @@
#!/usr/bin/env php
<?php
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Output\ConsoleOutput;
/*
|--------------------------------------------------------------------------
| Create The Application
|--------------------------------------------------------------------------
|
| First we need to get an application instance. This creates an instance
| of the application / container and bootstraps the application so it
| is ready to receive HTTP / Console requests from the environment.
|
*/
$app = require __DIR__.'/bootstrap/app.php';
/*
|--------------------------------------------------------------------------
| Run The Artisan Application
|--------------------------------------------------------------------------
|
| When we run the console application, the current CLI command will be
| executed in this console and the response sent back to a terminal
| or another output device for the developers. Here goes nothing!
|
*/
$kernel = $app->make(
'Illuminate\Contracts\Console\Kernel'
);
exit($kernel->handle(new ArgvInput, new ConsoleOutput));

33
backup.sh Normal file
View file

@ -0,0 +1,33 @@
#!/bin/bash
#backup.sh /var/www/lfbackend OPTION
#OPTION
# - F FULL BACKUP
# - T Only save images from today
# - I Incremental Backup (Not implemented yet)
# (OPTIONS T and I only apply for images)
#CRON
#17 * * * * root cd /tmp && /var/www/lfbackend/backup.sh /var/www/lfbackend T
#45 5 * * * root cd /tmp && /var/www/lfbackend/backup.sh /var/www/lfbackend F
OPTION=$2
source $1/.env
TS=`date +%Y%m%d%H%M%S`
mysqldump -u $DB_USERNAME -p$DB_PASSWORD -h $DB_HOST $DB_DATABASE > database.sql
if [ "$OPTION" == "T" ]
then
tar -N "`date +%Y-%m-%d`" -zcvf images.tar.gz -C $1/public/staticimages/ .
elif [ "$OPTION" == "I" ]
then
tar -zcvf images.tar.gz -C $1/public/staticimages/ .
else
tar -zcvf images.tar.gz -C $1/public/staticimages/ .
fi
gzip -f database.sql
tar -cvf backup_${TS}_${OPTION}.tar database.sql.gz images.tar.gz

102
bootstrap/app.php Normal file
View file

@ -0,0 +1,102 @@
<?php
require_once __DIR__.'/../vendor/autoload.php';
(new Laravel\Lumen\Bootstrap\LoadEnvironmentVariables(
dirname(__DIR__)
))->bootstrap();
/*
|--------------------------------------------------------------------------
| Create The Application
|--------------------------------------------------------------------------
|
| Here we will load the environment and create the application instance
| that serves as the central piece of this framework. We'll use this
| application as an "IoC" container and router for this framework.
|
*/
$app = new Laravel\Lumen\Application(
dirname(__DIR__)
);
$app->withFacades();
$app->withEloquent();
/*
|--------------------------------------------------------------------------
| Register Container Bindings
|--------------------------------------------------------------------------
|
| Now we will register a few bindings in the service container. We will
| register the exception handler and the console kernel. You may add
| your own bindings here if you like or you can make another file.
|
*/
$app->singleton(
Illuminate\Contracts\Debug\ExceptionHandler::class,
App\Exceptions\Handler::class
);
$app->singleton(
Illuminate\Contracts\Console\Kernel::class,
App\Console\Kernel::class
);
/*
|--------------------------------------------------------------------------
| Register Middleware
|--------------------------------------------------------------------------
|
| Next, we will register the middleware with the application. These can
| be global middleware that run before and after each request into a
| route or middleware that'll be assigned to some specific routes.
|
*/
// $app->middleware([
// App\Http\Middleware\ExampleMiddleware::class
// ]);
// $app->routeMiddleware([
// 'auth' => App\Http\Middleware\Authenticate::class,
// ]);
/*
|--------------------------------------------------------------------------
| Register Service Providers
|--------------------------------------------------------------------------
|
| Here we will register all of the application's service providers which
| are used to bind services into the container. Service providers are
| totally optional, so you are not required to uncomment this line.
|
*/
// $app->register(App\Providers\AppServiceProvider::class);
// $app->register(App\Providers\AuthServiceProvider::class);
// $app->register(App\Providers\EventServiceProvider::class);
$app->register(\Mpociot\ApiDoc\ApiDocGeneratorServiceProvider::class);
$app->configure('apidoc');
/*
|--------------------------------------------------------------------------
| Load The Application Routes
|--------------------------------------------------------------------------
|
| Next we will include the routes file so that they can all be added to
| the application. This will provide all of the URLs the application
| can respond to, as well as the controllers that may handle them.
|
*/
$app->router->group([
'namespace' => 'App\Http\Controllers',
], function ($router) {
require __DIR__.'/../routes/web.php';
});
return $app;

44
composer.json Normal file
View file

@ -0,0 +1,44 @@
{
"name": "laravel/lumen",
"description": "The Laravel Lumen Framework.",
"keywords": ["framework", "laravel", "lumen"],
"license": "MIT",
"type": "project",
"require": {
"php": "^7.2",
"doctrine/dbal": "^2.10",
"laravel/lumen-framework": "^6.0",
"mpociot/laravel-apidoc-generator": "^4.0"
},
"require-dev": {
"fzaninotto/faker": "^1.4",
"phpunit/phpunit": "^8.0",
"mockery/mockery": "^1.0"
},
"autoload": {
"classmap": [
"database/seeds",
"database/factories"
],
"psr-4": {
"App\\": "app/"
}
},
"autoload-dev": {
"classmap": [
"tests/"
]
},
"scripts": {
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
]
},
"config": {
"preferred-install": "dist",
"sort-packages": true,
"optimize-autoloader": true
},
"minimum-stability": "dev",
"prefer-stable": true
}

245
config/apidoc.php Normal file
View file

@ -0,0 +1,245 @@
<?php
return [
/*
* The type of documentation output to generate.
* - "static" will generate a static HTMl page in the /public/docs folder,
* - "laravel" will generate the documentation as a Blade view,
* so you can add routing and authentication.
*/
'type' => 'static',
/*
* The router to be used (Laravel or Dingo).
*/
'router' => 'laravel',
/*
* The base URL to be used in examples and the Postman collection.
* By default, this will be the value of config('app.url').
*/
'base_url' => null,
/*
* Generate a Postman collection in addition to HTML docs.
* For 'static' docs, the collection will be generated to public/docs/collection.json.
* For 'laravel' docs, it will be generated to storage/app/apidoc/collection.json.
* The `ApiDoc::routes()` helper will add routes for both the HTML and the Postman collection.
*/
'postman' => [
/*
* Specify whether the Postman collection should be generated.
*/
'enabled' => true,
/*
* The name for the exported Postman collection. Default: config('app.name')." API"
*/
'name' => null,
/*
* The description for the exported Postman collection.
*/
'description' => null,
],
/*
* The routes for which documentation should be generated.
* Each group contains rules defining which routes should be included ('match', 'include' and 'exclude' sections)
* and rules which should be applied to them ('apply' section).
*/
'routes' => [
[
/*
* Specify conditions to determine what routes will be parsed in this group.
* A route must fulfill ALL conditions to pass.
*/
'match' => [
/*
* Match only routes whose domains match this pattern (use * as a wildcard to match any characters).
*/
'domains' => [
'*',
// 'domain1.*',
],
/*
* Match only routes whose paths match this pattern (use * as a wildcard to match any characters).
*/
'prefixes' => [
'*',
// 'users/*',
],
/*
* Match only routes registered under this version. This option is ignored for Laravel router.
* Note that wildcards are not supported.
*/
'versions' => [
'v1',
],
],
/*
* Include these routes when generating documentation,
* even if they did not match the rules above.
* Note that the route must be referenced by name here (wildcards are supported).
*/
'include' => [
// 'users.index', 'healthcheck*'
],
/*
* Exclude these routes when generating documentation,
* even if they matched the rules above.
* Note that the route must be referenced by name here (wildcards are supported).
*/
'exclude' => [
// 'users.create', 'admin.*'
],
/*
* Specify rules to be applied to all the routes in this group when generating documentation
*/
'apply' => [
/*
* Specify headers to be added to the example requests
*/
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
// 'Authorization' => 'Bearer {token}',
// 'Api-Version' => 'v2',
],
/*
* If no @response or @transformer declarations are found for the route,
* we'll try to get a sample response by attempting an API call.
* Configure the settings for the API call here.
*/
'response_calls' => [
/*
* API calls will be made only for routes in this group matching these HTTP methods (GET, POST, etc).
* List the methods here or use '*' to mean all methods. Leave empty to disable API calls.
*/
'methods' => ['GET'],
/*
* Laravel config variables which should be set for the API call.
* This is a good place to ensure that notifications, emails
* and other external services are not triggered
* during the documentation API calls
*/
'config' => [
'app.env' => 'documentation',
'app.debug' => false,
// 'service.key' => 'value',
],
/*
* Cookies which should be sent with the API call.
*/
'cookies' => [
// 'name' => 'value'
],
/*
* Query parameters which should be sent with the API call.
*/
'queryParams' => [
// 'key' => 'value',
],
/*
* Body parameters which should be sent with the API call.
*/
'bodyParams' => [
// 'key' => 'value',
],
],
],
],
],
'strategies' => [
'metadata' => [
\Mpociot\ApiDoc\Extracting\Strategies\Metadata\GetFromDocBlocks::class,
],
'urlParameters' => [
\Mpociot\ApiDoc\Extracting\Strategies\UrlParameters\GetFromUrlParamTag::class,
],
'queryParameters' => [
\Mpociot\ApiDoc\Extracting\Strategies\QueryParameters\GetFromQueryParamTag::class,
],
'headers' => [
\Mpociot\ApiDoc\Extracting\Strategies\RequestHeaders\GetFromRouteRules::class,
],
'bodyParameters' => [
\Mpociot\ApiDoc\Extracting\Strategies\BodyParameters\GetFromBodyParamTag::class,
],
'responses' => [
\Mpociot\ApiDoc\Extracting\Strategies\Responses\UseTransformerTags::class,
\Mpociot\ApiDoc\Extracting\Strategies\Responses\UseResponseTag::class,
\Mpociot\ApiDoc\Extracting\Strategies\Responses\UseResponseFileTag::class,
\Mpociot\ApiDoc\Extracting\Strategies\Responses\UseApiResourceTags::class,
\Mpociot\ApiDoc\Extracting\Strategies\Responses\ResponseCalls::class,
],
],
/*
* Custom logo path. The logo will be copied from this location
* during the generate process. Set this to false to use the default logo.
*
* Change to an absolute path to use your custom logo. For example:
* 'logo' => resource_path('views') . '/api/logo.png'
*
* If you want to use this, please be aware of the following rules:
* - the image size must be 230 x 52
*/
'logo' => false,
/*
* Name for the group of routes which do not have a @group set.
*/
'default_group' => 'general',
/*
* Example requests for each endpoint will be shown in each of these languages.
* Supported options are: bash, javascript, php, python
* You can add a language of your own, but you must publish the package's views
* and define a corresponding view for it in the partials/example-requests directory.
* See https://laravel-apidoc-generator.readthedocs.io/en/latest/generating-documentation.html
*
*/
'example_languages' => [
'bash',
'javascript',
],
/*
* Configure how responses are transformed using @transformer and @transformerCollection
* Requires league/fractal package: composer require league/fractal
*
*/
'fractal' => [
/* If you are using a custom serializer with league/fractal,
* you can specify it here.
*
* Serializers included with league/fractal:
* - \League\Fractal\Serializer\ArraySerializer::class
* - \League\Fractal\Serializer\DataArraySerializer::class
* - \League\Fractal\Serializer\JsonApiSerializer::class
*
* Leave as null to use no serializer or return a simple JSON.
*/
'serializer' => null,
],
/*
* If you would like the package to generate the same example values for parameters on each run,
* set this to any number (eg. 1234)
*
*/
'faker_seed' => null,
];

42
config/database.php Normal file
View file

@ -0,0 +1,42 @@
<?php
return [
'default' => env('DB_CONNECTION', 'sqlite'),
'connections' => [
'sqlite' => [
'driver' => 'sqlite',
'url' => env('DATABASE_URL'),
'database' => env('DB_DATABASE', database_path('default.sqlite')),
'prefix' => '',
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
],
'mysql' => [
'driver' => 'mysql',
'url' => env('DATABASE_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'default_db'),
'username' => env('DB_USERNAME', 'default_user'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
'sqlite_testing' => [
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
]
],
'fetch' => PDO::FETCH_CLASS, // Returns DB objects in an array format.
'migrations' => 'migrations'
];
?>

6
container/db/01_init.sql Normal file
View file

@ -0,0 +1,6 @@
DROP DATABASE IF EXISTS lostfound;
CREATE DATABASE lostfound;
CREATE OR REPLACE USER lostfound IDENTIFIED BY 'lostfound';
GRANT ALL privileges ON `lostfound`.* TO 'lostfound';

View file

@ -0,0 +1,9 @@
add_header Strict-Transport-Security "max-age=15768000; includeSubDomains;";
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header X-Robots-Tag none;
add_header X-Download-Options noopen;
add_header X-Permitted-Cross-Domain-Policies none;
add_header Access-Control-Allow-Origin '*';
client_max_body_size 50m;

View file

@ -0,0 +1,7 @@
rewrite '^/1/images/([0-9a-fA-F]{32})/?$' /staticimages/$1 last;
rewrite '^/1/thumbs/([0-9a-fA-F]{32})/?$' /thumbnails/$1 last;
# rewrite '^/thumbnails/([0-9a-fA-F]{32})$' /thumbnail.php?id=$1 last;
location /thumbnails/ {
try_files $uri /thumbnail.php?id=$uri;
}

8
container/web/init.sh Normal file
View file

@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -e
cd /app
echo "executing COMPOSER UPDATE"
composer update
echo "executing DATABASE MIGRATE"
php artisan migrate --force

View file

@ -0,0 +1,15 @@
location / {
if ($request_method = OPTIONS) {
add_header Content-Length 0;
add_header Content-Type text/plain;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
add_header Access-Control-Allow-Origin '*'; #$http_origin;
add_header Access-Control-Allow-Headers "Authorization, Content-Type";
add_header Access-Control-Allow-Credentials true;
return 200;
}
try_files $uri $uri/ /index.php?$query_string;
}

129
core/.gitignore vendored
View file

@ -1,129 +0,0 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/

View file

@ -1,17 +0,0 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from authentication.models import ExtendedUser
class ExtendedUserAdmin(UserAdmin):
list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff', 'is_superuser')
search_fields = ('username', 'email', 'first_name', 'last_name')
ordering = ('username',)
filter_horizontal = ('groups', 'user_permissions', 'permissions')
def permissions(self, obj):
return ', '.join(obj.get_all_permissions())
admin.site.register(ExtendedUser, ExtendedUserAdmin)

View file

@ -1,116 +0,0 @@
from rest_framework import routers, viewsets, serializers, permissions
from rest_framework.decorators import api_view, permission_classes, authentication_classes
from rest_framework.authtoken.serializers import AuthTokenSerializer
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from django.contrib.auth import login
from django.urls import path
from django.dispatch import receiver
from django.db.models.signals import post_save
from django.contrib.auth.models import Group
from knox.models import AuthToken
from knox.views import LoginView as KnoxLoginView
from authentication.models import ExtendedUser
class UserSerializer(serializers.ModelSerializer):
permissions = serializers.SerializerMethodField()
groups = serializers.SlugRelatedField(many=True, read_only=True, slug_field='name')
class Meta:
model = ExtendedUser
fields = ('id', 'username', 'email', 'first_name', 'last_name', 'permissions', 'groups')
read_only_fields = ('id', 'username', 'email', 'first_name', 'last_name', 'permissions', 'groups')
def get_permissions(self, obj):
return list(set(obj.get_permissions()))
@receiver(post_save, sender=ExtendedUser)
def create_auth_token(sender, instance=None, created=False, **kwargs):
if created:
AuthToken.objects.create(user=instance)
class UserViewSet(viewsets.ModelViewSet):
queryset = ExtendedUser.objects.all()
serializer_class = UserSerializer
class GroupSerializer(serializers.ModelSerializer):
permissions = serializers.SerializerMethodField()
members = serializers.SerializerMethodField()
class Meta:
model = Group
fields = ('id', 'name', 'permissions', 'members')
def get_permissions(self, obj):
return ["*:" + p.codename for p in obj.permissions.all()]
def get_members(self, obj):
return [u.username for u in obj.user_set.all()]
class GroupViewSet(viewsets.ModelViewSet):
queryset = Group.objects.all()
serializer_class = GroupSerializer
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def selfUser(request):
serializer = UserSerializer(request.user)
return Response(serializer.data, status=200)
@api_view(['POST'])
@permission_classes([])
@authentication_classes([])
def registerUser(request):
try:
username = request.data.get('username')
password = request.data.get('password')
email = request.data.get('email')
errors = {}
if not username:
errors['username'] = 'Username is required'
if not password:
errors['password'] = 'Password is required'
if not email:
errors['email'] = 'Email is required'
if ExtendedUser.objects.filter(email=email).exists():
errors['email'] = 'Email already exists'
if ExtendedUser.objects.filter(username=username).exists():
errors['username'] = 'Username already exists'
if errors:
return Response({'errors': errors}, status=400)
user = ExtendedUser.objects.create_user(username, email, password)
return Response({'username': user.username, 'email': user.email}, status=201)
except Exception as e:
return Response({'errors': str(e)}, status=400)
class LoginView(KnoxLoginView):
permission_classes = (permissions.AllowAny,)
authentication_classes = ()
def post(self, request, format=None):
serializer = AuthTokenSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = serializer.validated_data['user']
login(request, user)
return super(LoginView, self).post(request, format=None)
router = routers.SimpleRouter()
router.register(r'users', UserViewSet, basename='users')
router.register(r'groups', GroupViewSet, basename='groups')
urlpatterns = router.urls + [
path('self/', selfUser),
path('login/', LoginView.as_view()),
path('register/', registerUser),
]

View file

@ -1,67 +0,0 @@
# Generated by Django 4.2.7 on 2023-12-11 21:10
from django.conf import settings
import django.contrib.auth.models
import django.contrib.auth.validators
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
('inventory', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='ExtendedUser',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
],
options={
'verbose_name': 'Extended user',
'verbose_name_plural': 'Extended users',
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
migrations.CreateModel(
name='EventPermission',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.event')),
('permission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.permission')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'unique_together': {('user', 'permission', 'event')},
},
),
migrations.AddField(
model_name='extendeduser',
name='permissions',
field=models.ManyToManyField(through='authentication.EventPermission', to='auth.permission'),
),
migrations.AddField(
model_name='extendeduser',
name='user_permissions',
field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions'),
),
]

View file

@ -1,45 +0,0 @@
# Generated by Django 4.2.7 on 2023-12-11 21:11
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('inventory', '0001_initial'),
('knox', '0008_remove_authtoken_salt'),
('authentication', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='AuthTokenEventPermissions',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.event')),
('permission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='authentication.eventpermission')),
],
),
migrations.CreateModel(
name='ExtendedAuthToken',
fields=[
('authtoken_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='knox.authtoken')),
('permissions', models.ManyToManyField(through='authentication.AuthTokenEventPermissions', to='authentication.eventpermission')),
],
options={
'verbose_name': 'Extended auth token',
'verbose_name_plural': 'Extended auth tokens',
},
bases=('knox.authtoken',),
),
migrations.AddField(
model_name='authtokeneventpermissions',
name='token',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='authentication.extendedauthtoken'),
),
migrations.AlterUniqueTogether(
name='authtokeneventpermissions',
unique_together={('token', 'permission', 'event')},
),
]

View file

@ -1,33 +0,0 @@
# Generated by Django 4.2.7 on 2023-11-26 00:16
from django.conf import settings
from django.db import migrations
from django.contrib.auth.models import Permission, Group
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('authentication', '0002_authtokeneventpermissions_extendedauthtoken_and_more'),
('tickets', '0001_initial'),
]
def create_groups(apps, schema_editor):
admins = Group.objects.create(name='Admin')
orga = Group.objects.create(name='Orga')
team = Group.objects.create(name='Team')
users = Group.objects.create(name='User')
admins.permissions.add(*Permission.objects.all())
users.permissions.add(*Permission.objects.filter(codename__in=
['view_item', 'add_item', 'change_item', 'match_item']))
team.permissions.add(*Permission.objects.filter(codename__in=
['delete_item', 'view_issuethread', 'add_issuethread',
'change_issuethread', 'delete_issuethread', 'send_mail']),
*users.permissions.all())
orga.permissions.add(*Permission.objects.filter(codename__in=['add_event']),
*team.permissions.all())
operations = [
migrations.RunPython(create_groups),
]

View file

@ -1,19 +0,0 @@
from django.conf import settings
from django.db import migrations
from authentication.models import ExtendedUser
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('authentication', '0003_groups'),
]
def create_legacy_user(apps, schema_editor):
ExtendedUser.objects.create_user(settings.LEGACY_USER_NAME, 'mail@' + settings.MAIL_DOMAIN,
settings.LEGACY_USER_PASSWORD)
operations = [
migrations.RunPython(create_legacy_user)
]

View file

@ -1,26 +0,0 @@
# Generated by Django 4.2.7 on 2023-12-13 16:28
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('inventory', '0001_initial'),
('authentication', '0004_legacy_user'),
]
operations = [
migrations.AlterField(
model_name='eventpermission',
name='event',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='inventory.event'),
),
migrations.AlterField(
model_name='eventpermission',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='event_permissions', to=settings.AUTH_USER_MODEL),
),
]

View file

@ -1,62 +0,0 @@
from django.db import models
from django.contrib.auth.models import Permission, AbstractUser
from knox.models import AuthToken
from inventory.models import Event
class ExtendedUser(AbstractUser):
permissions = models.ManyToManyField(Permission, through='EventPermission', through_fields=('user', 'permission'))
class Meta:
verbose_name = 'Extended user'
verbose_name_plural = 'Extended users'
def get_permissions(self):
if self.is_superuser:
for permission in Permission.objects.all():
yield "*:" + permission.codename
for permission in self.user_permissions.all():
yield "*:" + permission.codename
for group in self.groups.all():
for permission in group.permissions.all():
yield "*:" + permission.codename
for permission in self.event_permissions.all():
yield permission.event.slug + ":" + permission.permission.codename
def has_event_perm(self, event, permission):
if self.is_superuser:
return True
permissions = set(self.get_permissions())
if "*:" + permission in permissions:
return True
if event.slug + ":" + permission in permissions:
return True
return False
class ExtendedAuthToken(AuthToken):
permissions = models.ManyToManyField('EventPermission', through='AuthTokenEventPermissions',
through_fields=('token', 'permission'))
class Meta:
verbose_name = 'Extended auth token'
verbose_name_plural = 'Extended auth tokens'
class EventPermission(models.Model):
user = models.ForeignKey(ExtendedUser, on_delete=models.CASCADE, related_name='event_permissions')
permission = models.ForeignKey(Permission, on_delete=models.CASCADE)
event = models.ForeignKey(Event, on_delete=models.CASCADE, null=True, blank=True)
class Meta:
unique_together = ('user', 'permission', 'event')
class AuthTokenEventPermissions(models.Model):
token = models.ForeignKey(ExtendedAuthToken, on_delete=models.CASCADE)
permission = models.ForeignKey(EventPermission, on_delete=models.CASCADE)
event = models.ForeignKey(Event, on_delete=models.CASCADE)
class Meta:
unique_together = ('token', 'permission', 'event')

View file

@ -1,90 +0,0 @@
from django.test import TestCase, Client
from django.contrib.auth.models import Permission
from knox.models import AuthToken
from authentication.models import EventPermission, ExtendedUser
from inventory.models import Event
class PermissionsTestCase(TestCase):
def setUp(self):
super().setUp()
self.user = ExtendedUser.objects.create_user('testuser', 'test', 'test')
self.user.user_permissions.add(*Permission.objects.all())
event1 = Event.objects.create(slug='testevent1', name='testevent1')
event2 = Event.objects.create(slug='testevent2', name='testevent2')
permission1 = Permission.objects.get(codename='view_event')
EventPermission.objects.create(user=self.user, permission=permission1, event=event1)
EventPermission.objects.create(user=self.user, permission=permission1, event=event2)
self.token = AuthToken.objects.create(user=self.user)
self.client = Client(headers={'Authorization': 'Token ' + self.token[1]})
self.newuser = ExtendedUser.objects.create_user('newuser', 'test', 'test')
self.newuser_token = AuthToken.objects.create(user=self.newuser)
self.newuser_client = Client(headers={'Authorization': 'Token ' + self.newuser_token[1]})
def test_user_permissions(self):
"""
Test that a user can only access their own data.
"""
response = self.client.get('/api/2/users/')
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 3)
self.assertEqual(response.json()[0]['username'], 'legacy_user')
self.assertEqual(response.json()[0]['email'], 'mail@localhost')
self.assertEqual(response.json()[0]['first_name'], '')
self.assertEqual(response.json()[0]['last_name'], '')
self.assertEqual(response.json()[0]['id'], 1)
self.assertEqual(response.json()[1]['username'], 'testuser')
self.assertEqual(response.json()[1]['email'], 'test')
self.assertEqual(response.json()[1]['first_name'], '')
self.assertEqual(response.json()[1]['last_name'], '')
def test_user_permission(self):
"""
Test that a user can only access their own data.
"""
#ä['add_logentry', 'change_logentry', 'delete_logentry', 'view_logentry', 'add_group', 'change_group',
#ä 'delete_group', 'view_group', 'add_permission', 'change_permission', 'delete_permission', 'view_permission',
#ä 'add_authtokeneventpermissions', 'change_authtokeneventpermissions', 'delete_authtokeneventpermissions',
#ä 'view_authtokeneventpermissions', 'add_eventpermission', 'change_eventpermission', 'delete_eventpermission',
#ä 'view_eventpermission', 'add_extendedauthtoken', 'change_extendedauthtoken', 'delete_extendedauthtoken',
#ä 'view_extendedauthtoken', 'add_extendeduser', 'change_extendeduser', 'delete_extendeduser',
#ä 'view_extendeduser', 'add_contenttype', 'change_contenttype', 'delete_contenttype', 'view_contenttype',
#ä 'add_file', 'change_file', 'delete_file', 'view_file', 'add_container', 'change_container', 'delete_container',
#ä 'view_container', 'add_event', 'change_event', 'delete_event', 'view_event', 'add_item', 'change_item',
#ä 'delete_item', 'match_item', 'view_item', 'add_authtoken', 'change_authtoken', 'delete_authtoken',
#ä 'view_authtoken', 'add_email', 'change_email', 'delete_email', 'view_email', 'add_eventaddress',
#ä 'change_eventaddress', 'delete_eventaddress', 'view_eventaddress', 'add_systemevent', 'change_systemevent',
#ä 'delete_systemevent', 'view_systemevent', 'add_session', 'change_session', 'delete_session', 'view_session',
#ä 'add_comment', 'change_comment', 'delete_comment', 'view_comment', 'add_issuethread', 'change_issuethread',
#ä 'delete_issuethread', 'send_mail', 'view_issuethread', 'add_statechange', 'change_statechange',
#ä 'delete_statechange', 'view_statechange']
user = ExtendedUser.objects.create_user('testuser2', 'test', 'test')
user.event_permissions.create(permission=Permission.objects.get(codename='view_item'), event=Event.objects.get(slug='testevent1'))
user.event_permissions.create(permission=Permission.objects.get(codename='view_item'), event=Event.objects.get(slug='testevent2'))
user.event_permissions.create(permission=Permission.objects.get(codename='add_item'), event=Event.objects.get(slug='testevent1'))
user.save()
#self.assertTrue(user.has_perm('inventory.view_event', Event.objects.get(slug='testevent1')))
#self.assertTrue(user.has_perm('inventory.view_event', Event.objects.get(slug='testevent2')))
#self.assertFalse(user.has_perm('inventory.add_event', Event.objects.get(slug='testevent1')))
#self.assertFalse(user.has_perm('inventory.add_event', Event.objects.get(slug='testevent2')))
def test_item_api_permissions(self):
"""
Test that a user can only access their own data.
"""
response = self.client.get('/api/2/testevent1/items/')
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 0)
response = self.client.get('/api/2/testevent2/items/')
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 0)
response = self.newuser_client.get('/api/2/testevent1/items/')
self.assertEqual(response.status_code, 403)
response = self.newuser_client.get('/api/2/testevent2/items/')
self.assertEqual(response.status_code, 403)

View file

@ -1,183 +0,0 @@
from django.test import TestCase, Client
from django.contrib.auth.models import Permission, Group
from knox.models import AuthToken
from authentication.models import ExtendedUser, EventPermission
from core import settings
from inventory.models import Event
class UserApiTest(TestCase):
def setUp(self):
self.event = Event.objects.create(name='testevent', slug='testevent')
self.group1 = Group.objects.create(name='testgroup1')
self.group2 = Group.objects.create(name='testgroup2')
self.group1.permissions.add(Permission.objects.get(codename='add_item'))
self.group1.permissions.add(Permission.objects.get(codename='view_item'))
self.group2.permissions.add(Permission.objects.get(codename='view_event'))
self.group2.permissions.add(Permission.objects.get(codename='view_item'))
self.user = ExtendedUser.objects.create_user('testuser', 'test', 'test')
self.user.user_permissions.add(Permission.objects.get(codename='add_event'))
self.user.groups.add(self.group1)
self.user.groups.add(self.group2)
self.user.save()
EventPermission.objects.create(event=self.event, user=self.user,
permission=Permission.objects.get(codename='delete_item'))
self.user.save()
self.token = AuthToken.objects.create(user=self.user)
self.client = Client(headers={'Authorization': 'Token ' + self.token[1]})
def test_users(self):
response = self.client.get('/api/2/users/')
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 2)
self.assertEqual(response.json()[0]['username'], settings.LEGACY_USER_NAME)
self.assertEqual(response.json()[0]['email'], 'mail@' + settings.MAIL_DOMAIN)
self.assertEqual(response.json()[0]['first_name'], '')
self.assertEqual(response.json()[0]['last_name'], '')
self.assertEqual(response.json()[0]['id'], 1)
self.assertEqual(response.json()[0]['groups'], [])
self.assertEqual(response.json()[1]['username'], 'testuser')
self.assertEqual(response.json()[1]['email'], 'test')
self.assertEqual(response.json()[1]['first_name'], '')
self.assertEqual(response.json()[1]['last_name'], '')
self.assertEqual(response.json()[1]['id'], 2)
self.assertEqual(response.json()[1]['groups'], ['testgroup1', 'testgroup2'])
def test_self_user(self):
response = self.client.get('/api/2/self/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()['username'], 'testuser')
self.assertEqual(response.json()['email'], 'test')
self.assertEqual(response.json()['first_name'], '')
self.assertEqual(response.json()['last_name'], '')
permissions = response.json()['permissions']
self.assertEqual(len(permissions), 5)
self.assertTrue('*:add_item' in permissions)
self.assertTrue('*:view_item' in permissions)
self.assertTrue('*:view_event' in permissions)
self.assertTrue('testevent:delete_item' in permissions)
self.assertTrue('*:add_event' in permissions)
def test_register_user(self):
anonymous = Client()
response = anonymous.post('/api/2/register/', {'username': 'testuser2', 'password': 'test', 'email': 'test2'},
content_type='application/json')
self.assertEqual(response.status_code, 201)
self.assertEqual(response.json()['username'], 'testuser2')
self.assertEqual(response.json()['email'], 'test2')
self.assertEqual(len(ExtendedUser.objects.all()), 3)
self.assertEqual(ExtendedUser.objects.get(username='testuser2').email, 'test2')
self.assertTrue(ExtendedUser.objects.get(username='testuser2').check_password('test'))
def test_register_user_duplicate(self):
anonymous = Client()
response = anonymous.post('/api/2/register/', {'username': 'testuser', 'password': 'test', 'email': 'test2'},
content_type='application/json')
self.assertEqual(response.status_code, 400)
self.assertEqual(response.json()['errors']['username'], 'Username already exists')
self.assertEqual(len(ExtendedUser.objects.all()), 2)
def test_register_user_no_username(self):
anonymous = Client()
response = anonymous.post('/api/2/register/', {'password': 'test', 'email': 'test2'},
content_type='application/json')
self.assertEqual(response.status_code, 400)
self.assertEqual(response.json()['errors']['username'], 'Username is required')
self.assertEqual(len(ExtendedUser.objects.all()), 2)
def test_register_user_no_password(self):
anonymous = Client()
response = anonymous.post('/api/2/register/', {'username': 'testuser2', 'email': 'test2'},
content_type='application/json')
self.assertEqual(response.status_code, 400)
self.assertEqual(response.json()['errors']['password'], 'Password is required')
self.assertEqual(len(ExtendedUser.objects.all()), 2)
def test_register_user_no_email(self):
anonymous = Client()
response = anonymous.post('/api/2/register/', {'username': 'testuser2', 'password': 'test'},
content_type='application/json')
self.assertEqual(response.status_code, 400)
self.assertEqual(response.json()['errors']['email'], 'Email is required')
self.assertEqual(len(ExtendedUser.objects.all()), 2)
def test_register_user_duplicate_email(self):
anonymous = Client()
response = anonymous.post('/api/2/register/', {'username': 'testuser2', 'password': 'test', 'email': 'test'},
content_type='application/json')
self.assertEqual(response.status_code, 400)
self.assertEqual(response.json()['errors']['email'], 'Email already exists')
self.assertEqual(len(ExtendedUser.objects.all()), 2)
def test_get_token(self):
anonymous = Client()
response = anonymous.post('/api/2/login/', {'username': 'testuser', 'password': 'test'},
content_type='application/json')
self.assertEqual(response.status_code, 200)
self.assertTrue('token' in response.json())
def test_legacy_user(self):
response = self.client.get('/api/2/users/1/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()['username'], settings.LEGACY_USER_NAME)
self.assertEqual(response.json()['email'], 'mail@' + settings.MAIL_DOMAIN)
self.assertEqual(response.json()['first_name'], '')
self.assertEqual(response.json()['last_name'], '')
self.assertEqual(response.json()['id'], 1)
def test_get_legacy_user_token(self):
anonymous = Client()
response = anonymous.post('/api/2/login/', {
'username': settings.LEGACY_USER_NAME, 'password': settings.LEGACY_USER_PASSWORD},
content_type='application/json')
self.assertEqual(response.status_code, 200)
self.assertTrue('token' in response.json())
class GroupApiTest(TestCase):
def setUp(self):
self.event = Event.objects.create(name='testevent', slug='testevent')
# Admin, Orga, Team, User are created by default
self.group1 = Group.objects.create(name='testgroup1')
self.group2 = Group.objects.create(name='testgroup2')
self.group1.permissions.add(Permission.objects.get(codename='add_item'))
self.group1.permissions.add(Permission.objects.get(codename='view_item'))
self.group2.permissions.add(Permission.objects.get(codename='view_event'))
self.group2.permissions.add(Permission.objects.get(codename='view_item'))
self.user = ExtendedUser.objects.create_user('testuser', 'test', 'test')
self.user.user_permissions.add(Permission.objects.get(codename='add_event'))
self.user.groups.add(self.group1)
self.user.groups.add(self.group2)
self.user.save()
EventPermission.objects.create(event=self.event, user=self.user,
permission=Permission.objects.get(codename='delete_item'))
self.user.save()
self.token = AuthToken.objects.create(user=self.user)
self.client = Client(headers={'Authorization': 'Token ' + self.token[1]})
def test_groups(self):
response = self.client.get('/api/2/groups/')
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 6)
self.assertEqual(response.json()[0]['name'], 'Admin')
self.assertEqual(response.json()[1]['name'], 'Orga')
self.assertEqual(response.json()[2]['name'], 'Team')
self.assertEqual(response.json()[3]['name'], 'User')
self.assertEqual(response.json()[4]['name'], 'testgroup1')
self.assertEqual(response.json()[5]['name'], 'testgroup2')
def test_group(self):
response = self.client.get('/api/2/groups/5/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()['name'], 'testgroup1')
permissions = response.json()['permissions']
self.assertEqual(len(permissions), 2)
self.assertTrue('*:add_item' in permissions)
self.assertTrue('*:view_item' in permissions)
members = response.json()['members']
self.assertEqual(len(members), 1)
self.assertEqual(members[0], 'testuser')

View file

View file

@ -1,66 +0,0 @@
"""
ASGI config for core project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
"""
import os
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.security.websocket import AllowedHostsOriginValidator
from django.core.asgi import get_asgi_application
from notify_sessions.routing import websocket_urlpatterns
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
django_asgi_app = get_asgi_application()
class TokenAuthMiddleware:
"""
Token authorization middleware for Django Channels 2
"""
def __init__(self, inner):
self.inner = inner
def __call__(self, scope):
import base64
headers = dict(scope['headers'])
if b'authorization' in headers:
try:
token_name, token_key = headers[b'authorization'].decode().split()
if token_name == 'Basic':
b64 = base64.b64decode(token_key)
user = b64.decode().split(':')[0]
password = b64.decode().split(':')[1]
print(user, password)
else:
print("Token name is not Basic")
scope['user'] = None
except:
print("Token is not valid")
scope['user'] = None
else:
print("Token is not in headers")
scope['user'] = None
TokenAuthMiddlewareStack = lambda inner: TokenAuthMiddleware(AuthMiddlewareStack(inner))
websocket_asgi_app = AllowedHostsOriginValidator(
AuthMiddlewareStack(
URLRouter(
websocket_urlpatterns
)
)
)
application = ProtocolTypeRouter({
"http": django_asgi_app,
"websocket": websocket_asgi_app,
})

View file

@ -1,29 +0,0 @@
import asyncio
import logging
import signal
loop = asyncio.get_event_loop()
def create_task(coro):
global loop
loop.create_task(coro)
async def shutdown(sig, loop):
log = logging.getLogger()
log.info(f"Received exit signal {sig.name}...")
tasks = [t for t in asyncio.all_tasks() if t is not
asyncio.current_task()]
[task.cancel() for task in tasks]
log.info(f"Cancelling {len(tasks)} outstanding tasks")
await asyncio.wait_for(loop.shutdown_asyncgens(), timeout=10)
loop.stop()
log.info("Shutdown complete.")
def init_loop():
global loop
loop.add_signal_handler(signal.SIGTERM, lambda: asyncio.create_task(shutdown(signal.SIGTERM, loop)))
loop.add_signal_handler(signal.SIGINT, lambda: asyncio.create_task(shutdown(signal.SIGINT, loop)))
return loop

View file

@ -1,213 +0,0 @@
"""
Django settings for core project.
Generated by 'django-admin startproject' using Django 4.2.7.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.2/ref/settings/
"""
import os
import sys
import dotenv
from pathlib import Path
# 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 = '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 = True
ALLOWED_HOSTS = [os.getenv('HTTP_HOST', 'localhost')]
MAIL_DOMAIN = os.getenv('MAIL_DOMAIN', 'localhost')
CSRF_TRUSTED_ORIGINS = ["https://" + host for host in ALLOWED_HOSTS]
LEGACY_USER_NAME = os.getenv('LEGACY_API_USER', 'legacy_user')
LEGACY_USER_PASSWORD = os.getenv('LEGACY_API_PASSWORD', 'legacy_password')
SYSTEM3_VERSION = "0.0.0-dev.0"
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django_extensions',
'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.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',
]
ROOT_URLCONF = 'core.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'core.wsgi.application'
# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
if 'test' in sys.argv:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:',
}
}
else:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'HOST': os.getenv('DB_HOST', 'localhost'),
'PORT': os.getenv('DB_PORT', '3306'),
'NAME': os.getenv('DB_NAME', 'system3'),
'USER': os.getenv('DB_USER', 'system3'),
'PASSWORD': os.getenv('DB_PASSWORD', 'system3'),
'OPTIONS': {
'charset': 'utf8mb4'
}
}
}
# Password validation
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/
STATIC_ROOT = os.getenv('STATIC_ROOT', 'staticfiles')
STATIC_URL = '/static/'
MEDIA_ROOT = os.getenv('MEDIA_ROOT', 'userfiles')
MEDIA_URL = '/media/'
STORAGES = {
'default': {
'BACKEND': 'django.core.files.storage.FileSystemStorage',
'OPTIONS': {
'base_url': MEDIA_URL,
'location': BASE_DIR / MEDIA_ROOT
},
},
'staticfiles': {
'BACKEND': 'django.core.files.storage.FileSystemStorage',
'OPTIONS': {
'base_url': STATIC_URL,
'location': BASE_DIR / STATIC_ROOT
},
},
}
DATA_UPLOAD_MAX_MEMORY_SIZE = 1024 * 1024 * 128 # 128 MB
# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
'hosts': [('localhost', 6379)],
},
}
}
TEST_RUNNER = 'core.test_runner.FastTestRunner'

View file

@ -1,33 +0,0 @@
from django.conf import settings
from django.test.runner import DiscoverRunner
class FastTestRunner(DiscoverRunner):
def setup_test_environment(self):
super(FastTestRunner, self).setup_test_environment()
# Don't write files
settings.STORAGES = {
'default': {
'BACKEND': 'django.core.files.storage.InMemoryStorage',
'OPTIONS': {
'base_url': '/media/',
'location': '',
},
},
}
# Bonus: Use a faster password hasher. This REALLY helps.
settings.PASSWORD_HASHERS = (
'django.contrib.auth.hashers.MD5PasswordHasher',
)
settings.CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels.layers.InMemoryChannelLayer'
}
}
settings.DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:',
}
}

View file

@ -1,35 +0,0 @@
"""
URL configuration for core project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/4.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
from .version import get_info
urlpatterns = [
path('djangoadmin/', admin.site.urls),
path('api/1/', include('inventory.api_v1')),
path('api/1/', include('files.api_v1')),
path('api/1/', include('files.media_v1')),
path('api/2/', include('inventory.api_v2')),
path('api/2/', include('files.api_v2')),
path('media/2/', include('files.media_v2')),
path('api/2/', include('tickets.api_v2')),
path('api/2/', include('mail.api_v2')),
path('api/2/', include('notify_sessions.api_v2')),
path('api/2/', include('authentication.api_v2')),
path('api/', get_info),
]

View file

@ -1,15 +0,0 @@
from rest_framework.decorators import api_view, permission_classes, authentication_classes
from rest_framework.response import Response
from .settings import SYSTEM3_VERSION
@api_view(['GET'])
@permission_classes([])
@authentication_classes([])
def get_info(request):
return Response({
"framework_version": SYSTEM3_VERSION,
"api_min_version": "1.0",
"api_max_version": "1.0",
})

View file

@ -1,16 +0,0 @@
"""
WSGI config for core project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
application = get_wsgi_application()

View file

View file

@ -1,10 +0,0 @@
from django.contrib import admin
from files.models import File
class FileAdmin(admin.ModelAdmin):
pass
admin.site.register(File, FileAdmin)

View file

@ -1,27 +0,0 @@
from rest_framework import serializers, viewsets, routers
from files.models import File
class FileSerializer(serializers.ModelSerializer):
data = serializers.CharField(max_length=1000000, write_only=True)
class Meta:
model = File
fields = ['hash', 'data']
read_only_fields = ['hash']
class FileViewSet(viewsets.ModelViewSet):
serializer_class = FileSerializer
queryset = File.objects.all()
lookup_field = 'hash'
permission_classes = []
authentication_classes = []
router = routers.SimpleRouter(trailing_slash=False)
router.register(r'files', FileViewSet, basename='files')
router.register(r'file', FileViewSet, basename='files')
urlpatterns = router.urls

View file

@ -1,24 +0,0 @@
from rest_framework import serializers, viewsets, routers
from files.models import File
class FileSerializer(serializers.ModelSerializer):
data = serializers.CharField(max_length=1000000, write_only=True)
class Meta:
model = File
fields = ['hash', 'data']
read_only_fields = ['hash']
class FileViewSet(viewsets.ModelViewSet):
serializer_class = FileSerializer
queryset = File.objects.all()
lookup_field = 'hash'
router = routers.SimpleRouter()
router.register(r'files', FileViewSet, basename='files')
urlpatterns = router.urls

View file

@ -1,65 +0,0 @@
import os
from django.http import HttpResponse
from django.urls import path
from drf_yasg.utils import swagger_auto_schema
from rest_framework import status
from rest_framework.decorators import api_view, permission_classes, authentication_classes
from rest_framework.response import Response
from core.settings import MEDIA_ROOT
from files.models import File
@swagger_auto_schema(method='GET', auto_schema=None)
@api_view(['GET'])
@permission_classes([])
@authentication_classes([])
def media_urls(request, hash):
try:
file = File.objects.get(hash=hash)
hash_path = file.file
return HttpResponse(status=status.HTTP_200_OK,
content_type=file.mime_type,
headers={
'X-Accel-Redirect': f'/redirect_media/{hash_path}',
'Access-Control-Allow-Origin': '*',
}) # TODO Expires and Cache-Control
except File.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND)
@swagger_auto_schema(method='GET', auto_schema=None)
@api_view(['GET'])
@permission_classes([])
@authentication_classes([])
def thumbnail_urls(request, hash):
size = 200
try:
file = File.objects.get(hash=hash)
hash_path = file.file
if not os.path.exists(MEDIA_ROOT + f'/thumbnails/{size}/{hash_path}'):
from PIL import Image
image = Image.open(file.file)
image.thumbnail((size, size))
rgb_image = image.convert('RGB')
thumb_dir = os.path.dirname(MEDIA_ROOT + f'/thumbnails/{size}/{hash_path}')
if not os.path.exists(thumb_dir):
os.makedirs(thumb_dir)
rgb_image.save(MEDIA_ROOT + f'/thumbnails/{size}/{hash_path}', 'jpeg', quality=90)
return HttpResponse(status=status.HTTP_200_OK,
content_type="image/jpeg",
headers={
'X-Accel-Redirect': f'/redirect_thumbnail/{size}/{hash_path}',
'Access-Control-Allow-Origin': '*',
}) # TODO Expires and Cache-Control
except File.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND)
urlpatterns = [
path('thumbs/<path:hash>', thumbnail_urls),
path('images/<path:hash>', media_urls),
]

View file

@ -1,92 +0,0 @@
from datetime import datetime, timedelta
import os
from django.http import HttpResponse
from django.urls import path
from drf_yasg.utils import swagger_auto_schema
from rest_framework import status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from core.settings import MEDIA_ROOT
from files.models import File
from mail.models import EmailAttachment
@swagger_auto_schema(method='GET', auto_schema=None)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
def media_urls(request, hash):
try:
if request.META.get('HTTP_IF_NONE_MATCH') and request.META.get('HTTP_IF_NONE_MATCH') == hash:
return HttpResponse(status=status.HTTP_304_NOT_MODIFIED)
file = File.objects.filter(hash=hash).first()
attachment = EmailAttachment.objects.filter(hash=hash).first()
file = file if file else attachment
if not file:
return Response(status=status.HTTP_404_NOT_FOUND)
hash_path = file.file
return HttpResponse(status=status.HTTP_200_OK,
content_type=file.mime_type,
headers={
'X-Accel-Redirect': f'/redirect_media/{hash_path}',
'Access-Control-Allow-Origin': '*',
'Cache-Control': 'max-age=31536000, private',
'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',
'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),
]

View file

@ -1,30 +0,0 @@
# Generated by Django 4.2.7 on 2023-12-09 02:13
from django.db import migrations, models
import django.db.models.deletion
import files.models
class Migration(migrations.Migration):
initial = True
dependencies = [
('inventory', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='File',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(blank=True, null=True)),
('updated_at', models.DateTimeField(blank=True, null=True)),
('deleted_at', models.DateTimeField(blank=True, null=True)),
('file', models.ImageField(upload_to=files.models.hash_upload)),
('mime_type', models.CharField(max_length=255)),
('hash', models.CharField(max_length=64, unique=True)),
('item', models.ForeignKey(blank=True, db_column='iid', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='files', to='inventory.item')),
],
),
]

View file

@ -1,19 +0,0 @@
# Generated by Django 4.2.7 on 2024-01-10 19:04
from django.db import migrations, models
import files.models
class Migration(migrations.Migration):
dependencies = [
('files', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='file',
name='file',
field=models.FileField(upload_to=files.models.hash_upload),
),
]

View file

@ -1,93 +0,0 @@
from django.core.files.base import ContentFile
from django.db import models, IntegrityError
from django_softdelete.models import SoftDeleteModel
from inventory.models import Item
def hash_upload(instance, filename):
return f"{instance.hash[:2]}/{instance.hash[2:4]}/{instance.hash[4:6]}/{instance.hash[6:]}"
class FileManager(models.Manager):
def get_or_create(self, **kwargs):
if 'data' in kwargs and type(kwargs['data']) == str:
import base64
from hashlib import sha256
raw = kwargs['data']
if not raw.startswith('data:'):
raise ValueError('data must be a base64 encoded string or file and hash must be provided')
raw = raw.split(';base64,')
if len(raw) != 2:
raise ValueError('data must be a base64 encoded string or file and hash must be provided')
mime_type = raw[0].split(':')[1]
content = base64.b64decode(raw[1], validate=True)
kwargs.pop('data')
content_hash = sha256(content).hexdigest()
kwargs['file'] = ContentFile(content, content_hash)
kwargs['hash'] = content_hash
kwargs['mime_type'] = mime_type
elif 'file' in kwargs and 'hash' in kwargs and type(kwargs['file']) == ContentFile and 'mime_type' in kwargs:
pass
else:
raise ValueError('data must be a base64 encoded string or file and hash must be provided')
try:
return self.get(hash=kwargs['hash']), False
except self.model.DoesNotExist:
obj = super().create(**kwargs)
obj.file.save(content=kwargs['file'], name=kwargs['hash'])
return obj, True
def create(self, **kwargs):
if 'data' in kwargs and type(kwargs['data']) == str:
import base64
from hashlib import sha256
raw = kwargs['data']
if not raw.startswith('data:'):
raise ValueError('data must be a base64 encoded string or file and hash must be provided')
raw = raw.split(';base64,')
if len(raw) != 2:
raise ValueError('data must be a base64 encoded string or file and hash must be provided')
mime_type = raw[0].split(':')[1]
content = base64.b64decode(raw[1], validate=True)
kwargs.pop('data')
content_hash = sha256(content).hexdigest()
kwargs['file'] = ContentFile(content, content_hash)
kwargs['hash'] = content_hash
kwargs['mime_type'] = mime_type
elif 'file' in kwargs and 'hash' in kwargs and type(kwargs['file']) == ContentFile and 'mime_type' in kwargs:
pass
else:
raise ValueError('data must be a base64 encoded string or file and hash must be provided')
if not self.filter(hash=kwargs['hash']).exists():
obj = super().create(**kwargs)
obj.file.save(content=kwargs['file'], name=kwargs['hash'])
return obj
else:
raise IntegrityError('File with this hash already exists')
class AbstractFile(models.Model):
created_at = models.DateTimeField(blank=True, null=True)
updated_at = models.DateTimeField(blank=True, null=True)
deleted_at = models.DateTimeField(blank=True, null=True)
file = models.FileField(upload_to=hash_upload)
mime_type = models.CharField(max_length=255, null=False, blank=False)
hash = models.CharField(max_length=64, null=False, blank=False, unique=True)
objects = FileManager()
def save(self, *args, **kwargs):
from django.utils import timezone
if not self.created_at:
self.created_at = timezone.now()
self.updated_at = timezone.now()
super().save(*args, **kwargs)
class Meta:
abstract = True
class File(AbstractFile):
item = models.ForeignKey(Item, models.CASCADE, db_column='iid', null=True, blank=True, related_name='files')
pass

View file

@ -1,68 +0,0 @@
from django.test import TestCase, Client
from django.core.files.base import ContentFile
from files.models import File
from inventory.models import Event, Container, Item
client = Client()
class FileTestCase(TestCase):
def setUp(self):
super().setUp()
self.event = Event.objects.create(slug='EVENT', name='Event')
self.box = Container.objects.create(name='BOX')
def test_create_file_raw(self):
from hashlib import sha256
content = b"foo"
chash = sha256(content).hexdigest()
item = Item.objects.create(container=self.box, event=self.event, description='1')
file = File.objects.create(file=ContentFile(b"foo"), mime_type='text/plain', hash=chash, item=item)
file.save()
self.assertEqual(1, len(File.objects.all()))
self.assertEqual(content, File.objects.all()[0].file.read())
self.assertEqual(chash, File.objects.all()[0].hash)
def test_list_files(self):
import base64
item = File.objects.create(data="data:text/plain;base64," + base64.b64encode(b"foo").decode('utf-8'))
response = client.get('/api/1/files')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()[0]['hash'], item.hash)
self.assertEqual(len(response.json()[0]['hash']), 64)
self.assertEqual(len(File.objects.all()), 1)
self.assertEqual(File.objects.all()[0].file.read(), b"foo")
def test_one_file(self):
import base64
item = File.objects.create(data="data:text/plain;base64," + base64.b64encode(b"foo").decode('utf-8'))
response = client.get(f'/api/1/file/{item.hash}')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()['hash'], item.hash)
self.assertEqual(len(response.json()['hash']), 64)
self.assertEqual(len(File.objects.all()), 1)
self.assertEqual(File.objects.all()[0].file.read(), b"foo")
def test_create_file(self):
import base64
Item.objects.create(container=self.box, event=self.event, description='1')
item = Item.objects.create(container=self.box, event=self.event, description='2')
response = client.post('/api/1/file',
{'data': "data:text/plain;base64," + base64.b64encode(b"foo").decode('utf-8')},
content_type='application/json')
self.assertEqual(response.status_code, 201)
self.assertEqual(len(response.json()['hash']), 64)
self.assertEqual(len(File.objects.all()), 1)
self.assertEqual(File.objects.all()[0].file.read(), b"foo")
def test_delete_file(self):
import base64
item = Item.objects.create(container=self.box, event=self.event, description='1')
File.objects.create(item=item, data="data:text/plain;base64," + base64.b64encode(b"foo").decode('utf-8'))
file = File.objects.create(item=item, data="data:text/plain;base64," + base64.b64encode(b"bar").decode('utf-8'))
self.assertEqual(len(File.objects.all()), 2)
response = client.delete(f'/api/1/file/{file.hash}')
self.assertEqual(response.status_code, 204)

View file

@ -1,55 +0,0 @@
from django.test import TestCase, Client
from django.contrib.auth.models import Permission
from authentication.models import ExtendedUser
from files.models import File
from inventory.models import Event, Container, Item
from knox.models import AuthToken
class FileTestCase(TestCase):
def setUp(self):
super().setUp()
self.user = ExtendedUser.objects.create_user('testuser', 'test', 'test')
self.user.user_permissions.add(*Permission.objects.all())
self.user.save()
self.token = AuthToken.objects.create(user=self.user)
self.client = Client(headers={'Authorization': 'Token ' + self.token[1]})
self.event = Event.objects.create(slug='EVENT', name='Event')
self.box = Container.objects.create(name='BOX')
def test_list_files(self):
import base64
item = File.objects.create(data="data:text/plain;base64," + base64.b64encode(b"foo").decode('utf-8'))
response = self.client.get('/api/2/files/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()[0]['hash'], item.hash)
self.assertEqual(len(response.json()[0]['hash']), 64)
def test_one_file(self):
import base64
item = File.objects.create(data="data:text/plain;base64," + base64.b64encode(b"foo").decode('utf-8'))
response = self.client.get(f'/api/2/files/{item.hash}/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()['hash'], item.hash)
self.assertEqual(len(response.json()['hash']), 64)
def test_create_file(self):
import base64
Item.objects.create(container=self.box, event=self.event, description='1')
item = Item.objects.create(container=self.box, event=self.event, description='2')
response = self.client.post('/api/2/files/',
{'data': "data:text/plain;base64," + base64.b64encode(b"foo").decode('utf-8')},
content_type='application/json')
self.assertEqual(response.status_code, 201)
self.assertEqual(len(response.json()['hash']), 64)
def test_delete_file(self):
import base64
item = Item.objects.create(container=self.box, event=self.event, description='1')
File.objects.create(item=item, data="data:text/plain;base64," + base64.b64encode(b"foo").decode('utf-8'))
file = File.objects.create(item=item, data="data:text/plain;base64," + base64.b64encode(b"bar").decode('utf-8'))
self.assertEqual(len(File.objects.all()), 2)
response = self.client.delete(f'/api/2/files/{file.hash}/')
self.assertEqual(response.status_code, 204)

View file

@ -1,30 +0,0 @@
import asyncio
import logging
import signal
loop = None
def create_task(coro):
global loop
loop.create_task(coro)
async def shutdown(sig, loop):
log = logging.getLogger()
log.info(f"Received exit signal {sig.name}...")
tasks = [t for t in asyncio.all_tasks() if t is not
asyncio.current_task()]
[task.cancel() for task in tasks]
log.info(f"Cancelling {len(tasks)} outstanding tasks")
await asyncio.wait_for(loop.shutdown_asyncgens(), timeout=10)
loop.stop()
log.info("Shutdown complete.")
def init_loop():
global loop
loop = asyncio.get_event_loop()
loop.add_signal_handler(signal.SIGTERM, lambda: asyncio.create_task(shutdown(signal.SIGTERM, loop)))
loop.add_signal_handler(signal.SIGINT, lambda: asyncio.create_task(shutdown(signal.SIGINT, loop)))
return loop

View file

@ -1,24 +0,0 @@
from django.contrib import admin
from inventory.models import Item, Container, Event
class ItemAdmin(admin.ModelAdmin):
pass
admin.site.register(Item, ItemAdmin)
class ContainerAdmin(admin.ModelAdmin):
pass
admin.site.register(Container, ContainerAdmin)
class EventAdmin(admin.ModelAdmin):
pass
admin.site.register(Event, EventAdmin)

View file

@ -1,169 +0,0 @@
from datetime import datetime
from django.urls import re_path
from rest_framework import routers, viewsets, serializers
from rest_framework.decorators import api_view, permission_classes, authentication_classes
from rest_framework.response import Response
from files.models import File
from inventory.models import Event, Container, Item
class EventSerializer(serializers.ModelSerializer):
class Meta:
model = Event
fields = ['eid', 'slug', 'name', 'start', 'end', 'pre_start', 'post_end']
read_only_fields = ['eid']
class EventViewSet(viewsets.ModelViewSet):
serializer_class = EventSerializer
queryset = Event.objects.all()
permission_classes = []
authentication_classes = []
class ContainerSerializer(serializers.ModelSerializer):
itemCount = serializers.SerializerMethodField()
class Meta:
model = Container
fields = ['cid', 'name', 'itemCount']
read_only_fields = ['cid', 'itemCount']
def get_itemCount(self, instance):
return Item.objects.filter(container=instance.cid).count()
class ContainerViewSet(viewsets.ModelViewSet):
serializer_class = ContainerSerializer
queryset = Container.objects.all()
permission_classes = []
authentication_classes = []
class ItemSerializer(serializers.ModelSerializer):
dataImage = serializers.CharField(write_only=True, required=False)
cid = serializers.SerializerMethodField()
box = serializers.SerializerMethodField()
file = serializers.SerializerMethodField()
class Meta:
model = Item
fields = ['cid', 'box', 'uid', 'description', 'file', 'dataImage']
read_only_fields = ['uid']
def get_cid(self, instance):
return instance.container.cid
def get_box(self, instance):
return instance.container.name
def get_file(self, instance):
if len(instance.files.all()) > 0:
return instance.files.all().order_by('-created_at')[0].hash
return None
def to_internal_value(self, data):
if 'cid' in data:
container = Container.objects.get(cid=data['cid'])
internal = super().to_internal_value(data)
internal['container'] = container
return internal
return super().to_internal_value(data)
def validate(self, attrs):
return super().validate(attrs)
def create(self, validated_data):
if 'dataImage' in validated_data:
file = File.objects.create(data=validated_data['dataImage'])
validated_data.pop('dataImage')
item = Item.objects.create(**validated_data)
item.files.set([file])
return item
return Item.objects.create(**validated_data)
def update(self, instance, validated_data):
if 'returned' in validated_data:
if validated_data['returned']:
validated_data['returned_at'] = datetime.now()
validated_data.pop('returned')
if 'dataImage' in validated_data:
file = File.objects.create(data=validated_data['dataImage'])
validated_data.pop('dataImage')
instance.files.add(file)
return super().update(instance, validated_data)
@api_view(['GET'])
@permission_classes([])
@authentication_classes([])
def search_items(request, event_slug, query):
try:
event = Event.objects.get(slug=event_slug)
query_tokens = query.split(' ')
q = Item.objects.filter(event=event)
for token in query_tokens:
if token:
q = q.filter(description__icontains=token)
return Response(ItemSerializer(q, many=True).data)
except Event.DoesNotExist:
return Response(status=404)
@api_view(['GET', 'POST'])
@permission_classes([])
@authentication_classes([])
def item(request, event_slug):
try:
event = Event.objects.get(slug=event_slug)
if request.method == 'GET':
return Response(ItemSerializer(Item.objects.filter(event=event), many=True).data)
elif request.method == 'POST':
validated_data = ItemSerializer(data=request.data)
if validated_data.is_valid():
validated_data.save(event=event)
return Response(validated_data.data, status=201)
except Event.DoesNotExist:
return Response(status=404)
@api_view(['GET', 'PUT', 'DELETE'])
@permission_classes([])
@authentication_classes([])
def item_by_id(request, event_slug, id):
try:
event = Event.objects.get(slug=event_slug)
item = Item.objects.get(event=event, uid=id)
if request.method == 'GET':
return Response(ItemSerializer(item).data)
elif request.method == 'PUT':
validated_data = ItemSerializer(item, data=request.data)
if validated_data.is_valid():
validated_data.save()
return Response(validated_data.data)
elif request.method == 'DELETE':
item.delete()
return Response(status=204)
except Item.DoesNotExist:
return Response(status=404)
except Event.DoesNotExist:
return Response(status=404)
urlpatterns = [
re_path('events/?$', EventViewSet.as_view({'get': 'list', 'post': 'create'})),
re_path('events/(?P<pk>[0-9]+)/?$',
EventViewSet.as_view({'get': 'retrieve', 'put': 'update', 'delete': 'destroy'})),
re_path('boxes/?$', ContainerViewSet.as_view({'get': 'list', 'post': 'create'})),
re_path('boxes/(?P<pk>[0-9]+)/?$',
ContainerViewSet.as_view({'get': 'retrieve', 'put': 'update', 'delete': 'destroy'})),
re_path('box/?$', ContainerViewSet.as_view({'get': 'list', 'post': 'create'})),
re_path('box/(?P<pk>[0-9]+)/?$',
ContainerViewSet.as_view({'get': 'retrieve', 'put': 'update', 'delete': 'destroy'})),
re_path('(?P<event_slug>[a-zA-Z0-9]+)/items/?$', item),
re_path('(?P<event_slug>[a-zA-Z0-9]+)/items/(?P<query>[^/]+)/?$', search_items),
re_path('(?P<event_slug>[a-zA-Z0-9]+)/item/?$', item),
re_path('(?P<event_slug>[a-zA-Z0-9]+)/item/(?P<id>[0-9]+)/?$', item_by_id),
]

View file

@ -1,191 +0,0 @@
from datetime import datetime
from django.urls import path, re_path
from django.contrib.auth.decorators import permission_required
from rest_framework import routers, viewsets, serializers
from rest_framework.decorators import api_view, permission_classes
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from files.models import File
from inventory.models import Event, Container, Item
class EventSerializer(serializers.ModelSerializer):
class Meta:
model = Event
fields = ['eid', 'slug', 'name', 'start', 'end', 'pre_start', 'post_end']
read_only_fields = ['eid']
class EventViewSet(viewsets.ModelViewSet):
serializer_class = EventSerializer
queryset = Event.objects.all()
permission_classes = []
class ContainerSerializer(serializers.ModelSerializer):
itemCount = serializers.SerializerMethodField()
class Meta:
model = Container
fields = ['cid', 'name', 'itemCount']
read_only_fields = ['cid', 'itemCount']
def get_itemCount(self, instance):
return Item.objects.filter(container=instance.cid).count()
class ContainerViewSet(viewsets.ModelViewSet):
serializer_class = ContainerSerializer
queryset = Container.objects.all()
class ItemSerializer(serializers.ModelSerializer):
dataImage = serializers.CharField(write_only=True, required=False)
cid = serializers.SerializerMethodField()
box = serializers.SerializerMethodField()
file = serializers.SerializerMethodField()
returned = serializers.SerializerMethodField(required=False)
class Meta:
model = Item
fields = ['cid', 'box', 'uid', 'description', 'file', 'dataImage', 'returned']
read_only_fields = ['uid']
def get_cid(self, instance):
return instance.container.cid
def get_box(self, instance):
return instance.container.name
def get_file(self, instance):
if len(instance.files.all()) > 0:
return instance.files.all().order_by('-created_at')[0].hash
return None
def get_returned(self, instance):
return instance.returned_at is not None
def to_internal_value(self, data):
container = None
returned = False
if 'cid' in data:
container = Container.objects.get(cid=data['cid'])
if 'returned' in data:
returned = data['returned']
internal = super().to_internal_value(data)
if container:
internal['container'] = container
if returned:
internal['returned_at'] = datetime.now()
return internal
def validate(self, attrs):
return super().validate(attrs)
def create(self, validated_data):
if 'dataImage' in validated_data:
file = File.objects.create(data=validated_data['dataImage'])
validated_data.pop('dataImage')
item = Item.objects.create(**validated_data)
item.files.set([file])
return item
return Item.objects.create(**validated_data)
def update(self, instance, validated_data):
if 'returned' in validated_data:
if validated_data['returned']:
validated_data['returned_at'] = datetime.now()
validated_data.pop('returned')
if 'dataImage' in validated_data:
file = File.objects.create(data=validated_data['dataImage'])
validated_data.pop('dataImage')
instance.files.add(file)
return super().update(instance, validated_data)
@api_view(['GET'])
@permission_classes([IsAuthenticated])
@permission_required('view_item', raise_exception=True)
def search_items(request, event_slug, query):
try:
event = Event.objects.get(slug=event_slug)
query_tokens = query.split(' ')
q = Item.objects.filter(event=event)
for token in query_tokens:
if token:
q = q.filter(description__icontains=token)
return Response(ItemSerializer(q, many=True).data)
except Event.DoesNotExist:
return Response(status=404)
@api_view(['GET', 'POST'])
@permission_classes([IsAuthenticated])
def item(request, event_slug):
try:
event = Event.objects.get(slug=event_slug)
if request.method == 'GET':
if not request.user.has_event_perm(event, 'view_item'):
return Response(status=403)
return Response(ItemSerializer(Item.objects.filter(event=event), many=True).data)
elif request.method == 'POST':
if not request.user.has_event_perm(event, 'add_item'):
return Response(status=403)
validated_data = ItemSerializer(data=request.data)
if validated_data.is_valid():
validated_data.save(event=event)
return Response(validated_data.data, status=201)
except Event.DoesNotExist:
return Response(status=404)
@api_view(['GET', 'PUT', 'DELETE', 'PATCH'])
@permission_classes([IsAuthenticated])
def item_by_id(request, event_slug, id):
try:
event = Event.objects.get(slug=event_slug)
item = Item.objects.get(event=event, uid=id)
if request.method == 'GET':
if not request.user.has_event_perm(event, 'view_item'):
return Response(status=403)
return Response(ItemSerializer(item).data)
elif request.method == 'PUT':
if not request.user.has_event_perm(event, 'change_item'):
return Response(status=403)
validated_data = ItemSerializer(item, data=request.data)
if validated_data.is_valid():
validated_data.save()
return Response(validated_data.data)
return Response(validated_data.errors, status=400)
elif request.method == 'PATCH':
if not request.user.has_event_perm(event, 'change_item'):
return Response(status=403)
validated_data = ItemSerializer(item, data=request.data, partial=True)
if validated_data.is_valid():
validated_data.save()
return Response(validated_data.data)
return Response(validated_data.errors, status=400)
elif request.method == 'DELETE':
if not request.user.has_event_perm(event, 'delete_item'):
return Response(status=403)
item.delete()
return Response(status=204)
except Item.DoesNotExist:
return Response(status=404)
except Event.DoesNotExist:
return Response(status=404)
router = routers.SimpleRouter()
router.register(r'events', EventViewSet, basename='events')
router.register(r'boxes', ContainerViewSet, basename='boxes')
router.register(r'box', ContainerViewSet, basename='boxes')
urlpatterns = router.urls + [
path('<event_slug>/items/', item),
path('<event_slug>/items/<query>/', search_items),
path('<event_slug>/item/', item),
path('<event_slug>/item/<id>/', item_by_id),
]

View file

@ -1,54 +0,0 @@
# Generated by Django 4.2.7 on 2023-11-18 11:28
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Container',
fields=[
('cid', models.AutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=255)),
('created_at', models.DateTimeField(blank=True, null=True)),
('updated_at', models.DateTimeField(blank=True, null=True)),
],
),
migrations.CreateModel(
name='Event',
fields=[
('eid', models.AutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=255)),
('slug', models.CharField(max_length=255, unique=True)),
('start', models.DateTimeField(blank=True, null=True)),
('end', models.DateTimeField(blank=True, null=True)),
('pre_start', models.DateTimeField(blank=True, null=True)),
('post_end', models.DateTimeField(blank=True, null=True)),
('created_at', models.DateTimeField(blank=True, null=True)),
('updated_at', models.DateTimeField(blank=True, null=True)),
],
),
migrations.CreateModel(
name='Item',
fields=[
('iid', models.AutoField(primary_key=True, serialize=False)),
('uid', models.IntegerField()),
('description', models.TextField()),
('returned_at', models.DateTimeField(blank=True, null=True)),
('created_at', models.DateTimeField(blank=True, null=True)),
('updated_at', models.DateTimeField(blank=True, null=True)),
('container', models.ForeignKey(db_column='cid', on_delete=django.db.models.deletion.CASCADE, to='inventory.container')),
('event', models.ForeignKey(db_column='eid', on_delete=django.db.models.deletion.CASCADE, to='inventory.event')),
],
options={
'unique_together': {('uid', 'event')},
},
),
]

View file

@ -1,33 +0,0 @@
# Generated by Django 4.2.7 on 2023-11-20 11:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='container',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='container',
name='is_deleted',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='item',
name='deleted_at',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='item',
name='is_deleted',
field=models.BooleanField(default=False),
),
]

View file

@ -1,17 +0,0 @@
# Generated by Django 4.2.7 on 2024-01-07 18:46
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('inventory', '0002_container_deleted_at_container_is_deleted_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='item',
options={'permissions': [('match_item', 'Can match item')]},
),
]

View file

@ -1,23 +0,0 @@
# Generated by Django 4.2.7 on 2024-01-22 16:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0003_alter_item_options'),
]
operations = [
migrations.AlterField(
model_name='event',
name='created_at',
field=models.DateTimeField(auto_now_add=True, null=True),
),
migrations.AlterField(
model_name='item',
name='created_at',
field=models.DateTimeField(auto_now_add=True, null=True),
),
]

View file

@ -1,55 +0,0 @@
from django.core.files.base import ContentFile
from django.db import models, IntegrityError
from django_softdelete.models import SoftDeleteModel, SoftDeleteManager
class ItemManager(SoftDeleteManager):
def create(self, **kwargs):
if 'uid' in kwargs:
raise ValueError('uid must not be set manually')
uid = Item.all_objects.filter(event=kwargs['event']).count() + 1
kwargs['uid'] = uid
return super().create(**kwargs)
def get_queryset(self):
return super().get_queryset().filter(returned_at__isnull=True)
class Item(SoftDeleteModel):
iid = models.AutoField(primary_key=True)
uid = models.IntegerField()
description = models.TextField()
event = models.ForeignKey('Event', models.CASCADE, db_column='eid')
container = models.ForeignKey('Container', models.CASCADE, db_column='cid')
returned_at = models.DateTimeField(blank=True, null=True)
created_at = models.DateTimeField(null=True, auto_now_add=True)
updated_at = models.DateTimeField(blank=True, null=True)
objects = ItemManager()
all_objects = models.Manager()
class Meta:
unique_together = (('uid', 'event'),)
permissions = [
('match_item', 'Can match item')
]
class Container(SoftDeleteModel):
cid = models.AutoField(primary_key=True)
name = models.CharField(max_length=255)
created_at = models.DateTimeField(blank=True, null=True)
updated_at = models.DateTimeField(blank=True, null=True)
class Event(models.Model):
eid = models.AutoField(primary_key=True)
name = models.CharField(max_length=255)
slug = models.CharField(max_length=255, unique=True)
start = models.DateTimeField(blank=True, null=True)
end = models.DateTimeField(blank=True, null=True)
pre_start = models.DateTimeField(blank=True, null=True)
post_end = models.DateTimeField(blank=True, null=True)
created_at = models.DateTimeField(null=True, auto_now_add=True)
updated_at = models.DateTimeField(blank=True, null=True)

View file

@ -1,34 +0,0 @@
from django.test import TestCase, Client
client = Client()
class ApiTest(TestCase):
def test_root(self):
from core.settings import SYSTEM3_VERSION
response = client.get('/api/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["framework_version"], SYSTEM3_VERSION)
def test_events(self):
response = client.get('/api/1/events')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), [])
def test_containers(self):
response = client.get('/api/1/boxes')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), [])
def test_files(self):
response = client.get('/api/1/files')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), [])
def test_items(self):
from inventory.models import Event
Event.objects.create(slug='TEST1', name='Event')
response = client.get('/api/1/TEST1/items')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), [])

View file

@ -1,59 +0,0 @@
from django.test import TestCase, Client
from inventory.models import Container
client = Client()
class ContainerTestCase(TestCase):
def test_empty(self):
response = client.get('/api/1/boxes')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), [])
def test_members(self):
Container.objects.create(name='BOX')
response = client.get('/api/1/boxes')
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 1)
self.assertEqual(response.json()[0]['cid'], 1)
self.assertEqual(response.json()[0]['name'], 'BOX')
self.assertEqual(response.json()[0]['itemCount'], 0)
def test_multi_members(self):
Container.objects.create(name='BOX 1')
Container.objects.create(name='BOX 2')
Container.objects.create(name='BOX 3')
response = client.get('/api/1/boxes')
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 3)
def test_create_container(self):
response = client.post('/api/1/box', {'name': 'BOX'})
self.assertEqual(response.status_code, 201)
self.assertEqual(response.json()['cid'], 1)
self.assertEqual(response.json()['name'], 'BOX')
self.assertEqual(response.json()['itemCount'], 0)
self.assertEqual(len(Container.objects.all()), 1)
self.assertEqual(Container.objects.all()[0].cid, 1)
self.assertEqual(Container.objects.all()[0].name, 'BOX')
def test_update_container(self):
from rest_framework.test import APIClient
box = Container.objects.create(name='BOX 1')
response = APIClient().put(f'/api/1/box/{box.cid}', {'name': 'BOX 2'})
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()['cid'], 1)
self.assertEqual(response.json()['name'], 'BOX 2')
self.assertEqual(response.json()['itemCount'], 0)
self.assertEqual(len(Container.objects.all()), 1)
self.assertEqual(Container.objects.all()[0].cid, 1)
self.assertEqual(Container.objects.all()[0].name, 'BOX 2')
def test_delete_container(self):
box = Container.objects.create(name='BOX 1')
Container.objects.create(name='BOX 2')
self.assertEqual(len(Container.objects.all()), 2)
response = client.delete(f'/api/1/box/{box.cid}')
self.assertEqual(response.status_code, 204)
self.assertEqual(len(Container.objects.all()), 1)

View file

@ -1,56 +0,0 @@
from django.test import TestCase, Client
from inventory.models import Event
client = Client()
class EventTestCase(TestCase):
def test_empty(self):
response = client.get('/api/1/events')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), [])
def test_members(self):
Event.objects.create(slug='EVENT', name='Event')
response = client.get('/api/1/events')
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 1)
self.assertEqual(response.json()[0]['slug'], 'EVENT')
self.assertEqual(response.json()[0]['name'], 'Event')
def test_multi_members(self):
Event.objects.create(slug='EVENT1', name='Event 1')
Event.objects.create(slug='EVENT2', name='Event 2')
Event.objects.create(slug='EVENT3', name='Event 3')
response = client.get('/api/1/events')
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 3)
def test_create_event(self):
response = client.post('/api/1/events', {'slug': 'EVENT', 'name': 'Event'})
self.assertEqual(response.status_code, 201)
self.assertEqual(response.json()['slug'], 'EVENT')
self.assertEqual(response.json()['name'], 'Event')
self.assertEqual(len(Event.objects.all()), 1)
self.assertEqual(Event.objects.all()[0].slug, 'EVENT')
self.assertEqual(Event.objects.all()[0].name, 'Event')
def test_update_event(self):
from rest_framework.test import APIClient
event = Event.objects.create(slug='EVENT1', name='Event 1')
response = APIClient().put(f'/api/1/events/{event.eid}', {'slug': 'EVENT2', 'name': 'Event 2 new'})
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()['slug'], 'EVENT2')
self.assertEqual(response.json()['name'], 'Event 2 new')
self.assertEqual(len(Event.objects.all()), 1)
self.assertEqual(Event.objects.all()[0].slug, 'EVENT2')
self.assertEqual(Event.objects.all()[0].name, 'Event 2 new')
def test_remove_event(self):
event = Event.objects.create(slug='EVENT1', name='Event 1')
Event.objects.create(slug='EVENT2', name='Event 2')
self.assertEqual(len(Event.objects.all()), 2)
response = client.delete(f'/api/1/events/{event.eid}')
self.assertEqual(response.status_code, 204)
self.assertEqual(len(Event.objects.all()), 1)

View file

@ -1,133 +0,0 @@
from django.test import TestCase, Client
from files.models import File
from inventory.models import Event, Container, Item
client = Client()
class ItemTestCase(TestCase):
def setUp(self):
super().setUp()
self.event = Event.objects.create(slug='EVENT', name='Event')
self.box = Container.objects.create(name='BOX')
def test_empty(self):
response = client.get(f'/api/1/{self.event.slug}/item')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content, b'[]')
def test_members(self):
item = Item.objects.create(container=self.box, event=self.event, description='1')
response = client.get(f'/api/1/{self.event.slug}/item')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(),
[{'uid': 1, 'description': '1', 'box': 'BOX', 'cid': self.box.cid, 'file': None}])
def test_members_with_file(self):
import base64
item = Item.objects.create(container=self.box, event=self.event, description='1')
file = File.objects.create(item=item, data="data:text/plain;base64," + base64.b64encode(b"foo").decode('utf-8'))
response = client.get(f'/api/1/{self.event.slug}/item')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(),
[{'uid': 1, 'description': '1', 'box': 'BOX', 'cid': self.box.cid, 'file': file.hash}])
def test_multi_members(self):
Item.objects.create(container=self.box, event=self.event, description='1')
Item.objects.create(container=self.box, event=self.event, description='2')
Item.objects.create(container=self.box, event=self.event, description='3')
response = client.get(f'/api/1/{self.event.slug}/item')
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 3)
def test_create_item(self):
response = client.post(f'/api/1/{self.event.slug}/item', {'cid': self.box.cid, 'description': '1'})
self.assertEqual(response.status_code, 201)
self.assertEqual(response.json(),
{'uid': 1, 'description': '1', 'box': 'BOX', 'cid': self.box.cid, 'file': None})
self.assertEqual(len(Item.objects.all()), 1)
self.assertEqual(Item.objects.all()[0].uid, 1)
self.assertEqual(Item.objects.all()[0].description, '1')
self.assertEqual(Item.objects.all()[0].container.cid, self.box.cid)
def test_create_item_with_file(self):
import base64
response = client.post(f'/api/1/{self.event.slug}/item',
{'cid': self.box.cid, 'description': '1',
'dataImage': "data:text/plain;base64," + base64.b64encode(b"foo").decode(
'utf-8')}, content_type='application/json')
self.assertEqual(response.status_code, 201)
self.assertEqual(response.json()['uid'], 1)
self.assertEqual(response.json()['description'], '1')
self.assertEqual(response.json()['box'], 'BOX')
self.assertEqual(response.json()['cid'], self.box.cid)
self.assertEqual(len(response.json()['file']), 64)
self.assertEqual(len(Item.objects.all()), 1)
self.assertEqual(Item.objects.all()[0].uid, 1)
self.assertEqual(Item.objects.all()[0].description, '1')
self.assertEqual(Item.objects.all()[0].container.cid, self.box.cid)
self.assertEqual(len(File.objects.all()), 1)
def test_update_item(self):
item = Item.objects.create(container=self.box, event=self.event, description='1')
response = client.put(f'/api/1/{self.event.slug}/item/{item.uid}', {'description': '2'},
content_type='application/json')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(),
{'uid': 1, 'description': '2', 'box': 'BOX', 'cid': self.box.cid, 'file': None})
self.assertEqual(len(Item.objects.all()), 1)
self.assertEqual(Item.objects.all()[0].uid, 1)
self.assertEqual(Item.objects.all()[0].description, '2')
self.assertEqual(Item.objects.all()[0].container.cid, self.box.cid)
def test_update_item_with_file(self):
import base64
item = Item.objects.create(container=self.box, event=self.event, description='1')
response = client.put(f'/api/1/{self.event.slug}/item/{item.uid}',
{'description': '2',
'dataImage': "data:text/plain;base64," + base64.b64encode(b"foo").decode('utf-8')},
content_type='application/json')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()['uid'], 1)
self.assertEqual(response.json()['description'], '2')
self.assertEqual(response.json()['box'], 'BOX')
self.assertEqual(response.json()['cid'], self.box.cid)
self.assertEqual(len(response.json()['file']), 64)
self.assertEqual(len(Item.objects.all()), 1)
self.assertEqual(Item.objects.all()[0].uid, 1)
self.assertEqual(Item.objects.all()[0].description, '2')
self.assertEqual(Item.objects.all()[0].container.cid, self.box.cid)
self.assertEqual(len(File.objects.all()), 1)
def test_delete_item(self):
item = Item.objects.create(container=self.box, event=self.event, description='1')
Item.objects.create(container=self.box, event=self.event, description='2')
self.assertEqual(len(Item.objects.all()), 2)
response = client.delete(f'/api/1/{self.event.slug}/item/{item.uid}')
self.assertEqual(response.status_code, 204)
self.assertEqual(len(Item.objects.all()), 1)
def test_delete_item2(self):
Item.objects.create(container=self.box, event=self.event, description='1')
item2 = Item.objects.create(container=self.box, event=self.event, description='2')
self.assertEqual(len(Item.objects.all()), 2)
response = client.delete(f'/api/1/{self.event.slug}/item/{item2.uid}')
self.assertEqual(response.status_code, 204)
self.assertEqual(len(Item.objects.all()), 1)
item3 = Item.objects.create(container=self.box, event=self.event, description='3')
self.assertEqual(item3.uid, 3)
self.assertEqual(len(Item.objects.all()), 2)
def test_item_count(self):
Item.objects.create(container=self.box, event=self.event, description='1')
Item.objects.create(container=self.box, event=self.event, description='2')
response = client.get('/api/1/boxes')
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 1)
self.assertEqual(response.json()[0]['itemCount'], 2)
def test_item_nonexistent(self):
response = client.get(f'/api/1/NOEVENT/item')
self.assertEqual(response.status_code, 404)

View file

@ -1,44 +0,0 @@
from django.test import TestCase, Client
from django.contrib.auth.models import Permission
from knox.models import AuthToken
from authentication.models import ExtendedUser
class ApiTest(TestCase):
def setUp(self):
super().setUp()
self.user = ExtendedUser.objects.create_user('testuser', 'test', 'test')
self.user.user_permissions.add(*Permission.objects.all())
self.user.save()
self.token = AuthToken.objects.create(user=self.user)
self.client = Client(headers={'Authorization': 'Token ' + self.token[1]})
def test_root(self):
from core.settings import SYSTEM3_VERSION
response = self.client.get('/api/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["framework_version"], SYSTEM3_VERSION)
def test_events(self):
response = self.client.get('/api/2/events/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), [])
def test_containers(self):
response = self.client.get('/api/2/boxes/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), [])
def test_files(self):
response = self.client.get('/api/2/files/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), [])
def test_items(self):
from inventory.models import Event
Event.objects.create(slug='TEST1', name='Event')
response = self.client.get('/api/2/TEST1/items/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), [])

View file

@ -1,66 +0,0 @@
from django.test import TestCase, Client
from django.contrib.auth.models import Permission
from knox.models import AuthToken
from authentication.models import ExtendedUser
from inventory.models import Container
class ContainerTestCase(TestCase):
def setUp(self):
self.user = ExtendedUser.objects.create_user('testuser', 'test', 'test')
self.user.user_permissions.add(*Permission.objects.all())
self.token = AuthToken.objects.create(user=self.user)
self.client = Client(headers={'Authorization': 'Token ' + self.token[1]})
def test_empty(self):
response = self.client.get('/api/2/boxes/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), [])
def test_members(self):
Container.objects.create(name='BOX')
response = self.client.get('/api/2/boxes/')
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 1)
self.assertEqual(response.json()[0]['cid'], 1)
self.assertEqual(response.json()[0]['name'], 'BOX')
self.assertEqual(response.json()[0]['itemCount'], 0)
def test_multi_members(self):
Container.objects.create(name='BOX 1')
Container.objects.create(name='BOX 2')
Container.objects.create(name='BOX 3')
response = self.client.get('/api/2/boxes/')
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 3)
def test_create_container(self):
response = self.client.post('/api/2/box/', {'name': 'BOX'})
self.assertEqual(response.status_code, 201)
self.assertEqual(response.json()['cid'], 1)
self.assertEqual(response.json()['name'], 'BOX')
self.assertEqual(response.json()['itemCount'], 0)
self.assertEqual(len(Container.objects.all()), 1)
self.assertEqual(Container.objects.all()[0].cid, 1)
self.assertEqual(Container.objects.all()[0].name, 'BOX')
def test_update_container(self):
box = Container.objects.create(name='BOX 1')
response = self.client.put(f'/api/2/box/{box.cid}/', {'name': 'BOX 2'}, content_type='application/json')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()['cid'], 1)
self.assertEqual(response.json()['name'], 'BOX 2')
self.assertEqual(response.json()['itemCount'], 0)
self.assertEqual(len(Container.objects.all()), 1)
self.assertEqual(Container.objects.all()[0].cid, 1)
self.assertEqual(Container.objects.all()[0].name, 'BOX 2')
def test_delete_container(self):
box = Container.objects.create(name='BOX 1')
Container.objects.create(name='BOX 2')
self.assertEqual(len(Container.objects.all()), 2)
response = self.client.delete(f'/api/2/box/{box.cid}/')
self.assertEqual(response.status_code, 204)
self.assertEqual(len(Container.objects.all()), 1)

View file

@ -1,56 +0,0 @@
from django.test import TestCase, Client
from inventory.models import Event
client = Client()
class EventTestCase(TestCase):
def test_empty(self):
response = client.get('/api/2/events/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), [])
def test_members(self):
Event.objects.create(slug='EVENT', name='Event')
response = client.get('/api/2/events/')
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 1)
self.assertEqual(response.json()[0]['slug'], 'EVENT')
self.assertEqual(response.json()[0]['name'], 'Event')
def test_multi_members(self):
Event.objects.create(slug='EVENT1', name='Event 1')
Event.objects.create(slug='EVENT2', name='Event 2')
Event.objects.create(slug='EVENT3', name='Event 3')
response = client.get('/api/2/events/')
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 3)
def test_create_event(self):
response = client.post('/api/2/events/', {'slug': 'EVENT', 'name': 'Event'})
self.assertEqual(response.status_code, 201)
self.assertEqual(response.json()['slug'], 'EVENT')
self.assertEqual(response.json()['name'], 'Event')
self.assertEqual(len(Event.objects.all()), 1)
self.assertEqual(Event.objects.all()[0].slug, 'EVENT')
self.assertEqual(Event.objects.all()[0].name, 'Event')
def test_update_event(self):
from rest_framework.test import APIClient
event = Event.objects.create(slug='EVENT1', name='Event 1')
response = APIClient().put(f'/api/2/events/{event.eid}/', {'slug': 'EVENT2', 'name': 'Event 2 new'})
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()['slug'], 'EVENT2')
self.assertEqual(response.json()['name'], 'Event 2 new')
self.assertEqual(len(Event.objects.all()), 1)
self.assertEqual(Event.objects.all()[0].slug, 'EVENT2')
self.assertEqual(Event.objects.all()[0].name, 'Event 2 new')
def test_remove_event(self):
event = Event.objects.create(slug='EVENT1', name='Event 1')
Event.objects.create(slug='EVENT2', name='Event 2')
self.assertEqual(len(Event.objects.all()), 2)
response = client.delete(f'/api/2/events/{event.eid}/')
self.assertEqual(response.status_code, 204)
self.assertEqual(len(Event.objects.all()), 1)

View file

@ -1,172 +0,0 @@
from datetime import datetime
from django.test import TestCase, Client
from django.contrib.auth.models import Permission
from knox.models import AuthToken
from authentication.models import ExtendedUser
from files.models import File
from inventory.models import Event, Container, Item
class ItemTestCase(TestCase):
def setUp(self):
super().setUp()
self.event = Event.objects.create(slug='EVENT', name='Event')
self.box = Container.objects.create(name='BOX')
self.user = ExtendedUser.objects.create_user('testuser', 'test', 'test')
self.user.user_permissions.add(*Permission.objects.all())
self.token = AuthToken.objects.create(user=self.user)
self.client = Client(headers={'Authorization': 'Token ' + self.token[1]})
def test_empty(self):
response = self.client.get(f'/api/2/{self.event.slug}/item/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content, b'[]')
def test_members(self):
item = Item.objects.create(container=self.box, event=self.event, description='1')
response = self.client.get(f'/api/2/{self.event.slug}/item/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(),
[{'uid': 1, 'description': '1', 'box': 'BOX', 'cid': self.box.cid, 'file': None,
'returned': False}])
def test_members_with_file(self):
import base64
item = Item.objects.create(container=self.box, event=self.event, description='1')
file = File.objects.create(item=item, data="data:text/plain;base64," + base64.b64encode(b"foo").decode('utf-8'))
response = self.client.get(f'/api/2/{self.event.slug}/item/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(),
[{'uid': 1, 'description': '1', 'box': 'BOX', 'cid': self.box.cid, 'file': file.hash,
'returned': False}])
def test_multi_members(self):
Item.objects.create(container=self.box, event=self.event, description='1')
Item.objects.create(container=self.box, event=self.event, description='2')
Item.objects.create(container=self.box, event=self.event, description='3')
response = self.client.get(f'/api/2/{self.event.slug}/item/')
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 3)
def test_create_item(self):
response = self.client.post(f'/api/2/{self.event.slug}/item/', {'cid': self.box.cid, 'description': '1'})
self.assertEqual(response.status_code, 201)
self.assertEqual(response.json(),
{'uid': 1, 'description': '1', 'box': 'BOX', 'cid': self.box.cid, 'file': None,
'returned': False})
self.assertEqual(len(Item.objects.all()), 1)
self.assertEqual(Item.objects.all()[0].uid, 1)
self.assertEqual(Item.objects.all()[0].description, '1')
self.assertEqual(Item.objects.all()[0].container.cid, self.box.cid)
def test_create_item_with_file(self):
import base64
response = self.client.post(f'/api/2/{self.event.slug}/item/',
{'cid': self.box.cid, 'description': '1',
'dataImage': "data:text/plain;base64," + base64.b64encode(b"foo").decode(
'utf-8')}, content_type='application/json')
self.assertEqual(response.status_code, 201)
self.assertEqual(response.json()['uid'], 1)
self.assertEqual(response.json()['description'], '1')
self.assertEqual(response.json()['box'], 'BOX')
self.assertEqual(response.json()['cid'], self.box.cid)
self.assertEqual(len(response.json()['file']), 64)
self.assertEqual(len(Item.objects.all()), 1)
self.assertEqual(Item.objects.all()[0].uid, 1)
self.assertEqual(Item.objects.all()[0].description, '1')
self.assertEqual(Item.objects.all()[0].container.cid, self.box.cid)
self.assertEqual(len(File.objects.all()), 1)
def test_update_item(self):
item = Item.objects.create(container=self.box, event=self.event, description='1')
response = self.client.put(f'/api/2/{self.event.slug}/item/{item.uid}/', {'description': '2'},
content_type='application/json')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(),
{'uid': 1, 'description': '2', 'box': 'BOX', 'cid': self.box.cid, 'file': None,
'returned': False})
self.assertEqual(len(Item.objects.all()), 1)
self.assertEqual(Item.objects.all()[0].uid, 1)
self.assertEqual(Item.objects.all()[0].description, '2')
self.assertEqual(Item.objects.all()[0].container.cid, self.box.cid)
def test_update_item_with_file(self):
import base64
item = Item.objects.create(container=self.box, event=self.event, description='1')
response = self.client.put(f'/api/2/{self.event.slug}/item/{item.uid}/',
{'description': '2',
'dataImage': "data:text/plain;base64," + base64.b64encode(b"foo").decode('utf-8')},
content_type='application/json')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()['uid'], 1)
self.assertEqual(response.json()['description'], '2')
self.assertEqual(response.json()['box'], 'BOX')
self.assertEqual(response.json()['cid'], self.box.cid)
self.assertEqual(len(response.json()['file']), 64)
self.assertEqual(len(Item.objects.all()), 1)
self.assertEqual(Item.objects.all()[0].uid, 1)
self.assertEqual(Item.objects.all()[0].description, '2')
self.assertEqual(Item.objects.all()[0].container.cid, self.box.cid)
self.assertEqual(len(File.objects.all()), 1)
def test_delete_item(self):
item = Item.objects.create(container=self.box, event=self.event, description='1')
Item.objects.create(container=self.box, event=self.event, description='2')
self.assertEqual(len(Item.objects.all()), 2)
response = self.client.delete(f'/api/2/{self.event.slug}/item/{item.uid}/')
self.assertEqual(response.status_code, 204)
self.assertEqual(len(Item.objects.all()), 1)
def test_delete_item2(self):
Item.objects.create(container=self.box, event=self.event, description='1')
item2 = Item.objects.create(container=self.box, event=self.event, description='2')
self.assertEqual(len(Item.objects.all()), 2)
response = self.client.delete(f'/api/2/{self.event.slug}/item/{item2.uid}/')
self.assertEqual(response.status_code, 204)
self.assertEqual(len(Item.objects.all()), 1)
item3 = Item.objects.create(container=self.box, event=self.event, description='3')
self.assertEqual(item3.uid, 3)
self.assertEqual(len(Item.objects.all()), 2)
def test_item_count(self):
Item.objects.create(container=self.box, event=self.event, description='1')
Item.objects.create(container=self.box, event=self.event, description='2')
response = self.client.get('/api/2/boxes/')
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 1)
self.assertEqual(response.json()[0]['itemCount'], 2)
def test_item_nonexistent(self):
response = self.client.get(f'/api/2/NOEVENT/item/')
self.assertEqual(response.status_code, 404)
def test_item_return(self):
item = Item.objects.create(container=self.box, event=self.event, description='1')
self.assertEqual(item.returned_at, None)
response = self.client.get(f'/api/2/{self.event.slug}/item/')
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 1)
response = self.client.patch(f'/api/2/{self.event.slug}/item/{item.uid}/', {'returned': True},
content_type='application/json')
self.assertEqual(response.status_code, 200)
item.refresh_from_db()
self.assertNotEqual(item.returned_at, None)
response = self.client.get(f'/api/2/{self.event.slug}/item/')
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 0)
def test_item_show_not_returned(self):
item1 = Item.objects.create(container=self.box, event=self.event, description='1')
item2 = Item.objects.create(container=self.box, event=self.event, description='2')
response = self.client.get(f'/api/2/{self.event.slug}/item/')
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 2)
item2.returned_at = datetime.now()
item2.save()
response = self.client.get(f'/api/2/{self.event.slug}/item/')
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 1)
self.assertEqual(response.json()[0]['uid'], item1.uid)

View file

View file

@ -1,17 +0,0 @@
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)

View file

@ -1,26 +0,0 @@
from rest_framework import routers, viewsets, serializers
from mail.models import Email, EmailAttachment
class AttachmentSerializer(serializers.ModelSerializer):
class Meta:
model = EmailAttachment
fields = ['hash', 'mime_type', 'name']
class EmailSerializer(serializers.ModelSerializer):
class Meta:
model = Email
fields = '__all__'
class EmailViewSet(viewsets.ModelViewSet):
serializer_class = EmailSerializer
queryset = Email.objects.all()
router = routers.SimpleRouter()
router.register(r'mails', EmailViewSet, basename='mails')
urlpatterns = router.urls

View file

@ -1,46 +0,0 @@
# Generated by Django 4.2.7 on 2023-12-09 02:13
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('inventory', '0001_initial'),
('tickets', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='EventAddress',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('address', models.CharField(max_length=255)),
('event', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='inventory.event')),
],
),
migrations.CreateModel(
name='Email',
fields=[
('is_deleted', models.BooleanField(default=False)),
('deleted_at', models.DateTimeField(blank=True, null=True)),
('id', models.AutoField(primary_key=True, serialize=False)),
('timestamp', models.DateTimeField(auto_now_add=True)),
('body', models.TextField()),
('subject', models.CharField(max_length=255)),
('sender', models.CharField(max_length=255)),
('recipient', models.CharField(max_length=255)),
('reference', models.CharField(max_length=255, null=True, unique=True)),
('in_reply_to', models.CharField(max_length=255, null=True)),
('raw', models.TextField()),
('event', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='inventory.event')),
('issue_thread', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='emails', to='tickets.issuethread')),
],
options={
'abstract': False,
},
),
]

View file

@ -1,32 +0,0 @@
# Generated by Django 4.2.7 on 2023-12-09 02:13
import quopri
from django.db import migrations
from mail.protocol import unescape_and_decode_quoted_printable, unescape_and_decode_base64
class Migration(migrations.Migration):
initial = True
dependencies = [
('mail', '0001_initial'),
]
def convert_printed_quotable(apps, schema_editor):
Email = apps.get_model('mail', 'Email')
for mail in Email.objects.all():
mail.body = unescape_and_decode_quoted_printable(mail.body)
mail.body = unescape_and_decode_base64(mail.body)
mail.subject = unescape_and_decode_quoted_printable(mail.subject)
mail.subject = unescape_and_decode_base64(mail.subject)
mail.save()
IssueThread = apps.get_model('tickets', 'IssueThread')
for issue in IssueThread.objects.all():
issue.name = unescape_and_decode_quoted_printable(issue.name)
issue.name = unescape_and_decode_base64(issue.name)
issue.save()
operations = [
migrations.RunPython(convert_printed_quotable),
]

Some files were not shown because too many files have changed in this diff Show more