47 Commits
0.1.0 ... 0.5.0

Author SHA1 Message Date
bca6020ab0 Add ability to edit forms and add new fields
This commit adds the ability to edit and modify existing forms.
On the technical side, it also adds a new reusable validation Facade which helps reduce duplicated code.
2020-07-15 06:48:49 +01:00
1f50faaea7 Add ability to preview application 2020-07-12 19:36:12 +01:00
e978a5417b Added ability to delete single application
Also moved User observer code to Application observer
2020-07-12 17:01:33 +01:00
4dc412e53c Added check for constrained models when deleting 2020-07-12 06:39:39 +01:00
bd0664ce0d Add ability to edit Vacancies 2020-07-11 20:34:26 +01:00
4b390ea536 Added full Vacancy description
Also added support for Markdown
2020-07-11 05:34:12 +01:00
035c9399a6 Add "All Applications" page 2020-07-11 02:43:59 +01:00
bbe3001157 Example env version update 2020-07-10 05:30:22 +01:00
d1f4be124c Update wrong key name 2020-07-10 05:26:40 +01:00
0f126886f5 Use package for .env editing
The solution provided by this package is similar to what was previously implemented (see #1), but it's nicely packaged and made available in a Facade.
2020-07-10 05:00:41 +01:00
4deb882d23 Add check for null env and attempt to recover 2020-07-09 09:07:49 +01:00
635f8593d3 Update outdated information 2020-07-09 08:53:56 +01:00
c3a6b16a13 Clear configuration cache before editing it 2020-07-09 08:50:54 +01:00
fb4d832489 Beautified the search and replace function 2020-07-09 08:46:45 +01:00
4456d19c1f Clear cache and refresh *before* running migration 2020-07-09 07:12:15 +01:00
c7fd8989f9 More command improvements 2020-07-09 07:07:28 +01:00
fb422c1ef7 Merge remote-tracking branch 'origin-github/master' 2020-07-09 07:04:20 +01:00
a42df354c7 Command improvements
Remove process args


Silence npm WARN output (when applicable)


Compat for npm in different directories


Reverse npm silence


Replace Symfony Process


Remove Symfony Process refs


Add forgotten output for commands


Removed colons that artisan already added


Remove optional parameters in install cmd


Simplify settings save call


Further simplify settings save call
2020-07-09 07:04:04 +01:00
173dc57aa7 Further simplify settings save call 2020-07-09 06:27:21 +01:00
cb2b45b55f Simplify settings save call 2020-07-09 06:22:43 +01:00
a3e727f1f1 Remove optional parameters in install cmd 2020-07-09 06:14:07 +01:00
669c3c87e6 Removed colons that artisan already added 2020-07-09 06:01:10 +01:00
96298cd38c Add forgotten output for commands 2020-07-09 05:55:23 +01:00
00f37d3f7e Remove Symfony Process refs 2020-07-09 05:53:00 +01:00
653641f4e8 Replace Symfony Process 2020-07-09 05:52:41 +01:00
d121119706 Reverse npm silence 2020-07-09 05:37:11 +01:00
1c08cd9057 Compat for npm in different directories 2020-07-09 05:29:55 +01:00
1bc63725c9 Silence npm WARN output (when applicable) 2020-07-09 05:27:33 +01:00
c7caf3a67c Remove process args 2020-07-09 05:23:46 +01:00
7c7d4a306c Separate process args 2020-07-09 05:20:16 +01:00
016ce1fbf6 Remove invalid reference to Artisan 2020-07-09 05:12:18 +01:00
f673ef8222 Add installation bootstrap script 2020-07-09 05:10:22 +01:00
2bf8c8e90c Enable maintenance mode during installation 2020-07-09 05:06:25 +01:00
d111864bea Add depedency install progress bar 2020-07-09 05:01:50 +01:00
4dbf86a328 Add installation command
Adds an installation and .env modification commands for better usability
2020-07-09 04:52:53 +01:00
291c98cd0e Merge branch 'master' of github.com:spacejewel-hosting/staffmanagement 2020-07-08 08:15:01 +01:00
119ea1d8ec Updated config defaults 2020-07-08 08:14:36 +01:00
0282a6a7c9 Update issue templates 2020-07-08 07:58:18 +01:00
8ed2eb5027 Update README.md 2020-07-08 07:50:14 +01:00
vagrant
61034ccae9 Added simple installation script 2020-07-08 06:02:32 +00:00
ed95f02e00 Fix error where exception appeared instead of error message
This commit fixes an issue with fake MC usernames being used and 
resulting in a fatal exception. Displays an error msg now.
2020-06-28 04:51:32 +01:00
c58c46eda8 Fix error where wrong method is called 2020-06-28 01:30:34 +01:00
91c408fb4f Trim config results to prevent error in case of extra spaces 2020-06-28 00:50:28 +01:00
599d742e96 Use UUID conversion facade 2020-06-28 00:46:49 +01:00
50692a8abf Remove card 2020-06-27 23:46:03 +01:00
664bdcf45e Improve hompeage 2020-06-27 23:44:07 +01:00
9ec2f9edce Update .env.example to reflect new config values 2020-06-27 22:22:33 +01:00
63 changed files with 2122 additions and 12305 deletions

View File

@@ -21,6 +21,18 @@ RECAPTCHA_VERIFY_URL="https://www.google.com/recaptcha/api/siteverify"
IPGEO_API_KEY=""
IPGEO_API_URL=""
MOJANG_STATUS_URL="https://status.mojang.com/check"
MOJANG_API_URL="https://api.mojang.com"
IPGEO_API_KEY=""
IPGEO_API_URL="https://api.ipgeolocation.io/ipgeo"
ARCANEDEV_LOGVIEWER_MIDDLEWARE=web,auth,can:admin.maintenance.logs.view
RELEASE=staffmanagement@0.2.0
SLACK_INTEGRATION_WEBHOOK=
BROADCAST_DRIVER=log
CACHE_DRIVER=file
QUEUE_CONNECTION=sync

42
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,42 @@
---
name: Bug report
about: Create a report to help us improve
title: "[BUG] - Short description of what went wrong"
labels: bug, help wanted
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**Steps to reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**PHP Version: **
**Additional context**
Add any other context about the problem here.
**Log files: attach any log files from the storage/logs directory**.

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

1
.gitignore vendored
View File

@@ -4,6 +4,7 @@
/public/storage
/storage/*.key
/vendor
/tools
.env
.env.backup
.phpunit.result.cache

5
.phive/phars.xml Normal file
View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<phive xmlns="https://phar.io/phive">
<phar name="phpunit" version="^9.2.5" installed="9.2.5" location="./tools/phpunit" copy="false"/>
<phar name="php-cs-fixer" version="^2.16.4" installed="2.16.4" location="./tools/php-cs-fixer" copy="false"/>
</phive>

116
README.md
View File

@@ -1,79 +1,75 @@
<p align="center"><img src="https://res.cloudinary.com/dtfbvvkyp/image/upload/v1566331377/laravel-logolockup-cmyk-red.svg" width="400"></p>
# Raspberry Teams - The Simple Staff Application Manager v 0.1.0
## The quick and pain-free staff application manager (for Minecraft)
<p align="center">
<a href="https://travis-ci.org/laravel/framework"><img src="https://travis-ci.org/laravel/framework.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://poser.pugx.org/laravel/framework/d/total.svg" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://poser.pugx.org/laravel/framework/v/stable.svg" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://poser.pugx.org/laravel/framework/license.svg" alt="License"></a>
</p>
Have you ever gotten tired of managing your Minecraft server/network's applications through Discord (or anything else) and having to scroll through hundreds of new messages just to find that one applicant's username?
## About Laravel
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
Wish you had a better application managemet strategy? Well, then Raspberry Teams is for you! It was originally designed and developed for internal use, but sharing is caring! After noticing a worrying lack of "human resources" management systems on SpigotMC's resources section (There was only one outdated/unsupported project), I've decided to take it up into my own terms and start working on it.
- [Simple, fast routing engine](https://laravel.com/docs/routing).
- [Powerful dependency injection container](https://laravel.com/docs/container).
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
- [Robust background job processing](https://laravel.com/docs/queues).
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
Laravel is accessible, powerful, and provides tools required for large, robust applications.
# Features (not exhaustive)
- Beautiful (customizable in future releases) landing page for your application management center; It displays all available staff ranks
- Contact form on landing page for those un-registerd users
- User registration/authentication system; Users will be sent to the authentication flow to complete their application, if not logged in
- Candidate tracking system - Applicants will be tracked from start to finish.
- Peer approval system - Have all your staff members vote on applications and decide whether they should be accepted (this is overridable)
- Interview scheduling (simple) - Schedule interviews with your candidates and automatically notify them!
- Interview notes: Every staff member is able to add and edit interview notes (how the interview went, etc)
- Application comments: Finally no more having to go to a private Discord channel just to comment on a single application. Comments are organised neatly for every application! This should help in the decision process of voting for an application.
- User profiles - Fill out your profile for others to better find you
- User directory - Public profile directory for everyone
- Staff rank management - Add/remove ranks on demand, that users will be able to apply to
- Simple form builder - Create your application forms easily!
- Termination - Has a staff member met their untimely demise? Terminate them. This will strip their permissions and roles.
- Controllable permissions - Every user has permissions! Control who has access to what (You can skip the application process and add staff members directly here).
- Ban system - Having trouble with pesky spammers? Ban them! This will publicly shame their profile and keep them from signing up or logging in.
- Notifications: Notifies slack and email primarily
## Learning Laravel
And many more features!
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework.
# Roadmap
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains over 1500 video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
Many other features are currently planned for this app, such as:
- Discord role management (approved applicants)
- Luckperms/PEX integration - For now, you'll have to promote users manually in-game
- Flexibility - This app is built on a flexible concept! It will be able to be used for other purposes other than MC staff members.
- Customisable front page (**priority**)
- Auto provisioning - Sign up on a website and get your instance of Raspberry Teams up and running in no time
- Suggestions accepted!
## Laravel Sponsors
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the Laravel [Patreon page](https://patreon.com/taylorotwell).
# Technical overview
- **[Vehikl](https://vehikl.com/)**
- **[Tighten Co.](https://tighten.co)**
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
- **[64 Robots](https://64robots.com)**
- **[Cubet Techno Labs](https://cubettech.com)**
- **[Cyber-Duck](https://cyber-duck.co.uk)**
- **[British Software Development](https://www.britishsoftware.co)**
- **[Webdock, Fast VPS Hosting](https://www.webdock.io/en)**
- **[DevSquad](https://devsquad.com)**
- [UserInsights](https://userinsights.com)
- [Fragrantica](https://www.fragrantica.com)
- [SOFTonSOFA](https://softonsofa.com/)
- [User10](https://user10.com)
- [Soumettre.fr](https://soumettre.fr/)
- [CodeBrisk](https://codebrisk.com)
- [1Forge](https://1forge.com)
- [TECPRESSO](https://tecpresso.co.jp/)
- [Runtime Converter](http://runtimeconverter.com/)
- [WebL'Agence](https://weblagence.com/)
- [Invoice Ninja](https://www.invoiceninja.com)
- [iMi digital](https://www.imi-digital.de/)
- [Earthlink](https://www.earthlink.ro/)
- [Steadfast Collective](https://steadfastcollective.com/)
- [We Are The Robots Inc.](https://watr.mx/)
- [Understand.io](https://www.understand.io/)
- [Abdel Elrafa](https://abdelelrafa.com)
- [Hyper Host](https://hyper.host)
- [Appoly](https://www.appoly.co.uk)
- [OP.GG](https://op.gg)
- [云软科技](http://www.yunruan.ltd/)
Tech stack:
- [Laravel 7](https://laravel.com/)
- Eloquent ORM
- AdminLTE / Bootstrap 4
- jQuery / Plain Javascript
- vueJS (in the future)
## Contributing
# Operating System Requirements
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
Currently, this application is only supported on Linux environments (Ubuntu 20.04 or derivatives are recommended).
## Code of Conduct
# Software Requirements
- ``composer`` (min version: 1.8.4)
- ``npm`` (tested w/ v 5.8.0)
- ``php`` (required PHP 7 or newer - lower versions unsupported!)
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
# PHP Extension Requirements
## Security Vulnerabilities
- JSON
- Curl (highly recommended)
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
## License
# Installation
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
Make sure all prerequisites are installed. Afterwards, clone this repository, make ``install.sh``executable and run it.
# Configuration
Configuration is currently done via the installer. Alternatively, you may also edit the ``.env`` file directly.
This process will be moved to the browser later.
# Bug reports
Please report any bugs you find to the issues section here! It'd be immensely helpful. PRs are also accepted.

View File

@@ -0,0 +1,137 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
class Install extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'application:install {-u|--unattended: Install non-interactively (currently unused: WIP)}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Installs the application and prepares for production use.';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$basePath = base_path();
if (Storage::disk('local')->missing('INSTALLED'))
{
$this->info('[!! Welcome to Rasberry Teams !!]');
$this->info('>> Installing...');
$this->call('down', [
'--message' => 'Down for maintenance. We\'ll be right back!'
]);
copy($basePath . '/.env.example', $basePath . '/.env');
$this->call('key:generate');
$this->info('>> Installing and preparing dependencies. This may take a while, depending on your computer.');
$npmOut = 0;
$npmMessages = [];
$npmBuildOut = 0;
$npmBuildMessages = [];
exec('cd ' . $basePath . ' && npm install --silent', $npmBuildOut, $npmOut);
exec('cd ' . $basePath . '&& npm run dev --silent', $npmBuildMessages, $npmBuildOut);
if($npmOut !== 0 && $npmBuildOut !== 0)
{
$this->error('[!] One or more errors have ocurred whilst attempting to install dependencies.');
$this->error('[!] It is recommended to run this command again, and report a bug if it keeps happening.');
return false;
}
$settings = [];
$this->info('>> Configuring application - We\'re going to ask a few questions here!');
do
{
$this->info('== Database Settings (1/6) ==');
$settings['DB_USERNAME'] = $this->ask('Database username');
$settings['DB_PASSWORD'] = $this->secret('Database password (Input won\'t be seen)');
$settings['DB_DATABASE'] = $this->ask('Database name');
$settings['DB_PORT'] = $this->ask('Database port');
$settings['DB_HOST'] = $this->ask('Database hostname');
$this->info('== Antispam Settings (2/6) (Recaptcha v2) ==');
$settings['RECAPTCHA_SITE_KEY'] = $this->ask('Site key');
$settings['RECAPTCHA_PRIVATE_KEY'] = $this->ask('Private site key');
$this->info('== IP Geolocation Settings (3/6) (refer to README.md) ==');
$settings['IPGEO_API_KEY'] = $this->ask('API Key');
$this->info('== Notification Settings (4/6) (Email) ==');
$settings['MAIL_USERNAME'] = $this->ask('SMTP Username');
$settings['MAIL_PASSWORD'] = $this->secret('SMTP Password (Input won\'t be seen)');
$settings['MAIL_PORT'] = $this->ask('SMTP Server Port');
$settings['MAIL_HOST'] = $this->ask('SMTP Server Hostname');
$this->info('== Notification Settings (5/6) (Slack) ==');
$settings['SLACK_INTEGRATION_WEBHOOK'] = $this->ask('Integration webhook URL');
$this->info('== Web Settings (6/6) ==');
$settings['APP_URL'] = $this->ask('Application\'s URL');
} while(!$this->confirm('Are you sure you want to save these settings? You can always go back and try again.'));
foreach($settings as $keyname => $value)
{
$this->call('environment:modify', [
'key' => $keyname,
'value' => $value
]);
}
$this->info('>> Saved configuration settings!');
$this->info('>> Preparing database...');
$this->callSilent('config:cache');
$this->call('migrate');
$this->call('db:seed');
touch($basePath . '/INSTALLED');
$this->call('up');
$this->info('>> All done! Visit ' . $basePath . ' to start using your brand new installation of Raspberry Teams!');
}
else
{
$this->error('[!] The application is already installed!');
}
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use GeoSot\EnvEditor\Facades\EnvEditor;
class SetEnv extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'environment:modify {key : Key name} {value : New value}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Permanently modifies an environment variable on the .env file for later use.';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
* @return mixed
*/
public function handle()
{
$path = base_path('/.env');
$key = $this->argument('key');
$value = $this->argument('value');
if (file_exists($path))
{
EnvEditor::editKey($key, $value);
}
else
{
$this->error('Cannot update a file that doesn\'t exist! Please create .env first.');
return false;
}
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace App\Facades;
use Illuminate\Support\Facades\Facade;
class ContextAwareValidation extends Facade
{
protected static function getFacadeAccessor()
{
return 'contextAwareValidator';
}
}

View File

@@ -16,7 +16,7 @@ class Form extends Model
public function vacancies()
{
return $this->hasMany('vacancies', 'vacancyFormID', 'id');
return $this->hasMany('App\Vacancy', 'vacancyFormID', 'id');
}
public function responses()

View File

@@ -0,0 +1,72 @@
<?php
namespace App\Helpers;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Collection;
class ContextAwareValidator
{
/**
* The getValidator() method will take an array of fields from the request body, iterates through them,
* and dynamically adds validation rules for them. Depending on parameters, it may or may not generate
* a form structure for rendering purposes.
*
* This method is mostly meant by internal use by means of static proxies (Facades), in order to reduce code repetition;
* Using it outside it's directed scope may cause unexpected results; For instance, the method expects inputs to be in array format, e.g. myFieldNameID1[],
* myFieldNameID2[], and so on and so forth.
*
* This isn't checked by the code yet, but if you're implementing it this way in the HTML markup, make sure it's consistent (e.g. use a loop).
*
* P.S This method automatically ignores the CSRF token for validation.
*
* @param array $fields The request form fields
* @param bool $generateStructure Whether to incldue a JSON-ready form structure for rendering
* @param bool $includeFormName Whether to include formName in the list of validation rules
* @return Validator|Collection A validator instance you can use to check for validity, or a Collection with a validator and structure (validator, structure)
*/
public function getValidator(array $fields, bool $generateStructure = false, bool $includeFormName = false)
{
$formStructure = [];
$validator = [];
$excludedNames = [
'_token',
'_method',
'formName'
];
if ($includeFormName)
{
$validator['formName'] = 'required|string|max:100';
}
foreach ($fields as $fieldName => $field)
{
if(!in_array($fieldName, $excludedNames))
{
$validator[$fieldName . ".0"] = 'required|string';
$validator[$fieldName . ".1"] = 'required|string';
if ($generateStructure)
{
$formStructure['fields'][$fieldName]['title'] = $field[0];
$formStructure['fields'][$fieldName]['type'] = $field[1];
}
}
}
$validatorInstance = Validator::make($fields, $validator);
return ($generateStructure) ?
collect([
'validator' => $validatorInstance,
'structure' => json_encode($formStructure)
])
: $validatorInstance;
}
}

View File

@@ -45,10 +45,9 @@ class ApplicationController extends Controller
}
public function showUserApp(Request $request, $applicationID)
{
// TODO: Inject it instead (do this where there is no injection, not just here)
$application = Application::find($applicationID);
$this->authorize('view', $application);
@@ -77,6 +76,12 @@ class ApplicationController extends Controller
public function showAllApps()
{
return view('dashboard.appmanagement.all')
->with('applications', Application::paginate(6));
}
public function showAllPendingApps()
{
@@ -87,9 +92,6 @@ class ApplicationController extends Controller
}
public function showPendingInterview()
{
$this->authorize('viewAny', Application::class);
@@ -280,4 +282,16 @@ class ApplicationController extends Controller
return redirect()->back();
}
public function delete(Request $request, Application $application)
{
$this->authorize('delete', $application);
$application->delete(); // observers will run, cleaning it up
$request->session()->flash('success', 'Application deleted. Comments, appointments and responses have also been deleted.');
return redirect()->back();
}
}

View File

@@ -7,6 +7,8 @@ use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Auth;
use ContextAwareValidator;
class FormController extends Controller
{
@@ -29,39 +31,17 @@ class FormController extends Controller
{
$this->authorize('create', Form::class);
$fields = $request->all();
$formFields = $request->all();
$contextValidation = ContextAwareValidator::getValidator($fields, true, true);
$formStructure = [];
$excludedNames = [
'_token',
'formName' // It's added outside the loop. Not excluding causes unwanted duplication.
];
$validator = [
'formName' => 'required|string|max:100'
];
foreach ($formFields as $fieldName => $field)
if (!$contextValidation->get('validator')->fails())
{
if(!in_array($fieldName, $excludedNames))
{
$validator[$fieldName . ".0"] = 'required|string';
$validator[$fieldName . ".1"] = 'required|string';
$formStructure['fields'][$fieldName]['title'] = $field[0];
$formStructure['fields'][$fieldName]['type'] = $field[1];
}
}
$validation = Validator::make($formFields, $validator);
if (!$validation->fails())
{
$storableFormStructure = json_encode($formStructure);
$storableFormStructure = $contextValidation->get('structure');
Form::create(
[
'formName' => $formFields['formName'],
'formName' => $fields['formName'],
'formStructure' => $storableFormStructure,
'formStatus' => 'ACTIVE'
]
@@ -71,7 +51,7 @@ class FormController extends Controller
return redirect()->to(route('showForms'));
}
$request->session()->flash('errors', $validation->errors()->getMessages());
$request->session()->flash('errors', $contextValidation->get('validator')->errors()->getMessages());
return redirect()->back();
}
@@ -80,19 +60,68 @@ class FormController extends Controller
$form = Form::find($id);
$this->authorize('delete', $form);
$deletable = true;
// TODO: Check if form is linked to vacancies before allowing deletion
if (!is_null($form))
if (!is_null($form) && !is_null($form->vacancies) && $form->vacancies->count() !== 0 || !is_null($form->responses))
{
$form->delete();
$request->session()->flash('success', 'Form deleted successfully.');
return redirect()->back();
$deletable = false;
}
if ($deletable)
{
$form->delete();
$request->session()->flash('success', 'Form deleted successfully.');
}
else
{
$request->session()->flash('error', 'You cannot delete this form because it\'s tied to one or more applications and ranks, or because it doesn\'t exist.');
}
$request->session()->flash('error', 'The form you\'re trying to delete does not exist.');
return redirect()->back();
}
public function preview(Request $request, Form $form)
{
return view('dashboard.administration.formpreview')
->with('form', json_decode($form->formStructure, true))
->with('title', $form->formName)
->with('formID', $form->id);
}
public function edit(Request $request, Form $form)
{
return view('dashboard.administration.editform')
->with('formStructure', json_decode($form->formStructure, true))
->with('title', $form->formName)
->with('formID', $form->id);
}
public function update(Request $request, Form $form)
{
$contextValidation = ContextAwareValidator::getValidator($request->all(), true);
$this->authorize('update', $form);
if (!$contextValidation->get('validator')->fails())
{
// Add the new structure into the form. New, subsquent fields will be identified by the "new" prefix
// This prefix doesn't actually change the app's behavior when it receives applications.
// Additionally, old applications won't of course display new and updated fields, because we can't travel into the past and get data for them
$form->formStructure = $contextValidation->get('structure');
$form->save();
$request->session()->flash('success', 'Hooray! Your form was updated. New applications for it\'s vacancy will use it.');
}
else
{
$request->session()->flash('errors', $contextValidation->get('validator')->errors()->getMessages());
}
return redirect()->to(route('previewForm', ['form' => $form->id]));
}
}

View File

@@ -15,15 +15,12 @@ class HomeController extends Controller
*/
public function index()
{
// TODO: Relationships for Applications, Users and Responses
// Also prevent apps if user already has one in the space of 30d
// Display apps in the relevant menus
$positions = DB::table('vacancies')
->where('vacancyStatus', 'OPEN')
->where('vacancyCount', '!=', 0)
->get();
$positions = Vacancy::where('vacancyStatus', 'OPEN')
->where('vacancyCount', '<>', 0)
->get();
return view('home')
->with('positions', $positions);
}

View File

@@ -3,15 +3,19 @@
namespace App\Http\Controllers;
use App\Http\Requests\VacancyRequest;
use App\Http\Requests\VacancyEditRequest;
use App\Vacancy;
use App\User;
use App\Form;
use App\Notifications\VacancyClosed;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Auth;
class VacancyController extends Controller
{
public function index()
@@ -31,10 +35,16 @@ class VacancyController extends Controller
if (!is_null($form))
{
/* note: since we can't convert HTML back to Markdown, we'll have to do the converting when the user requests a page,
* and leave the database with Markdown only so it can be used and edited everywhere.
* for several vacancies, this would require looping through all of them and replacing MD with HTML, which is obviously not the most clean solution;
* however, the Model can be configured to return MD instead of HTML on that specific field saving us from looping.
*/
Vacancy::create([
'vacancyName' => $request->vacancyName,
'vacancyDescription' => $request->vacancyDescription,
'vacancyFullDescription' => $request->vacancyFullDescription,
'vacancySlug' => Str::slug($request->vacancyName),
'permissionGroupName' => $request->permissionGroup,
'discordRoleID' => $request->discordRole,
@@ -101,4 +111,29 @@ class VacancyController extends Controller
return redirect()->back();
}
public function edit(Request $request, Vacancy $position)
{
$this->authorize('update', $vacancy);
return view('dashboard.administration.editposition')
->with('vacancy', $position);
}
public function update(VacancyEditRequest $request, Vacancy $position)
{
$this->authorize('update', $vacancy);
$position->vacancyFullDescription = $request->vacancyFullDescription;
$position->vacancyDescription = $request->vacancyDescription;
$position->vacancyCount = $request->vacancyCount;
$position->save();
$request->session()->flash('success', 'Vacancy successfully updated.');
return redirect()->back();
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Http\Middleware;
use Closure;
use App\Facades\UUID;
use Illuminate\Support\Facades\Http;
class UsernameUUID
@@ -19,13 +20,18 @@ class UsernameUUID
$input = $request->all();
if (isset($input['uuid']))
{
// TODO: Switch to custom Facade
$username = $input['uuid'];
try
{
$username = $input['uuid'];
$input['uuid'] = UUID::toUUID($username);
}
catch(\InvalidArgumentException $iae)
{
report($iae);
$conversionRequest = Http::get(config('general.urls.mojang.api') . '/users/profiles/minecraft/' . $username)->body();
$decodedConversionRequest = json_decode($conversionRequest, true);
$input['uuid'] = $decodedConversionRequest['id'];
$request->session()->flash('error', $iae->getMessage());
return redirect(route('register'));
}
$request->replace($input);
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
class VacancyEditRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return Auth::user()->can('admin.hiring.vacancy.edit');
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'vacancyDescription' => 'required|string',
'vacancyFullDescription' => 'nullable|string',
'vacancyCount' => 'required|integer|min:1'
];
}
}

View File

@@ -26,6 +26,7 @@ class VacancyRequest extends FormRequest
return [
'vacancyName' => 'required|string',
'vacancyDescription' => 'required|string',
'vacancyFullDescription' => 'nullable|string',
'permissionGroup' => 'required|string',
'discordRole' => 'required|string',
'vacancyCount' => 'required|integer',

View File

@@ -6,12 +6,13 @@ use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use Illuminate\Queue\SerializesModels;
use App\Vacancy;
class VacancyClosed extends Notification implements ShouldQueue
{
use Queueable;
use Queueable, SerializesModels;
protected $vacancy;

View File

@@ -0,0 +1,93 @@
<?php
namespace App\Observers;
use App\Application;
class ApplicationObserver
{
/**
* Handle the application "created" event.
*
* @param \App\Application $application
* @return void
*/
public function created(Application $application)
{
//
}
/**
* Handle the application "updated" event.
*
* @param \App\Application $application
* @return void
*/
public function updated(Application $application)
{
//
}
public function deleting(Application $application)
{
$application->response()->delete();
$votes = $application->votes;
foreach ($votes as $vote)
{
Log::debug('Referential integrity cleanup: Deleting and detaching vote ' . $vote->id);
$vote->application()->detach($application->id);
$vote->delete();
}
if (!is_null($application->appointment))
{
Log::debug('RIC: Deleting appointment!');
$application->appointment()->delete();
}
if (!$application->comments->isEmpty())
{
Log::debug('RIC: Deleting comments!');
foreach($application->comments as $comment)
{
$comment->delete();
}
}
// application can now be deleted
}
/**
* Handle the application "deleted" event.
*
* @param \App\Application $application
* @return void
*/
public function deleted(Application $application)
{
//
}
/**
* Handle the application "restored" event.
*
* @param \App\Application $application
* @return void
*/
public function restored(Application $application)
{
//
}
/**
* Handle the application "force deleted" event.
*
* @param \App\Application $application
* @return void
*/
public function forceDeleted(Application $application)
{
//
}
}

View File

@@ -48,30 +48,7 @@ class UserObserver
Log::debug('RIC: Now trying to delete applications and responses...');
foreach($applications as $application)
{
$application->response()->delete();
$votes = $application->votes;
foreach ($votes as $vote)
{
Log::debug('RIC: Deleting and detaching vote ' . $vote->id);
$vote->application()->detach($application->id);
$vote->delete();
}
if (!is_null($application->appointment))
{
Log::debug('RIC: Deleting appointment!');
$application->appointment()->delete();
}
if (!$application->comments->isEmpty())
{
Log::debug('RIC: Deleting comments!');
foreach($application->comments as $comment)
{
$comment->delete();
}
}
// code moved to Application observer, where it gets rid of attached elements individually
Log::debug('RIC: Deleting application ' . $application->id);
$application->delete();

View File

@@ -0,0 +1,64 @@
<?php
namespace App\Observers;
use App\Vacancy;
class VacancyObserver
{
/**
* Handle the vacancy "created" event.
*
* @param \App\Vacancy $vacancy
* @return void
*/
public function created(Vacancy $vacancy)
{
//
}
/**
* Handle the vacancy "updated" event.
*
* @param \App\Vacancy $vacancy
* @return void
*/
public function updated(Vacancy $vacancy)
{
//
}
/**
* Handle the vacancy "deleted" event.
*
* @param \App\Vacancy $vacancy
* @return void
*/
public function deleted(Vacancy $vacancy)
{
// TODO: Handle deletion of children's data
}
/**
* Handle the vacancy "restored" event.
*
* @param \App\Vacancy $vacancy
* @return void
*/
public function restored(Vacancy $vacancy)
{
//
}
/**
* Handle the vacancy "force deleted" event.
*
* @param \App\Vacancy $vacancy
* @return void
*/
public function forceDeleted(Vacancy $vacancy)
{
//
}
}

View File

@@ -45,4 +45,11 @@ class ApplicationPolicy
{
return $user->hasAnyRole('admin', 'hiringManager');
}
public function delete(User $user, Application $application)
{
return $user->hasRole('admin');
}
}

View File

@@ -45,7 +45,7 @@ class FormPolicy
*/
public function create(User $user)
{
return $this->user->can('admin.hiring.forms');
return $user->can('admin.hiring.forms');
}
/**
@@ -57,7 +57,7 @@ class FormPolicy
*/
public function update(User $user, Form $form)
{
// unused
return $user->can('admin.hiring.forms');
}
/**
@@ -69,7 +69,7 @@ class FormPolicy
*/
public function delete(User $user, Form $form)
{
return $this->user->can('admin.hiring.forms');
return $user->can('admin.hiring.forms');
}
/**

View File

@@ -53,7 +53,7 @@ class VacancyPolicy
*/
public function update(User $user, Vacancy $vacancy)
{
return $user->hasRole('admin', 'hiringManager');
return $user->hasAnyRole('admin', 'hiringManager');
}
/**

View File

@@ -6,6 +6,7 @@ use App\Observers\UserObserver;
use App\User;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\ServiceProvider;
use Sentry;
class AppServiceProvider extends ServiceProvider
{
@@ -26,6 +27,10 @@ class AppServiceProvider extends ServiceProvider
*/
public function boot()
{
Sentry\init([
'release' => env('RELEASE')
]);
Schema::defaultStringLength(191);
User::observe(UserObserver::class);
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App;
class ContextAwareValidatorProvider extends ServiceProvider
{
/**
* Register services.
*
* @return void
*/
public function register()
{
//
}
/**
* Bootstrap services.
*
* @return void
*/
public function boot()
{
App::bind('contextAwareValidator', function(){
return new App\Helpers\ContextAwareValidator();
});
}
}

View File

@@ -19,11 +19,17 @@ class UUID
throw new \LogicException('Argument username for ' . __METHOD__ . ' cannot be null!');
}
$response = json_decode(Http::post(config('general.urls.mojang.api') . '/profiles/minecraft', [
$response = json_decode(Http::post(trim(config('general.urls.mojang.api')) . '/profiles/minecraft', [
$username
])->body(), true);
return $response[0]['id'];
if (isset($response[0]))
{
return $response[0]['id'];
}
throw new \InvalidArgumentException("You must supply a valid, premium Minecraft account to sign up.");
}
// Note: Caching could simply be assigning the username to it's UUID, however, to make this work, we'd need to loop over all cache items, which would be slighly ineffective
@@ -38,7 +44,7 @@ class UUID
$shortUUID = substr($uuid, 0, 8);
$username = Cache::remember('uuid_' . $shortUUID, now()->addDays(30), function() use ($shortUUID, $uuid) {
$response = json_decode(Http::get(config('general.urls.mojang.session') . '/session/minecraft/profile/' . $uuid)->body(), true);
$response = json_decode(Http::get(trim(config('general.urls.mojang.session')) . '/session/minecraft/profile/' . $uuid)->body(), true);
Log::debug('Caching ' . $shortUUID . 'for thirty days');
return $response['name'];

View File

@@ -6,6 +6,9 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use GrahamCampbell\Markdown\Facades\Markdown;
class Vacancy extends Model
{
public $fillable = [
@@ -13,6 +16,7 @@ class Vacancy extends Model
'permissionGroupName',
'vacancyName',
'vacancyDescription',
'vacancyFullDescription',
'discordRoleID',
'vacancyFormID',
'vacancyCount',
@@ -21,6 +25,26 @@ class Vacancy extends Model
];
/**
* Get the HTML variant of the vacancyFullDescription attribute.
*
* @param string $value The original value
* @return string
*/
public function getVacancyFullDescriptionAttribute($value)
{
if (!is_null($value))
{
return Markdown::convertToHTML($value);
}
else
{
return null;
}
}
public function forms()
{
return $this->belongsTo('App\Form', 'vacancyFormID', 'id');

View File

@@ -0,0 +1,33 @@
<?php
namespace App\View\Components;
use Illuminate\View\Component;
class Form extends Component
{
public $formFields;
public $disableFields = false;
/**
* Create a new component instance.
*
* @return void
*/
public function __construct($disableFields = false)
{
$this->disableFields = $disableFields;
}
/**
* Get the view / contents that represent the component.
*
* @return \Illuminate\View\View|string
*/
public function render()
{
return view('components.form');
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App\View\Components;
use Illuminate\View\Component;
class NoPermission extends Component
{
public $type;
public $inDashboard;
/**
* Create a new component instance.
*
* @return void
*/
public function __construct($type, $inDashboard = true)
{
$this->type = $type;
$this->inDashboard = $inDashboard;
}
/**
* Get the view / contents that represent the component.
*
* @return \Illuminate\View\View|string
*/
public function render()
{
return view('components.no-permission');
}
}

View File

@@ -14,6 +14,8 @@
"doctrine/dbal": "^2.10",
"fideloper/proxy": "^4.2",
"fruitcake/laravel-cors": "^1.0",
"geo-sot/laravel-env-editor": "^0.9.9",
"graham-campbell/markdown": "^12.0",
"guzzlehttp/guzzle": "^6.5",
"jeroennoten/laravel-adminlte": "^3.2",
"laravel/framework": "^7.0",

128
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "fd14fa762c6ac8136b171abd8437e7d7",
"content-hash": "51429857899e8134bbe6b4fa61145cc3",
"packages": [
{
"name": "almasaeed2010/adminlte",
@@ -1032,6 +1032,132 @@
],
"time": "2020-04-28T08:47:37+00:00"
},
{
"name": "geo-sot/laravel-env-editor",
"version": "v0.9.9",
"source": {
"type": "git",
"url": "https://github.com/GeoSot/Laravel-EnvEditor.git",
"reference": "e828d3d3310890286d0b53045de9381187258605"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/GeoSot/Laravel-EnvEditor/zipball/e828d3d3310890286d0b53045de9381187258605",
"reference": "e828d3d3310890286d0b53045de9381187258605",
"shasum": ""
},
"require": {
"laravel/framework": "~5.5.0|~5.6.0|~5.7.0|~5.8.0|~6.0|~7.0",
"php": "^7.1"
},
"require-dev": {
"orchestra/testbench": "^3"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "0.9.*-dev"
},
"laravel": {
"providers": [
"GeoSot\\EnvEditor\\ServiceProvider"
],
"aliases": {
"EnvEditor": "GeoSot\\EnvEditor\\Facades\\EnvEditor"
}
}
},
"autoload": {
"psr-4": {
"GeoSot\\EnvEditor\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Geo Sot",
"email": "geo.sotis@gmail.com"
}
],
"description": "A laravel Package that Supports .Env File editing and backup ",
"keywords": [
"EnvEditor",
"geo-sot",
"laravel",
"laravel-env-editor"
],
"time": "2020-04-17T23:33:36+00:00"
},
{
"name": "graham-campbell/markdown",
"version": "v12.0.2",
"source": {
"type": "git",
"url": "https://github.com/GrahamCampbell/Laravel-Markdown.git",
"reference": "584eb9f24004238b80ee98b6e7be82f0933554dd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/GrahamCampbell/Laravel-Markdown/zipball/584eb9f24004238b80ee98b6e7be82f0933554dd",
"reference": "584eb9f24004238b80ee98b6e7be82f0933554dd",
"shasum": ""
},
"require": {
"illuminate/contracts": "^6.0|^7.0",
"illuminate/support": "^6.0|^7.0",
"illuminate/view": "^6.0|^7.0",
"league/commonmark": "^1.3",
"php": "^7.2.5"
},
"require-dev": {
"graham-campbell/analyzer": "^3.0",
"graham-campbell/testbench": "^5.4",
"mockery/mockery": "^1.3.1",
"phpunit/phpunit": "^8.5|^9.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "12.0-dev"
},
"laravel": {
"providers": [
"GrahamCampbell\\Markdown\\MarkdownServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"GrahamCampbell\\Markdown\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Graham Campbell",
"email": "graham@alt-three.com"
}
],
"description": "Markdown Is A CommonMark Wrapper For Laravel",
"keywords": [
"Graham Campbell",
"GrahamCampbell",
"Laravel Markdown",
"Laravel-Markdown",
"common mark",
"commonmark",
"framework",
"laravel",
"markdown"
],
"time": "2020-04-14T16:14:52+00:00"
},
{
"name": "guzzlehttp/guzzle",
"version": "6.5.4",

View File

@@ -251,6 +251,12 @@ return [
'header' => 'Application Management',
'can' => ['applications.view.all', 'applications.vote']
],
[
'text' => 'All applications',
'url' => 'applications/staff/all',
'icon' => 'fas fa-list-ol',
'can' => 'applications.view.all'
],
[
'text' => 'Outstanding Applications',
'url' => '/applications/staff/outstanding',
@@ -521,6 +527,7 @@ return [
'location' => 'https://cdn.jsdelivr.net/npm/fullcalendar@5.0.1/main.min.css'
]
]
]
],
],
];

View File

@@ -164,7 +164,9 @@ return [
/*
* Package Service Providers...
*/
GrahamCampbell\Markdown\MarkdownServiceProvider::class,
/*
* Application Service Providers...
@@ -173,6 +175,7 @@ return [
App\Providers\AuthServiceProvider::class,
App\Providers\UUIDConversionProvider::class,
App\Providers\IPInfoProvider::class,
App\Providers\ContextAwareValidatorProvider::class,
// App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
@@ -230,7 +233,9 @@ return [
'Validator' => Illuminate\Support\Facades\Validator::class,
'View' => Illuminate\Support\Facades\View::class,
'UUID' => App\Facades\UUID::class,
'IP' => App\Facades\IP::class
'IP' => App\Facades\IP::class,
'Markdown' => GrahamCampbell\Markdown\Facades\Markdown::class,
'ContextAwareValidator' => App\Facades\ContextAwareValidation::class
],

158
config/markdown.php Normal file
View File

@@ -0,0 +1,158 @@
<?php
declare(strict_types=1);
/*
* This file is part of Laravel Markdown.
*
* (c) Graham Campbell <graham@alt-three.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
return [
/*
|--------------------------------------------------------------------------
| Enable View Integration
|--------------------------------------------------------------------------
|
| This option specifies if the view integration is enabled so you can write
| markdown views and have them rendered as html. The following extensions
| are currently supported: ".md", ".md.php", and ".md.blade.php". You may
| disable this integration if it is conflicting with another package.
|
| Default: true
|
*/
'views' => true,
/*
|--------------------------------------------------------------------------
| CommonMark Extensions
|--------------------------------------------------------------------------
|
| This option specifies what extensions will be automatically enabled.
| Simply provide your extension class names here.
|
| Default: []
|
*/
'extensions' => [],
/*
|--------------------------------------------------------------------------
| Renderer Configuration
|--------------------------------------------------------------------------
|
| This option specifies an array of options for rendering HTML.
|
| Default: [
| 'block_separator' => "\n",
| 'inner_separator' => "\n",
| 'soft_break' => "\n",
| ]
|
*/
'renderer' => [
'block_separator' => "\n",
'inner_separator' => "\n",
'soft_break' => "\n",
],
/*
|--------------------------------------------------------------------------
| Enable Em Tag Parsing
|--------------------------------------------------------------------------
|
| This option specifies if `<em>` parsing is enabled.
|
| Default: true
|
*/
'enable_em' => true,
/*
|--------------------------------------------------------------------------
| Enable Strong Tag Parsing
|--------------------------------------------------------------------------
|
| This option specifies if `<strong>` parsing is enabled.
|
| Default: true
|
*/
'enable_strong' => true,
/*
|--------------------------------------------------------------------------
| Enable Asterisk Parsing
|--------------------------------------------------------------------------
|
| This option specifies if `*` should be parsed for emphasis.
|
| Default: true
|
*/
'use_asterisk' => true,
/*
|--------------------------------------------------------------------------
| Enable Underscore Parsing
|--------------------------------------------------------------------------
|
| This option specifies if `_` should be parsed for emphasis.
|
| Default: true
|
*/
'use_underscore' => true,
/*
|--------------------------------------------------------------------------
| HTML Input
|--------------------------------------------------------------------------
|
| This option specifies how to handle untrusted HTML input.
|
| Default: 'strip'
|
*/
'html_input' => 'strip',
/*
|--------------------------------------------------------------------------
| Allow Unsafe Links
|--------------------------------------------------------------------------
|
| This option specifies whether to allow risky image URLs and links.
|
| Default: true
|
*/
'allow_unsafe_links' => false,
/*
|--------------------------------------------------------------------------
| Maximum Nesting Level
|--------------------------------------------------------------------------
|
| This option specifies the maximum permitted block nesting level.
|
| Default: INF
|
*/
'max_nesting_level' => INF,
];

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddDetailedDescriptionToVacancy extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('vacancies', function (Blueprint $table) {
$table->longText('vacancyFullDescription')->nullable()->after('vacancyDescription');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('vacancies', function (Blueprint $table) {
$table->dropColumn('vacancyFullDescription');
});
}
}

9
install.sh Executable file
View File

@@ -0,0 +1,9 @@
#!/usr/bin/env bash
echo "Preliminary installation starting!"
sleep 3
composer install
echo "Full installation starting!"
sleep 3
php artisan application:install

10395
package-lock.json generated

File diff suppressed because it is too large Load Diff

1
public/css/home.css vendored Normal file
View File

@@ -0,0 +1 @@
.full-img-bg

1
public/img/403.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 17 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.1 KiB

1
public/img/editor.svg Normal file
View File

@@ -0,0 +1 @@
<svg id="a5860ba0-ef67-4914-ac96-07e2ebcccb4f" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="1095.74023" height="664.03433" viewBox="0 0 1095.74023 664.03433"><title>abstract</title><rect x="701" y="661" width="394.74023" height="2" fill="#3f3d56"/><polygon points="474 429 474 216 718 216 718 0 0 0 0 643 718 643 718 429 474 429" fill="#3f3d56"/><polygon points="718 216 718 0 469 0 469 213 249 213 249 0 0 0 0 216 225 216 225 429 0 429 0 643 476 643 476 429 474 429 474 216 718 216" opacity="0.1"/><path d="M224.12988,117.98284a172.00382,172.00382,0,0,1-172,172v-172Z" transform="translate(-52.12988 -117.98284)" fill="#6c63ff"/><path d="M132.12988,117.98284a80.00357,80.00357,0,0,1-80,80v44a124.00426,124.00426,0,0,0,124-124Z" transform="translate(-52.12988 -117.98284)" opacity="0.1"/><polygon points="718 60 718 29 553 29 553 0 517 0 517 29 517 60 517 84 517 115 553 115 718 115 718 84 553 84 553 60 718 60" fill="#d0cde1"/><path d="M481.12988,448.98284a81,81,0,1,0-92,80.23639V642.98284H256.36627a81,81,0,1,0,0,22H389.12988v48h22v-48h45v-22h-45V529.21923A81.00034,81.00034,0,0,0,481.12988,448.98284Z" transform="translate(-52.12988 -117.98284)" fill="#d0cde1"/><polygon points="626.5 445.263 662.508 507.631 698.517 570 626.5 570 554.483 570 590.492 507.631 626.5 445.263" fill="#d0cde1"/><polygon points="610.5 578 538.483 578 574.492 515.631 574.742 515.198 561.5 492.263 525.492 554.631 489.483 617 561.5 617 633.517 617 611 578 610.5 578" fill="#6c63ff"/><polygon points="159 345 159 291 186 291 186 259 52 259 52 393 186 393 186 345 159 345" fill="#d0cde1"/><rect x="89" y="234" width="50" height="50" fill="#6c63ff"/><path d="M989.162,357.83165,972.618,401.5551l-59.664,20.935s-36-4-36,16c0,14.18058,43,2,43,2l78.66169-23.57271,24.816-61.44917Z" transform="translate(-52.12988 -117.98284)" fill="#ffb8b8"/><rect x="635" y="228" width="234" height="202" fill="#6c63ff"/><polygon points="747.5 289.263 783.508 351.631 819.517 414 747.5 414 675.483 414 711.492 351.631 747.5 289.263" fill="#3f3d56"/><polygon points="747.331 334.415 763.665 362.708 780 391 747.331 391 714.661 391 730.996 362.708 747.331 334.415" fill="#d0cde1"/><polygon points="747.331 242.415 763.665 270.708 780 299 747.331 299 714.661 299 730.996 270.708 747.331 242.415" fill="#d0cde1"/><path d="M1024.15387,329.42035s-16.544-2.36343-24.816,28.36116-10.63544,33.088-10.63544,33.088,48.45031,7.09029,49.632,4.72686S1048.96988,336.51064,1024.15387,329.42035Z" transform="translate(-52.12988 -117.98284)" fill="#2f2e41"/><circle cx="962.57027" cy="167.71406" r="31.9063" fill="#ffb8b8"/><path d="M1000.51957,311.69463s10.63544,27.17944,7.09029,30.72459,43.72345,8.272,43.72345,8.272,3.54515-17.72572-10.63543-28.36116c0,0-5.90858-17.72572-4.72686-21.27087S1000.51957,311.69463,1000.51957,311.69463Z" transform="translate(-52.12988 -117.98284)" fill="#ffb8b8"/><path d="M1028.88073,323.51178a85.216,85.216,0,0,0-31.9063,21.27087c-14.18058,15.36229-21.27087,20.08915-21.27087,20.08915s-16.544,5.90857-10.63544,34.26973c2.95429,14.18058.88629,30.13373-1.92028,42.54174a265.8223,265.8223,0,0,0-6.35172,58.65559v12.2473s-108.71777,228.071-72.08461,243.43328,69.72118,20.08915,72.08461,16.544,44.90517-187.89267,44.90517-187.89267,11.81715,197.34639,41.36,197.34639,77.99318-8.272,77.99318-8.272l-60.26746-236.343s14.18058-40.17831-21.27087-68.53947l-4.72686-12.99886c19.60447.01,28.56614-124.06968,8.272-134.7155C1043.06131,321.14835,1038.33445,317.6032,1028.88073,323.51178Z" transform="translate(-52.12988 -117.98284)" fill="#2f2e41"/><path d="M999.33786,387.32438l-16.544,43.72345-59.664,20.935s-36-4-36,16c0,14.18058,43,2,43,2l78.6617-23.57271,24.816-61.44918Z" transform="translate(-52.12988 -117.98284)" fill="#ffb8b8"/><ellipse cx="868.62393" cy="652.21718" rx="20.68001" ry="11.81715" fill="#2f2e41"/><ellipse cx="1026.97373" cy="652.21718" rx="20.68001" ry="11.81715" fill="#2f2e41"/><path d="M1027.01734,240.95422h-.00012a25.34145,25.34145,0,0,0-4.89258.48651,21.44172,21.44172,0,0,0-9.34021-2.15271h-.84777c-16.34473,0-29.59461,14.7901-29.59461,33.03461v.00006h5.47669l.88452-6.73224L990,272.32269h4.56226a64.57589,64.57589,0,0,0-2.119,16.55036v33.62384h8.28027l4.79395-12.45795-1.19849,12.45795h53.22314l4.35816-11.32538-1.0896,11.32538h5.99243V296.09613C1066.80311,265.64215,1048.99037,240.95422,1027.01734,240.95422Z" transform="translate(-52.12988 -117.98284)" fill="#2f2e41"/></svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.1 KiB

1
public/img/preview.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@@ -1,3 +1,4 @@
// TODO: Add cleaner and less verbose solution found in formeditor.js
$(document).ready(function() {
$("#add").click(function() {
var lastField = $("#buildyourform div:last");

19
public/js/formeditor.js vendored Normal file
View File

@@ -0,0 +1,19 @@
// reminder: use vuejs instead, this is still an ugly and cheap solution
$(document).ready(function(){
var fieldID = 0;
var wrapper = $('.field-container');
var newBtn = $('#add');
$(newBtn).click(function(e){
e.preventDefault()
fieldID++;
$(wrapper).append('<div id=group' + fieldID + '><input type="text" name="newFieldID' + fieldID + '[]" class="form-control" />');
$(wrapper).append('<select name="newFieldID' + fieldID + '[]" class="custom-select"> <option value="nil" disabled>Choose a type</option> <option value="textbox">Textbox</option> <option value="textarea">Multi line answer</option> <option value="checkbox">Checkbox</option> </select>');
//$(wrapper).append('<button type="button" class="btn btn-danger btn-sm float-right delete"><i class="fas fa-minus"></i></button></div>');
});
});

View File

@@ -1,9 +1,22 @@
@extends('layouts.app')
@section('content')
<x-global-errors></x-global-errors>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
@if (session()->has('error'))
<div class="alert alert-danger">
<i class="fas fa-exclamation"></i><b> Please verify your submission</b>
<p>
{{ session('error') }}
</p>
</div>
@endif
<div class="card">
<div class="card-header">{{ __('Register') }}</div>

View File

@@ -0,0 +1,32 @@
@foreach($form['fields'] as $fieldName => $field)
@switch ($field['type'])
@case('textarea')
<div class="form-group mt-2 mb-2">
<label for="{{$fieldName}}">{{$field['title']}}</label>
<textarea class="form-control" rows="7" name="{{$fieldName}}" id="{{$fieldName}}" {{ ($disableFields) ? 'disabled' : '' }}>
</textarea>
</div>
@break
@case('textbox')
<div class="form-group mt-2 mb-2">
<label for="{{$fieldName}}">{{$field['title']}}</label>
<input type="text" name="{{$fieldName}}" id="{{$fieldName}}" {{ ($disableFields) ? 'disabled' : '' }} class="form-control">
</div>
@break
@endswitch
@endforeach

View File

@@ -0,0 +1,74 @@
@if ($inDashboard)
<div class="row mb-4">
<div class="col-6 offset-5">
<img src="/img/403.svg" width="150px" alt="Access denied" />
</div>
</div>
<div class="row">
<!-- People find pleasure in different ways. I find it in keeping my mind clear. - Marcus Aurelius -->
<div class="col">
<div class="alert alert-{{$type}}">
<h4><i class="fas fa-user-lock"></i> Access Denied</h2>
<p>
We're sorry, but you do not have permission to access this web page.
</p>
<p>
Please contact your administrator if you believe this was in error.
</p>
</div>
</div>
</div>
@else
@extends('adminlte::page')
@section('title', 'Raspberry Network | Access Denied')
@section('content_header')
<h4>Access Denied - HTTP 403</h4>
@stop
@section('content')
<div class="row mb-4">
<div class="col-6 offset-5">
<img src="/img/403.svg" width="150px" alt="Access denied" />
</div>
</div>
<div class="row">
<div class="col">
<div class="alert alert-{{$type}}">
<h4><i class="fas fa-user-lock"></i> Access Denied</h2>
<p class="text-muted">
@if (isset($slot))
{{ $slot }}
@endif
</p>
<p>
We're sorry, but you do not have permission to access this web page.
</p>
<p>
Please contact your administrator if you believe this was in error.
</p>
</div>
</div>
</div>
@stop
@endif

View File

@@ -0,0 +1,103 @@
@extends('adminlte::page')
@section('title', 'Raspberry Network | Edit From')
@section('content_header')
<h4>Administration / Forms / Editor</h4>
@stop
@section('js')
<script src="/js/formeditor.js"></script>
<x-global-errors></x-global-errors>
@stop
@section('content')
<div class="row mb-5">
<div class="col">
</div>
<div class="col text-center">
<img src="/img/editor.svg" width="250px" alt="Editor illustration" class="img-responsive" />
</div>
<div class="col">
</div>
</div>
<div class="row">
<div class="col">
</div>
<div class="col">
<form id="editForm" method="POST" action="{{ route('updateForm', ['form' => $formID]) }}">
@csrf
@method('PATCH')
<div class="card">
<div class="card-header">
<h4>Editing {{ $title }}...</h4>
</div>
<div class="card-body">
@foreach($formStructure['fields'] as $fieldName => $field)
<div class="form-group mt-4 mb-4">
<input autocomplete="false" type="text" id="{{ $fieldName }}" class="form-control" name="{{ $fieldName }}[]" value="{{ $field['title'] }}" />
<select class="custom-select" id="{{ $fieldName }}-type" name="{{ $fieldName }}[]">
<option value="nil" disabled>Choose a type</option>
<option value="textbox" {{ ($field['type'] == 'textbox' ? 'selected' : '') }}>Textbox</option>
<option value="textarea" {{ ($field['type'] == 'textarea' ? 'selected' : '') }}>Multi line answer</option>
<option value="checkbox" {{ ($field['type'] == 'checkbox' ? 'selected' : '') }}>Checkbox</option>
</select>
</div>
@endforeach
<div class="field-container mt-4 mb-4">
</div>
</div>
<div class="card-footer text-center">
<button type="button" class="btn btn-warning ml-2" onclick="$('#editForm').submit()"><i class="fas fa-save"></i> Save & Quit</button>
<button type="button" class="btn btn-primary ml-2" id="add"><i class="fas fa-plus"></i> New field</button>
</div>
</div>
</form>
</div>
<div class="col">
</div>
</div>
@stop

View File

@@ -0,0 +1,155 @@
@extends('adminlte::page')
@section('title', 'Raspberry Network | Edit Positions')
@section('content_header')
<h4>Administration / Positions / Edit</h4>
@stop
@section('js')
<x-global-errors>
</x-global-errors>
@stop
@section('content')
<div class="row">
<div class="col center">
<h3>Vacancy Editor</h3>
</div>
</div>
<div class="row">
<div class="col">
<div class="card">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-clipboard"></i> {{ $vacancy->vacancyName }}</h3>
</div>
<div class="card-body">
<p class="text-muted"><i class="fas fa-question-circle"></i> For consistency purposes, grayed out fields can't be edited.</p>
<form method="POST" id="editPositionForm" action="{{ route('updatePosition', ['position' => $vacancy->id]) }}">
@csrf
@method('PATCH')
<div class="row">
<div class="col">
<label for="vacancyName">Vacancy Name</label>
<input type="text" value="{{ $vacancy->vacancyName }}" class="form-control" disabled />
</div>
<div class="col">
<label for="vacancyDescription">Vacancy description</label>
<input type="vacancyDescription" class="form-control" name="vacancyDescription" value="{{ $vacancy->vacancyDescription }}" />
</div>
</div>
<div class="row">
<div class="col">
<!-- skipping the accessor for obvious reasons -->
<label for="vacanyDetails">Vacancy details</label>
<textarea name="vacancyFullDescription" class="form-control" placeholder="{{ (is_null($vacancy->vacancyFullDescription)) ? 'No details yet. Add some!' : '' }}" rows="20">{{ $vacancy->getAttributes()['vacancyFullDescription'] }}</textarea>
<span class="text-muted"><i class="fab fa-markdown"></i> Markdown supported</span>
</div>
</div>
<div class="row">
<div class="col">
<label for "permissionGroupName">Permission Group</label>
<input type="text" class="form-control" value="{{ $vacancy->permissionGroupName }}" id="permissionGroupName" disabled />
</div>
<div class="col">
<label for "discordRoleID">Discord Role ID</label>
<input type="text" class="form-control" value="{{ $vacancy->discordRoleID }}" id="discordRoleID" disabled />
</div>
</div>
<div class="row">
<div class="col">
<label for "currentForm">Current Form (uneditable)</label>
<input type="text" class="form-control" value="{{ $vacancy->forms->formName }}" id="currentForm" disabled />
<label for "remainingSlots">Remaining slots</label>
<input type="text" class="form-control" value="{{ $vacancy->vacancyCount }}" id="remainingSlots" name="vacancyCount" />
</div>
</div>
</form>
</div>
<div class="card-footer">
<button type="button" class="btn btn-warning" onclick="$('#editPositionForm').submit()"><i class="fas fa-edit"></i> Save Changes</button>
<button type="button" class="btn btn-danger" onclick="window.location.href='{{ route('showPositions') }}'"><i class="fas fa-times"></i> Cancel</button>
@if($vacancy->vacancyStatus == 'OPEN')
<form method="POST" action="{{ route('updatePositionAvailability', ['id' => $vacancy->id, 'status' => 'close']) }}" style="display: inline">
@method('PATCH')
@csrf
<button type="submit" class="ml-4 btn btn-danger"><i class="fas fa-ban"></i> Close Position</button>
</form>
@endif
</div>
</div>
</div>
</div>
@stop

View File

@@ -0,0 +1,79 @@
@extends('adminlte::page')
@section('title', 'Raspberry Network | Application Form Preview')
@section('content_header')
<h4>Administration / Form Builder / Preview</h4>
@stop
@section('js')
<x-global-errors></x-global-errors>
@stop
@section('content')
<div class="row">
<div class="col-6 offset-4">
<img src="/img/preview.svg" width="250px" alt="Form preview illustration" />
</div>
</div>
<div class="row mt-4">
<div class="col">
<div class="alert alert-success">
<h5><i class="fas fa-eye"></i> This is how your form looks like to applicants</h3>
<p>
You may edit it and add more fields later.
</p>
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="card">
<div class="card-header">
<h3>{{ $title }}'s form</h2>
</div>
<div class="card-body">
@component('components.form', ['form' => $form, 'disableFields' => true])
@endcomponent
</div>
<div class="card-footer text-center">
<button type="button" class="btn btn-success ml-2" onclick="window.location.href='{{ route('showForms') }}'"><i class="fas fa-chevron-left"></i> Go back</button>
<button type="button" class="btn btn-warning ml-2" onclick="window.location.href='{{ route('editForm', ['form' => $formID]) }}'"><i class="far fa-edit"></i> Edit</button>
</div>
</div>
</div>
</div>
@stop

View File

@@ -38,6 +38,7 @@
<th>#</th>
<th>Form Title</th>
<th>Created On</th>
<th>Updated On</th>
<th>Actions</th>
</tr>
@@ -51,6 +52,7 @@
<td>{{$form->id}}</td>
<td>{{$form->formName}}</td>
<td>{{$form->created_at}}</td>
<td>{{ $form->updated_at }}</td>
<td>
<form style="display: inline-block; white-space: nowrap" action="{{route('destroyForm', ['id' => $form->id])}}" method="POST">
@@ -59,7 +61,7 @@
<button type="submit" class="btn btn-sm btn-danger mr-2"><i class="fa fa-trash"></i> Delete</button>
</form>
<button type="button" class="btn btn-sm btn-success"><i class="fa fa-eye"></i> Preview</button>
<button type="button" class="btn btn-sm btn-success" onclick="window.location.href='{{ route('previewForm', ['form' => $form->id]) }}'"><i class="fa fa-eye"></i> Preview</button>
</td>
</tr>

View File

@@ -4,7 +4,11 @@
@section('content_header')
<h4>Administration / Open Positions</h4>
@if (Auth::user()->hasAnyRole('admin', 'hiringManager'))
<h4>Administration / Open Positions</h4>
@else
<h4>Application Access Denied</h4>
@endif
@stop
@@ -33,7 +37,8 @@
@stop
@section('content')
@if (Auth::user()->hasAnyRole('admin', 'hiringManager'))
<!-- todo: switch to modal component -->
<div class="modal fade" tabindex="-1" id="newVacancyForm" role="dialog" aria-labelledby="modalFormLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
@@ -55,6 +60,9 @@
<label for="vacancyDescription">Vacancy Description</label>
<input type="text" id="vacancyDescription" name="vacancyDescription" class="form-control">
<label for="vacancyFullDescription">Vacancy Details</label>
<textarea name="vacancyFullDescription" class="form-control" rel="txtTooltip" title="Add things like admission requirements, rank resposibilities and roles, and anything else you feel is necessary" data-toggle="tooltip" data-placement="bottom"></textarea>
<span class="right text-muted"><i class="fab fa-markdown"></i> Markdown supported</span>
<div class="row mt-3">
<div class="col">
@@ -153,9 +161,8 @@
<thead>
<tr>
<th>#</th>
<th>Vacancy Name</th>
<th>Vacancy Description</th>
<th>Name</th>
<th>Description</th>
<th>Discord Role ID</th>
<th>Perm. Group Name</th>
<th>Open Slots</th>
@@ -171,8 +178,6 @@
@foreach($vacancies as $vacancy)
<tr>
<td>{{$vacancy->id}}</td>
<td>{{$vacancy->vacancyName}}</td>
<td>{{substr($vacancy->vacancyDescription, 0, 20)}}...</td>
<td><span class="badge badge-success">{{$vacancy->discordRoleID}}</span></td>
@@ -185,20 +190,23 @@
@endif
<td>{{$vacancy->created_at}}</td>
<td>
<button type="button" class="btn btn-sm btn-warning" onclick="window.location.href='{{ route('editPosition', ['position' => $vacancy->id]) }}'"><i class="fas fa-edit"></i></button>
@if ($vacancy->vacancyStatus == 'OPEN')
<form action="{{route('updatePositionAvailability', ['status' => 'close', 'id' => $vacancy->id])}}" method="POST" id="closePosition">
<form action="{{route('updatePositionAvailability', ['status' => 'close', 'id' => $vacancy->id])}}" method="POST" id="closePosition" style="display: inline">
@csrf
@method('PATCH')
<button type="submit" class="btn btn-sm btn-danger"><i class="fa fa-ban"></i> Close</button>
<button type="submit" class="btn btn-sm btn-danger"><i class="fa fa-ban"></i></button>
</form>
@else
<form action="{{route('updatePositionAvailability', ['status' => 'open', 'id' => $vacancy->id])}}" method="POST" id="openPosition">
<form action="{{route('updatePositionAvailability', ['status' => 'open', 'id' => $vacancy->id])}}" method="POST" id="openPosition" style="display: inline">
@csrf
@method('PATCH')
<button type="submit" class="btn btn-sm btn-success"><i class="fa fa-check"></i> Open</button>
<button type="submit" class="btn btn-sm btn-success"><i class="fa fa-check"></i></button>
</form>
@endif
@@ -232,5 +240,7 @@
</div>
</div>
@else
<x-no-permission type="danger"></x-no-permission>
@endif
@stop

View File

@@ -93,38 +93,10 @@
<form action="{{route('saveApplicationForm', ['vacancySlug' => $vacancy->vacancySlug])}}" method="POST" id="submitApplicationForm">
@csrf
@foreach($preprocessedForm['fields'] as $fieldName => $field)
@switch ($field['type'])
@component('components.form', ['form' => $preprocessedForm, 'disableFields' => false])
@case('textarea')
<div class="form-group mt-2 mb-2">
<label for="{{$fieldName}}">{{$field['title']}}</label>
<textarea class="form-control" rows="7" name="{{$fieldName}}" id="{{$fieldName}}">
</textarea>
</div>
@break
@case('textbox')
<div class="form-group mt-2 mb-2">
<label for="{{$fieldName}}">{{$field['title']}}</label>
<input type="text" name="{{$fieldName}}" id="{{$fieldName}}" class="form-control">
</div>
@break
@endswitch
@endforeach
@endcomponent
</form>

View File

@@ -0,0 +1,243 @@
@extends('adminlte::page')
@section('title', 'Raspberry Network | Profile')
@section('content_header')
<h4>Application Management / All Applications</h4>
@stop
@section('js')
<script type="text/javascript" src="/js/app.js"></script>
<x-global-errors></x-global-errors>
@stop
@section('content')
@foreach($applications as $application)
<x-modal id="deletionConfirmationModal-{{ $application->id }}" modal-label="deletion-{{ $application->id }}" modal-title="Are you sure?" include-close-button="true">
<h4><i class="fas fa-exclamation-triangle"></i> Really delete this?</h3>
<p>
This action is <b>IRREVERSBILE.</b>
</p>
<p>Comments, appointments and any votes attached to this application WILL be deleted too. Please make sure this application really needs to be deleted.</p>
<x-slot name="modalFooter">
<form method="POST" action="{{ route('deleteApplication', ['application' => $application->id]) }}">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-danger"><i class="fas fa-check-double"></i> Confirm</button>
</form>
</x-slot>
</x-modal>
@endforeach
<div class="row">
<div class="col">
<div class="callout callout-info">
<div class="row">
<div class="col-3">
<img src="/img/applications_all.svg" alt="Applications illustration" class="img-responsive" width="200px"/>
</div>
<div class="col">
<h3><i class="fas fa-info-circle"></i> You're looking at all applications ever received</h3>
<p>
Here, you have quick and easy access to all applications ever received by the system.
</p>
</div>
</div>
</div>
</div>
</div>
<div class="row mt-5">
<div class="col">
<div class="card">
<!-- MAIN CONTENT - APPS AND PICS -->
<div class="card-header">
<div class="row">
<div class="col-3">
<h3>All applications</h3>
</div>
<div class="col">
<div class="navbtn right" style="whitespace: nowrap">
<button type="button" class="btn btn-sm btn-primary" onclick="window.location.href='{{ route('staffPendingApps') }}'"><i class="far fa-folder-open"></i> Outstanding Applications</button>
<button type="button" class="btn btn-sm btn-primary" onclick="window.location.href='{{ route('pendingInterview') }}'"><i class="fas fa-microphone-alt"></i> Interview Queue</button>
<button type="button" class="btn btn-sm btn-primary" onclick="window.location.href='{{ route('peerReview') }}'"><i class="fas fa-search"></i> Peer Review</button>
</div>
</div>
</div>
</div>
<div class="card-body">
<div class="row">
<div class="col-3 center">
<img src="/img/placeholders.svg" alt="Placeholder illustration" class="img-responsive" width="200px"/>
</div>
<div class="col">
@if (!$applications->isEmpty())
<table class="table table-borderless" style="whitespace: nowrap">
<thead>
<tr>
<th>#</th>
<th>Applicant</th>
<th>Status</th>
<th>Date</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach($applications as $application)
<tr>
<td>{{ $application->id }}</td>
<td><a href="{{ route('showSingleProfile', ['user' => $application->user->id]) }}">{{ $application->user->name }}</a></td>
<td>
@switch($application->applicationStatus)
@case('STAGE_SUBMITTED')
<span class="badge badge-primary"><i class="far fa-clock"></i> Outstanding (Submitted)</span>
@break
@case('STAGE_PEERAPPROVAL')
<span class="badge badge-warning"><i class="fas fa-vote-yea"></i> Peer Approval</span>
@break
@case('STAGE_INTERVIEW')
<span class="badge badge-warning"><i class="fas fa-microphone-alt"></i> Interview</span>
@break
@case('STAGE_INTERVIEW_SCHEDULED')
<span class="badge badge-warning"><i class="far fa-clock"></i>Interview Scheduled</span>
@break
@case('APPROVED')
<span class="badge badge-success"><i class="fas fa-check"></i> Approved</span>
@break
@case('DENIED')
<span class="badge badge-danger"><i class="fas fa-times"></i> Denied</span>
@break;
@default
<span class="badge badge-secondary"><i class="fas fa-question-circle"></i> Unknown</span>
@endswitch
</td>
<td>{{ $application->created_at }}</td>
<td>
<button type="button" class="btn btn-success btn-sm" onclick="window.location.href='{{ route('showUserApp', ['id' => $application->id]) }}'"><i class="fas fa-eye"></i> View</button>
<button type="button" class="btn btn-danger btn-sm ml-2" onclick="$('#deletionConfirmationModal-{{ $application->id }}').modal('show')"><i class="fa fa-trash"></i> Delete</button>
</td>
</tr>
@endforeach
</tbody>
</table>
@else
<div class="alert alert-warning">
<h3><i class="fas fa-question-circle"></i> There are no applications here</h3>
<p>
We couldn't find any applications. Maybe no one has applied yet?
Please try again later.
</p>
</div>
@endif
</div>
</div>
</div>
<!-- end main content card -->
</div>
@if (!$applications->isEmpty() && isset($applications->links))
<div class="card-footer">
{{ $applications->links }}
</div>
@endif
</div>
</div>
@stop

View File

@@ -14,6 +14,38 @@
@section('content')
@if (!$vacancies->isEmpty())
@foreach($vacancies as $vacancy)
<x-modal id="{{ $vacancy->vacancySlug . '-details' }}" modal-label="{{ $vacancy->vacancySlug . '-details-label' }}" modal-title="Vacancy details" include-close-button="true">
@if (is_null($vacancy->vacancyFullDescription))
<div class="alert alert-warning">
<h3><i class="fas fa-question-circle"></i> There don't seem to be any details</h3>
<p>
This vacancy does not have any details yet.
</p>
</div>
@else
{!! $vacancy->vacancyFullDescription !!}
<p class="text-sm text-muted">
Last updated @ {{ $vacancy->updated_at }}
</p>
@endif
<x-slot name="modalFooter"></x-slot>
</x-modal>
@endforeach
@endif
<div class="row mt-5">
<div class="col">
@@ -190,7 +222,7 @@
<div class="card-footer text-center">
<button type="button" class="btn btn-primary btn-sm" onclick="window.location.href='{{ route('renderApplicationForm', ['vacancySlug' => $vacancy->vacancySlug]) }}'">Apply</button>
<button type="button" class="btn btn-warning btn-sm">Learn More</button>
<button type="button" class="btn btn-warning btn-sm" onclick="$('#{{ $vacancy->vacancySlug }}-details').modal('show')">Learn More</button>
</div>
</div>

View File

@@ -75,6 +75,18 @@
@endhasrole
<div class="row">
<div class="col">
<div class="alert alert-warning alert-dismissible">
<a href="#" class="close" data-dismiss="alert" aria-label="close">&times;</a>
<strong>Reminder:</strong> If this form has been updated, new fields and updated questions will not show up here!
</div>
</div>
</div>
<div class="row">
@@ -357,9 +369,9 @@
</div>
<div class="row">
<div class="col">
@if ($comments->isEmpty())
<div class="alert alert-warning">
@@ -378,11 +390,11 @@
@if (!$comments->isEmpty())
@foreach($comments as $comment)
<div class="row mt-3 mb-3">
<div class="row mt-3 mb-3">
<div class="col-md-2">
<div class="text-center">
@if($application->user->avatarPreference == 'gravatar')
<img class="profile-user-img img-fluid img-circle" src="https://gravatar.com/avatar/{{md5($comment->user->email)}}" alt="User profile picture">
@@ -394,24 +406,24 @@
</div>
<div class="card comment">
<div class="card-header comment-header">
<h1 class="commenter">{{$comment->user->name}} &#9679; {{Carbon\Carbon::parse($comment->created_at)->diffForHumans()}}</h3>
</div>
<div class="card-body">
{{$comment->text}}
</div>
@if(Auth::user()->is($comment->user) || Auth::user()->hasRole('admin'))
<div class="card-footer comment-footer">
<form method="POST" id="deleteComment" action="{{route('deleteApplicationComment', ['comment' => $comment->id])}}">
@csrf
@method('DELETE')
@@ -434,7 +446,7 @@
<div class="row mt-5">
<div class="col-md-2">
<div class="text-center">
@if($application->user->avatarPreference == 'gravatar')
<img class="profile-user-img img-fluid img-circle" src="https://gravatar.com/avatar/{{md5(Auth::user()->email)}}" alt="User profile picture">
@@ -443,15 +455,15 @@
@endif
</div>
</div>
</div>
<div class="col">
<div class="card border-top border-bottom">
<div class="card-body">
<form id="newComment" method="POST" action="{{route('addApplicationComment', ['application' => $application->id])}}">
@csrf
<textarea id="comment" name="comment" class="form-control" id="commentText"></textarea>
@@ -459,14 +471,14 @@
</form>
<div class="row">
<div class="col text-left">
<p class="text-sm text-muted">Commenting as {{Auth::user()->name}}</p>
</div>
<div class="col text-right">
<p class="text-sm text-muted"><span id="charcount">0</span>/600 max characters</p>
</div>
@@ -476,7 +488,7 @@
</div>
<div class="card-footer text-right">
<button type="button" id="submitComment" class="btn btn-sm btn-secondary">Post</button>
</div>

View File

@@ -2,6 +2,40 @@
@section('content')
@if(!$positions->isEmpty())
<!-- todo: details component -->
@foreach($positions as $position)
<x-modal id="{{ $position->vacancySlug . '-details' }}" modal-label="{{ $position->vacancySlug . '-details-label' }}" modal-title="Opening details" include-close-button="true">
@if (is_null($position->vacancyFullDescription))
<div class="alert alert-warning">
<h3><i class="fas fa-question-circle"></i> There don't seem to be any details</h3>
<p>
This opening does not have any details yet.
</p>
</div>
@else
{!! $position->vacancyFullDescription !!}
<p class="text-sm text-muted">
Last updated @ {{ $position->updated_at }}
</p>
@endif
<x-slot name="modalFooter"></x-slot>
</x-modal>
@endforeach
@endif
<!--Main Layout-->
<main class="py-5">
@@ -57,6 +91,7 @@
@guest
<button type="button" class="btn btn-success" onclick="window.location.href='{{route('renderApplicationForm', ['vacancySlug' => $position->vacancySlug])}}'">Apply</button>
<button type="button" class="btn btn-info" onclick="$('#{{ $position->vacancySlug }}-details').modal('show')">Learn more</button>
@endguest
</div>
@@ -163,72 +198,11 @@
<div class="row">
<div class="col text-center">
<p>
Join the team today and help out network grow and prosper!
</p>
</div>
<div class="card">
<!-- Card content -->
<div class="card-body text-center">
<!-- Title -->
<img src="https://crafatar.com/avatars/6102256a-bd28-4dd7-b68e-4c96ef313734" class="img-fluid mb-3" alt="miguel456's avatar">
<h4 class="card-title text-center"><a>miguel456</a></h4>
<!-- Text -->
<p class="card-text">Network Owner / Web Developer</p>
</div>
<div class="card-footer">
<button type="button" class="btn btn-info">More Info</button>
</div>
</div>
</div>
<div class="col text-center">
<div class="card">
<div class="card-body">
<h4 class="card-title">Moderator</h4>
<p class="card-text">Open Position!</p>
</div>
<div class="card-footer">
<button type="button" class="btn btn-success">Apply</button>
</div>
</div>
</div>
<div class="col text-center">
<div class="card">
<div class="card-body">
<h4 class="card-title">Helper</h4>
<p class="card-text">Open Position!</p>
</div>
<div class="card-footer">
<button type="button" class="btn btn-success">Apply</button>
</div>
</div>
</div>
</div>

View File

@@ -12,6 +12,7 @@
<!-- Scripts -->
<script src="{{ asset('js/app.js') }}" defer></script>
<!-- Fonts -->
<link rel="dns-prefetch" href="//fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css?family=Nunito" rel="stylesheet">
@@ -79,5 +80,7 @@
@yield('content')
</main>
</div>
</body>
</html>

View File

@@ -40,7 +40,6 @@ Route::group(['middleware' => ['auth', 'forcelogout']], function(){
->name('showUserApps')
->middleware('eligibility');
Route::get('/view/{id}', 'ApplicationController@showUserApp')
->name('showUserApp');
@@ -58,16 +57,27 @@ Route::group(['middleware' => ['auth', 'forcelogout']], function(){
Route::patch('/update/{id}/{newStatus}', 'ApplicationController@updateApplicationStatus')
->name('updateApplicationStatus');
Route::delete('{application}/delete', 'ApplicationController@delete')
->name('deleteApplication');
Route::get('/staff/all', 'ApplicationController@showAllApps')
->name('allApplications');
Route::get('/staff/outstanding', 'ApplicationController@showAllPendingApps')
->name('staffPendingApps');
Route::get('/staff/peer-review', 'ApplicationController@showPeerReview')
->name('peerReview');
Route::get('/staff/pending-interview', 'ApplicationController@showPendingInterview')
->name('pendingInterview');
Route::post('{id}/staff/vote', 'VoteController@vote')
->name('voteApplication');
@@ -161,6 +171,13 @@ Route::group(['middleware' => ['auth', 'forcelogout']], function(){
->name('savePosition');
Route::get('positions/edit/{position}', 'VacancyController@edit')
->name('editPosition');
Route::patch('positions/update/{position}', 'VacancyController@update')
->name('updatePosition');
Route::patch('positions/availability/{status}/{id}', 'VacancyController@updatePositionAvailability')
->name('updatePositionAvailability');
@@ -177,6 +194,15 @@ Route::group(['middleware' => ['auth', 'forcelogout']], function(){
Route::get('forms', 'FormController@index')
->name('showForms');
Route::get('forms/preview/{form}', 'FormController@preview')
->name('previewForm');
Route::get('forms/edit/{form}', 'FormController@edit')
->name('editForm');
Route::patch('forms/update/{form}', 'FormController@update')
->name('updateForm');
Route::get('devtools', 'DevToolsController@index')
->name('devTools');

1633
test.php

File diff suppressed because it is too large Load Diff