├── .circleci └── config.yml ├── .editorconfig ├── .env.example ├── .gitattributes ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── app ├── Console │ └── Kernel.php ├── Date.php ├── Exceptions │ └── Handler.php ├── Http │ ├── Controllers │ │ ├── Auth │ │ │ ├── ForgotPasswordController.php │ │ │ ├── LoginController.php │ │ │ ├── RegisterController.php │ │ │ ├── ResetPasswordController.php │ │ │ └── VerificationController.php │ │ ├── Controller.php │ │ ├── DatesController.php │ │ ├── HomeController.php │ │ ├── LanguagesController.php │ │ └── ReportsController.php │ ├── Kernel.php │ └── Middleware │ │ ├── Authenticate.php │ │ ├── CheckForMaintenanceMode.php │ │ ├── EncryptCookies.php │ │ ├── RedirectIfAuthenticated.php │ │ ├── ReferrerPolicy.php │ │ ├── RestoreLocale.php │ │ ├── SeeReportsHistory.php │ │ ├── TrimStrings.php │ │ ├── TrustProxies.php │ │ └── VerifyCsrfToken.php ├── Mail │ ├── FillBulletinReminder.php │ └── WeeklyReport.php ├── Project.php ├── Projects.php ├── Providers │ ├── AppServiceProvider.php │ ├── AuthServiceProvider.php │ ├── BroadcastServiceProvider.php │ ├── EventServiceProvider.php │ └── RouteServiceProvider.php ├── Report.php ├── Slack.php └── User.php ├── artisan ├── bootstrap ├── app.php └── cache │ └── .gitignore ├── composer.json ├── composer.lock ├── config ├── app.php ├── auth.php ├── broadcasting.php ├── cache.php ├── cors.php ├── database.php ├── filesystems.php ├── hashing.php ├── logging.php ├── mail.php ├── projects.example.yml ├── queue.php ├── services.php ├── session.php └── view.php ├── database ├── .gitignore ├── factories │ ├── DateFactory.php │ ├── ProjectFactory.php │ ├── ReportFactory.php │ └── UserFactory.php ├── migrations │ ├── 2019_02_28_102730_create_reports_table.php │ ├── 2019_05_22_193204_create_dates_table.php │ └── 2019_08_19_000000_create_failed_jobs_table.php └── seeds │ └── DatabaseSeeder.php ├── docker-compose.yml ├── docker ├── nginx-log.conf └── nginx.conf ├── package-lock.json ├── package.json ├── phpunit.xml ├── public ├── .htaccess ├── css │ ├── all.css │ └── app.css ├── favicon.ico ├── images │ ├── circuit-board.svg │ ├── favicon.png │ └── logos │ │ └── .gitkeep ├── index.php ├── js │ └── app.js ├── mix-manifest.json ├── robots.txt └── web.config ├── resources ├── css │ └── all.css ├── js │ ├── TextareaAutoResize.js │ ├── TextareaMarkdown.js │ ├── app.js │ ├── bootstrap.js │ └── components │ │ └── ExampleComponent.vue ├── lang │ ├── en │ │ ├── about.php │ │ ├── auth.php │ │ ├── dates.php │ │ ├── emails.php │ │ ├── form.php │ │ ├── layout.php │ │ ├── login.php │ │ ├── notifications.php │ │ ├── pagination.php │ │ ├── passwords.php │ │ ├── reports.php │ │ ├── success.php │ │ └── validation.php │ └── fr │ │ ├── about.php │ │ ├── auth.php │ │ ├── dates.php │ │ ├── emails.php │ │ ├── form.php │ │ ├── layout.php │ │ ├── login.php │ │ ├── notifications.php │ │ ├── pagination.php │ │ ├── passwords.php │ │ ├── reports.php │ │ ├── success.php │ │ └── validation.php ├── sass │ ├── _variables.scss │ └── app.scss └── views │ ├── about.blade.php │ ├── dates │ └── index.blade.php │ ├── emails │ ├── fill_reminder.blade.php │ └── report.blade.php │ ├── index.blade.php │ ├── login.blade.php │ ├── master.blade.php │ ├── reports │ ├── _info.blade.php │ ├── index.blade.php │ └── week_index.blade.php │ ├── success.blade.php │ └── vendor │ └── mail │ ├── html │ ├── button.blade.php │ ├── footer.blade.php │ ├── header.blade.php │ ├── layout.blade.php │ ├── message.blade.php │ ├── panel.blade.php │ ├── subcopy.blade.php │ ├── table.blade.php │ └── themes │ │ └── default.css │ └── text │ ├── button.blade.php │ ├── footer.blade.php │ ├── header.blade.php │ ├── layout.blade.php │ ├── message.blade.php │ ├── panel.blade.php │ ├── subcopy.blade.php │ └── table.blade.php ├── routes ├── api.php ├── channels.php ├── console.php └── web.php ├── server.php ├── storage ├── app │ ├── .gitignore │ └── public │ │ └── .gitignore ├── framework │ ├── .gitignore │ ├── cache │ │ ├── .gitignore │ │ └── data │ │ │ └── .gitignore │ ├── sessions │ │ └── .gitignore │ ├── testing │ │ └── .gitignore │ └── views │ │ └── .gitignore └── logs │ └── .gitignore ├── tests ├── CreatesApplication.php ├── Feature │ ├── KeyDatesHistoryTest.php │ ├── LoginTest.php │ ├── PagesTest.php │ ├── ReportsHistoryByProjectTest.php │ ├── ReportsHistoryByWeekTest.php │ ├── ReportsProjectExportTest.php │ ├── SwitchLocaleTest.php │ ├── WeeklyReportTest.php │ └── WriteReportTest.php ├── TestCase.php ├── Unit │ ├── DateTest.php │ ├── ProjectTest.php │ ├── ProjectsTest.php │ └── ReportTest.php └── boostrap.php └── webpack.mix.js /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/php:7.2-node-browsers 6 | steps: 7 | - checkout 8 | - restore_cache: 9 | keys: 10 | - composer-v1-{{ checksum "composer.lock" }} 11 | - composer-v1- 12 | 13 | - run: 14 | name: Composer install 15 | command: | 16 | composer install -n --prefer-dist 17 | 18 | - save_cache: 19 | key: composer-v1-{{ checksum "composer.lock" }} 20 | paths: 21 | - vendor 22 | 23 | - restore_cache: 24 | keys: 25 | - node-v3-{{ checksum "package.json" }} 26 | - node-v3- 27 | 28 | - run: 29 | name: npm install 30 | command: | 31 | npm install 32 | 33 | - save_cache: 34 | key: node-v3-{{ checksum "package.json" }} 35 | paths: 36 | - node_modules 37 | 38 | - run: 39 | name: Generate env file 40 | command: | 41 | cp .env.example .env 42 | cp config/projects.example.yml config/projects.yml 43 | php artisan key:generate 44 | 45 | - run: 46 | name: Run tests 47 | command: | 48 | php artisan test 49 | 50 | install_from_composer: 51 | docker: 52 | - image: circleci/php:7.2-node-browsers 53 | steps: 54 | - run: 55 | name: Composer create project 56 | command: | 57 | composer create-project --prefer-dist --stability=dev entrepreneur-interet-general/bulletins 58 | 59 | - run: 60 | name: Copy default env file 61 | command: | 62 | cd bulletins 63 | cp .env.example .env 64 | cp config/projects.example.yml config/projects.yml 65 | php artisan key:generate 66 | 67 | - run: 68 | name: Create database with seeding data 69 | command: | 70 | cd bulletins 71 | touch database/database.sqlite 72 | php artisan migrate:fresh --seed 73 | 74 | - run: 75 | name: Start PHP server 76 | command: | 77 | cd bulletins 78 | php artisan serve --port=8000 79 | background: true 80 | 81 | - run: 82 | name: Test GET / 83 | command: | 84 | sleep 1 85 | wget --tries=1 http://localhost:8000 86 | workflows: 87 | version: 2 88 | commit: 89 | jobs: 90 | - build 91 | cron: 92 | triggers: 93 | - schedule: 94 | cron: "20 1 * * *" 95 | filters: 96 | branches: 97 | only: 98 | - master 99 | jobs: 100 | - install_from_composer 101 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.yml] 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME=Bulletins 2 | APP_ENV=local 3 | APP_KEY= 4 | APP_DEBUG=true 5 | APP_URL=http://localhost 6 | APP_LOCALE=en 7 | 8 | LOG_CHANNEL=stack 9 | 10 | DB_CONNECTION=sqlite 11 | 12 | MAIL_DRIVER=smtp 13 | MAIL_HOST=smtp.mailtrap.io 14 | MAIL_PORT=2525 15 | MAIL_USERNAME=null 16 | MAIL_PASSWORD=null 17 | MAIL_ENCRYPTION=null 18 | 19 | PROJECTS_CONFIG_FILENAME=projects.yml 20 | REPORT_TIMEZONE=UTC 21 | REPORT_COUNTRY_CODE=FR 22 | REPORT_EMAIL=hello@example.com 23 | REPORT_SECRET=password 24 | REPORTS_PASSWORD_HINT= 25 | SLACK_TOKEN= 26 | SLACK_GENERAL_CHANNEL= 27 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.css linguist-vendored 3 | *.scss linguist-vendored 4 | *.js linguist-vendored 5 | CHANGELOG.md export-ignore 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 16 | 1. Go to '...' 17 | 2. Click on '....' 18 | 3. Scroll down to '....' 19 | 4. See error 20 | 21 | **Expected behavior** 22 | A clear and concise description of what you expected to happen. 23 | 24 | **Screenshots** 25 | If applicable, add screenshots to help explain your problem. 26 | 27 | **Desktop (please complete the following information):** 28 | - OS: [e.g. iOS] 29 | - Browser [e.g. chrome, safari] 30 | - Version [e.g. 22] 31 | 32 | **Smartphone (please complete the following information):** 33 | - Device: [e.g. iPhone6] 34 | - OS: [e.g. iOS8.1] 35 | - Browser [e.g. stock browser, safari] 36 | - Version [e.g. 22] 37 | 38 | **Additional context** 39 | Add any other context about the problem here. 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /public/hot 3 | /public/storage 4 | /storage/*.key 5 | /vendor 6 | .env 7 | .phpunit.result.cache 8 | Homestead.json 9 | Homestead.yaml 10 | npm-debug.log 11 | yarn-error.log 12 | config/projects.yml 13 | public/images/logos 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome**. Most active contributors will be credited in the README. We accept contributions via pull requests on GitHub. 4 | 5 | ## Pull Requests 6 | 7 | - **[PSR-2 Coding Standard.](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** The easiest way to apply the conventions is to install [PHP CS Fixer](https://github.com/FriendsOfPHP/PHP-CS-Fixer). 8 | - **Ask before working on a feature.** Before spending hours on a feature, open an issue and let's discuss about it. 9 | - **Add tests!** Patches get accepted only when they include tests. 10 | - **Document any change in behaviour.** Make sure the `README.md` and any other relevant documentation are kept up-to-date. 11 | - **Create feature branches.** Small fixes can be pulled from your master branch, but features needs to be pulled from a feature branch. 12 | - **One pull request per feature.** If you want to do more than one thing, send multiple pull requests. 13 | - **Send coherent history.** Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 14 | 15 | *Happy coding!* 16 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:7.2-fpm 2 | 3 | # Install dependencies 4 | RUN apt-get update && apt-get install -y \ 5 | build-essential \ 6 | libpng-dev \ 7 | libjpeg62-turbo-dev \ 8 | libfreetype6-dev \ 9 | locales \ 10 | zip \ 11 | jpegoptim optipng pngquant gifsicle \ 12 | vim \ 13 | unzip \ 14 | git \ 15 | curl 16 | 17 | # Clear cache 18 | RUN apt-get clean && rm -rf /var/lib/apt/lists/* 19 | 20 | # Install extensions 21 | RUN docker-php-ext-install pdo_mysql mbstring zip exif pcntl 22 | RUN docker-php-ext-configure gd --with-gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ --with-png-dir=/usr/include/ 23 | RUN docker-php-ext-install gd 24 | 25 | # Install composer 26 | RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer 27 | 28 | # Add user for laravel application 29 | RUN groupadd -g 1000 www 30 | RUN useradd -u 1000 -ms /bin/bash -g www www 31 | RUN chown -R www:www /var/www 32 | 33 | # Set working directory 34 | WORKDIR /var/www 35 | 36 | RUN rm -rf ./* 37 | 38 | USER www 39 | 40 | RUN composer create-project --prefer-dist --stability=dev entrepreneur-interet-general/bulletins . 41 | 42 | RUN cp ./config/projects.example.yml ./config/projects.yml 43 | 44 | RUN composer install 45 | 46 | # Expose port 9000 and start php-fpm server 47 | EXPOSE 9000 48 | 49 | CMD ["php-fpm"] 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CircleCI build status](https://img.shields.io/circleci/project/github/entrepreneur-interet-general/bulletins.svg?style=flat-square)](https://circleci.com/gh/entrepreneur-interet-general/bulletins) 2 | [![Software License](https://img.shields.io/badge/License-AGPL-orange.svg?style=flat-square)](https://github.com/entrepreneur-interet-general/bulletins/blob/master/LICENSE) 3 | 4 | # What is it 5 | Bulletins is a weekly retrospective tool for multiple projects or teams. It lets people reflect on their past week with 4 questions which can be answered super quickly: 6 | - What's the team mood? 7 | - What were the main goals this week? 8 | - What worked great and what was harder? 9 | - Do we need help? 10 | 11 | It's asynchronous and transparent at its heart. All teams can fill their retrospective when they want through a simple web interface, as long as it's before Friday 3 PM. On Fridays at 3 PM, everyone gets a weekly recap email with all filled retrospectives. The web interface lets everyone browse through previous retrospectives by week or by team. 12 | 13 | ## Documentation 14 | The documentation can be seen online at [bulletins.eig-forever.org](https://bulletins.eig-forever.org). 15 | 16 | ## License 17 | GNU Affero General Public License. Please see the [license file](LICENSE) for more information. 18 | 19 | 2019 Direction interministérielle du numérique et du système d’information et de communication de l'État. Antoine Augusti. 20 | 21 | 2019 Contributors accessible through the Git history. 22 | -------------------------------------------------------------------------------- /app/Console/Kernel.php: -------------------------------------------------------------------------------- 1 | isSameDay($lastWorkingDay); 37 | }; 38 | 39 | $schedule->call(function () { 40 | $text = trans('notifications.general_reminder', ['url' => config('app.url')]); 41 | Slack::sendMessage(config('app.slack_general_channel'), $text); 42 | })->timezone(config('app.report_timezone'))->when($isLatestWorkingDay)->at('10:00'); 43 | 44 | $schedule->call(function () { 45 | $this->unfilledProjects()->map->notify(); 46 | })->timezone(config('app.report_timezone'))->when($isLatestWorkingDay)->at('14:00'); 47 | 48 | $schedule->call(function () { 49 | $this->unfilledProjects()->map->notify(); 50 | })->timezone(config('app.report_timezone'))->when($isLatestWorkingDay)->at('14:45'); 51 | 52 | $schedule->call(function () { 53 | $builder = new WeeklyReport; 54 | if ($builder->hasReports()) { 55 | Mail::to(config('app.report_email'))->send($builder); 56 | } 57 | })->timezone(config('app.report_timezone'))->fridays()->at('15:05'); 58 | } 59 | 60 | private function unfilledProjects() 61 | { 62 | $currentWeek = now()->format('Y-W'); 63 | 64 | return config('app.projects')->active()->unfilledProjectsFor($currentWeek); 65 | } 66 | 67 | /** 68 | * Register the commands for the application. 69 | * 70 | * @return void 71 | */ 72 | protected function commands() 73 | { 74 | $this->load(__DIR__.'/Commands'); 75 | 76 | require base_path('routes/console.php'); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /app/Date.php: -------------------------------------------------------------------------------- 1 | 'datetime:Y-m-d']; 11 | 12 | public function getMonthAttribute() 13 | { 14 | return $this->date->monthName.' '.$this->date->year; 15 | } 16 | 17 | public function scopeForProject($query, $project) 18 | { 19 | return $query->where('project', '=', $project); 20 | } 21 | 22 | public function scopeUpcoming($query) 23 | { 24 | return $query->where('date', '>=', now())->orderBy('date'); 25 | } 26 | 27 | public function scopePast($query) 28 | { 29 | return $query->where('date', '<', now())->orderBy('date'); 30 | } 31 | 32 | public function projectObject() 33 | { 34 | return config('app.projects')->where('name', $this->project)->first(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/Exceptions/Handler.php: -------------------------------------------------------------------------------- 1 | middleware('guest'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/LoginController.php: -------------------------------------------------------------------------------- 1 | middleware('guest')->except('logout'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/RegisterController.php: -------------------------------------------------------------------------------- 1 | middleware('guest'); 41 | } 42 | 43 | /** 44 | * Get a validator for an incoming registration request. 45 | * 46 | * @param array $data 47 | * 48 | * @return \Illuminate\Contracts\Validation\Validator 49 | */ 50 | protected function validator(array $data) 51 | { 52 | return Validator::make($data, [ 53 | 'name' => ['required', 'string', 'max:255'], 54 | 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], 55 | 'password' => ['required', 'string', 'min:8', 'confirmed'], 56 | ]); 57 | } 58 | 59 | /** 60 | * Create a new user instance after a valid registration. 61 | * 62 | * @param array $data 63 | * 64 | * @return \App\User 65 | */ 66 | protected function create(array $data) 67 | { 68 | return User::create([ 69 | 'name' => $data['name'], 70 | 'email' => $data['email'], 71 | 'password' => Hash::make($data['password']), 72 | ]); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/ResetPasswordController.php: -------------------------------------------------------------------------------- 1 | middleware('guest'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/VerificationController.php: -------------------------------------------------------------------------------- 1 | middleware('auth'); 38 | $this->middleware('signed')->only('verify'); 39 | $this->middleware('throttle:6,1')->only('verify', 'resend'); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/Http/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | format('W'); 17 | } 18 | 19 | final protected function weekNumber() 20 | { 21 | return now()->format('Y-W'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/Http/Controllers/DatesController.php: -------------------------------------------------------------------------------- 1 | Date::orderBy('date', 'desc')->get()->groupBy->month, 16 | ]); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/Http/Controllers/HomeController.php: -------------------------------------------------------------------------------- 1 | $projects = config('app.projects')->active(), 14 | 'filledProjects' => $filledProjects = $this->filledProjects(), 15 | 'allFilled' => $filledProjects->count() === $projects->count(), 16 | 'week' => $this->week(), 17 | 'canBeFilled' => Report::canBeFilled(), 18 | ]); 19 | } 20 | 21 | public function login() 22 | { 23 | return view('login', ['passwordHint' => config('app.reports_password_hint')]); 24 | } 25 | 26 | public function authenticate(Request $request) 27 | { 28 | if ($request->input('password') !== config('app.report_secret')) { 29 | return back()->with('error', trans('auth.failed')); 30 | } 31 | 32 | session(['logged_in' => true]); 33 | 34 | return redirect()->intended(route('reports.choose')); 35 | } 36 | 37 | private function filledProjects() 38 | { 39 | return config('app.projects')->active()->filledProjectsFor($this->weekNumber())->names(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/Http/Controllers/LanguagesController.php: -------------------------------------------------------------------------------- 1 | $locale]); 16 | 17 | return redirect()->back(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/Http/Controllers/ReportsController.php: -------------------------------------------------------------------------------- 1 | validate([ 19 | 'spirit' => ['required', Rule::in(['☹️', '😐', '🙂', '😀'])], 20 | 'project' => ['required', Rule::in(config('app.projects')->active()->names())], 21 | 'priorities' => 'required|max:300', 22 | 'victories' => 'required|max:300', 23 | 'help' => 'max:300', 24 | 'key_date' => [ 25 | 'nullable', 26 | 'date', 27 | 'date_format:Y-m-d', 28 | 'required_with:key_date_description', 29 | 'unique:dates,date,null,id,project,'.$request->input('project'), 30 | ], 31 | 'key_date_description' => 'nullable|max:200|required_with:key_date', 32 | ]); 33 | 34 | Report::create([ 35 | 'project' => $request->input('project'), 36 | 'week_number' => $this->weekNumber(), 37 | 'spirit' => $request->input('spirit'), 38 | 'priorities' => $request->input('priorities'), 39 | 'victories' => $request->input('victories'), 40 | 'help' => $request->input('help'), 41 | ])->save(); 42 | 43 | if ($request->filled('key_date')) { 44 | Date::create([ 45 | 'project' => $request->input('project'), 46 | 'date' => $request->input('key_date'), 47 | 'description' => $request->input('key_date_description'), 48 | ]); 49 | } 50 | 51 | return view('success', [ 52 | 'week' => $this->week(), 53 | 'gif' => $this->randomGif(), 54 | ]); 55 | } 56 | 57 | public function choose() 58 | { 59 | abort_if(Report::published()->count() == 0, 404); 60 | 61 | $name = Report::published()->orderBy('project')->first()->project; 62 | 63 | return redirect()->route('reports.index', $name); 64 | } 65 | 66 | public function index(Request $request, Collection $reports) 67 | { 68 | abort_if($reports->count() == 0, 404); 69 | 70 | $currentProject = $request->route()->originalParameter('reports'); 71 | 72 | if (! is_null($request->query('signature'))) { 73 | $projects = collect([$currentProject]); 74 | } else { 75 | $projects = Report::published()->select('project')->distinct()->get()->pluck('project'); 76 | } 77 | 78 | return view('reports.index', [ 79 | 'reports' => $reports->groupBy->month, 80 | 'currentProject' => $reports->first()->projectObject(), 81 | 'projects' => $projects, 82 | 'shareUrl' => URL::signedRoute('reports.index', $currentProject), 83 | 'downloadUrl' => URL::signedRoute('reports.export', $currentProject), 84 | 'upcomingDates' => Date::forProject($currentProject)->upcoming()->get(), 85 | 'pastDates' => Date::forProject($currentProject)->past()->get(), 86 | ]); 87 | } 88 | 89 | public function weekIndex() 90 | { 91 | $weekNumbers = Report::published()->latest()->select('week_number')->distinct(); 92 | 93 | return view('reports.week_index', [ 94 | 'data' => $weekNumbers->get()->groupBy->month, 95 | ]); 96 | } 97 | 98 | public function export(Collection $reports) 99 | { 100 | $headers = [ 101 | 'Content-type' => 'text/csv', 102 | 'Pragma' => 'no-cache', 103 | 'Cache-Control' => 'must-revalidate, post-check=0, pre-check=0', 104 | 'Expires' => '0', 105 | ]; 106 | 107 | $project = $reports->first()->project; 108 | $filename = $project.'-'.now()->format('Y-m-d').'.csv'; 109 | 110 | $callback = function () use ($reports) { 111 | $file = fopen('php://output', 'w'); 112 | fputcsv($file, array_keys(Report::first()->toArray())); 113 | 114 | foreach ($reports as $report) { 115 | fputcsv($file, $report->toArray()); 116 | } 117 | fclose($file); 118 | }; 119 | 120 | return response()->streamDownload($callback, $filename, $headers); 121 | } 122 | 123 | private function randomGif() 124 | { 125 | return (object) collect([ 126 | ['id' => '4Zo41lhzKt6iZ8xff9', 'width' => 480, 'height' => 480], 127 | ['id' => 'Ztw0p2RGR36E0', 'width' => 480, 'height' => 270], 128 | ['id' => 'SwImQhtiNA7io', 'width' => 480, 'height' => 297], 129 | ['id' => 'eYilisUwipOEM', 'width' => 480, 'height' => 348], 130 | ['id' => '1136UBdSNn6Bu8', 'width' => 250, 'height' => 163], 131 | ])->shuffle()->first(); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /app/Http/Kernel.php: -------------------------------------------------------------------------------- 1 | [ 33 | \App\Http\Middleware\EncryptCookies::class, 34 | \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, 35 | \Illuminate\Session\Middleware\StartSession::class, 36 | // \Illuminate\Session\Middleware\AuthenticateSession::class, 37 | \Illuminate\View\Middleware\ShareErrorsFromSession::class, 38 | \App\Http\Middleware\VerifyCsrfToken::class, 39 | \Illuminate\Routing\Middleware\SubstituteBindings::class, 40 | \App\Http\Middleware\RestoreLocale::class, 41 | \App\Http\Middleware\ReferrerPolicy::class, 42 | ], 43 | 44 | 'api' => [ 45 | 'throttle:60,1', 46 | \Illuminate\Routing\Middleware\SubstituteBindings::class, 47 | ], 48 | ]; 49 | 50 | /** 51 | * The application's route middleware. 52 | * 53 | * These middleware may be assigned to groups or used individually. 54 | * 55 | * @var array 56 | */ 57 | protected $routeMiddleware = [ 58 | 'auth' => \App\Http\Middleware\Authenticate::class, 59 | 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 60 | 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class, 61 | 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, 62 | 'can' => \Illuminate\Auth\Middleware\Authorize::class, 63 | 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 64 | 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, 65 | 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, 66 | 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 67 | 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, 68 | 'logged_in' => SeeReportsHistory::class, 69 | ]; 70 | } 71 | -------------------------------------------------------------------------------- /app/Http/Middleware/Authenticate.php: -------------------------------------------------------------------------------- 1 | expectsJson()) { 19 | return route('login'); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/Http/Middleware/CheckForMaintenanceMode.php: -------------------------------------------------------------------------------- 1 | check()) { 22 | return redirect('/home'); 23 | } 24 | 25 | return $next($request); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Http/Middleware/ReferrerPolicy.php: -------------------------------------------------------------------------------- 1 | header('Referrer-Policy', 'no-referrer'); 23 | } 24 | 25 | return $response; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Http/Middleware/RestoreLocale.php: -------------------------------------------------------------------------------- 1 | setLocale($request->session()->get('locale', config()->get('app.locale'))); 19 | 20 | return $next($request); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/Http/Middleware/SeeReportsHistory.php: -------------------------------------------------------------------------------- 1 | session()->has('logged_in'); 13 | 14 | if ($loggedIn or $request->hasValidSignature()) { 15 | return $next($request); 16 | } 17 | 18 | return redirect()->guest(route('login')); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/Http/Middleware/TrimStrings.php: -------------------------------------------------------------------------------- 1 | project = $project; 19 | $this->url = $url; 20 | } 21 | 22 | public function build() 23 | { 24 | return $this->markdown('emails.fill_reminder'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/Mail/WeeklyReport.php: -------------------------------------------------------------------------------- 1 | week = $week ?? now()->format('Y-W'); 22 | 23 | if (! preg_match('/^\d{4}-\d{2}$/', $this->week)) { 24 | throw new UnexpectedValueException($week.' is not a valid week.'); 25 | } 26 | } 27 | 28 | public function hasReports() 29 | { 30 | return Report::forWeek($this->week)->count() > 0; 31 | } 32 | 33 | public function build() 34 | { 35 | if (! $this->hasReports()) { 36 | throw new InvalidArgumentException('No reports for week '.$this->week); 37 | } 38 | 39 | $reports = Report::forWeek($this->week)->get()->shuffle(); 40 | 41 | $helpRequests = Report::forWeek($this->week)->orderBy('project')->pluck('help', 'project')->filter(); 42 | $projectsNoInfo = config('app.projects')->active()->unfilledProjectsFor($this->week)->map->name; 43 | 44 | $subject = trans('emails.subject', ['week' => $this->week]); 45 | 46 | return $this->markdown('emails.report', [ 47 | 'reports' => $reports, 48 | 'upcomingDates' => $this->upcomingDates($reports), 49 | 'weekNumber' => $this->week, 50 | 'helpRequests' => $helpRequests, 51 | 'projectsNoInfo' => $projectsNoInfo, 52 | ])->subject($subject); 53 | } 54 | 55 | private function upcomingDates($reports) 56 | { 57 | $friday = $reports->first()->endOfWeek; 58 | $nextFriday = $friday->copy()->addWeek(1); 59 | 60 | return Date::whereBetween('date', [$friday, $nextFriday])->orderBy('date')->get(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/Project.php: -------------------------------------------------------------------------------- 1 | name = $attributes['name']; 26 | $this->channel = $attributes['channel']; 27 | $this->members = $attributes['members']; 28 | $this->logoUrl = $attributes['logoUrl']; 29 | $this->endsOn = is_null($attributes['endsOn']) ? null : Carbon::parse($attributes['endsOn']); 30 | } 31 | 32 | public function isActive() 33 | { 34 | return is_null($this->endsOn) || $this->endsOn->isFuture(); 35 | } 36 | 37 | public function notify() 38 | { 39 | switch ($this->channel) { 40 | case 'slack': 41 | $this->slackNotify(); 42 | break; 43 | 44 | case 'email': 45 | $this->emailNotify(); 46 | break; 47 | 48 | default: 49 | return; 50 | } 51 | } 52 | 53 | private function emailNotify() 54 | { 55 | foreach ($this->members as $member) { 56 | Mail::to($member)->send(new FillBulletinReminder($this->name, $this->fillUrl())); 57 | } 58 | } 59 | 60 | private function slackNotify() 61 | { 62 | $text = trans('notifications.individual_reminder', ['project' => $this->name, 'url' => $this->fillUrl()]); 63 | 64 | foreach ($this->members as $member) { 65 | Slack::sendMessage($member, $text); 66 | } 67 | } 68 | 69 | private function fillUrl() 70 | { 71 | return route('home', ['project' => $this->name]); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/Projects.php: -------------------------------------------------------------------------------- 1 | map(function ($project) { 21 | $attributes = [ 22 | 'name' => $project['name'], 23 | 'channel' => Arr::get($project, 'notification'), 24 | 'members' => $project['members'], 25 | 'logoUrl' => $project['logo'], 26 | 'endsOn' => Arr::get($project, 'ends_on'), 27 | ]; 28 | 29 | return new Project($attributes); 30 | })->sortBy('name'); 31 | 32 | return new self($projects); 33 | } 34 | 35 | public function active() 36 | { 37 | return $this->filter->isActive(); 38 | } 39 | 40 | public function names() 41 | { 42 | return $this->map->name; 43 | } 44 | 45 | public function add($item) 46 | { 47 | if ($this->contains($item->name)) { 48 | throw new UnexpectedValueException; 49 | } 50 | 51 | parent::add($item); 52 | } 53 | 54 | public function filledProjectsFor($week) 55 | { 56 | $this->checkIsWeek($week); 57 | 58 | return $this->whereIn('name', Report::where('week_number', $week)->pluck('project')); 59 | } 60 | 61 | public function unfilledProjectsFor($week) 62 | { 63 | $this->checkIsWeek($week); 64 | 65 | return $this->whereNotIn('name', Report::where('week_number', $week)->pluck('project')); 66 | } 67 | 68 | private function checkIsWeek($week) 69 | { 70 | if (! preg_match('/^\d{4}-\d{2}$/', $week)) { 71 | throw new UnexpectedValueException($week.' is not a valid week.'); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/Providers/AppServiceProvider.php: -------------------------------------------------------------------------------- 1 | 'App\Policies\ModelPolicy', 16 | ]; 17 | 18 | /** 19 | * Register any authentication / authorization services. 20 | * 21 | * @return void 22 | */ 23 | public function boot() 24 | { 25 | $this->registerPolicies(); 26 | 27 | // 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Providers/BroadcastServiceProvider.php: -------------------------------------------------------------------------------- 1 | [ 19 | SendEmailVerificationNotification::class, 20 | ], 21 | ]; 22 | 23 | /** 24 | * Register any events for your application. 25 | * 26 | * @return void 27 | */ 28 | public function boot() 29 | { 30 | parent::boot(); 31 | 32 | // 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/Providers/RouteServiceProvider.php: -------------------------------------------------------------------------------- 1 | published() 32 | ->orderBy('week_number', 'DESC') 33 | ->get() ?? abort(404); 34 | }); 35 | } 36 | 37 | /** 38 | * Define the routes for the application. 39 | * 40 | * @return void 41 | */ 42 | public function map() 43 | { 44 | $this->mapApiRoutes(); 45 | 46 | $this->mapWebRoutes(); 47 | 48 | // 49 | } 50 | 51 | /** 52 | * Define the "web" routes for the application. 53 | * 54 | * These routes all receive session state, CSRF protection, etc. 55 | * 56 | * @return void 57 | */ 58 | protected function mapWebRoutes() 59 | { 60 | Route::middleware('web') 61 | ->namespace($this->namespace) 62 | ->group(base_path('routes/web.php')); 63 | } 64 | 65 | /** 66 | * Define the "api" routes for the application. 67 | * 68 | * These routes are typically stateless. 69 | * 70 | * @return void 71 | */ 72 | protected function mapApiRoutes() 73 | { 74 | Route::prefix('api') 75 | ->middleware('api') 76 | ->namespace($this->namespace) 77 | ->group(base_path('routes/api.php')); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/Report.php: -------------------------------------------------------------------------------- 1 | week_number); 17 | 18 | return now()->setIsoDate($year, $week)->startOfWeek(); 19 | } 20 | 21 | public function getEndOfWeekAttribute() 22 | { 23 | return $this->startOfWeek->next(Carbon::FRIDAY); 24 | } 25 | 26 | public function getMonthAttribute() 27 | { 28 | return $this->startOfWeek->monthName.' '.$this->startOfWeek->year; 29 | } 30 | 31 | public static function canBeFilled() 32 | { 33 | $now = now(config('app.report_timezone')); 34 | $target = Carbon::parse('Friday this week 15:05', config('app.report_timezone')); 35 | 36 | return $now < $target; 37 | } 38 | 39 | public static function latestPublishedWeek() 40 | { 41 | $startOfWeek = now()->startOfWeek(); 42 | 43 | if (self::canBeFilled()) { 44 | return $startOfWeek->subWeek(1)->format('Y-W'); 45 | } 46 | 47 | return $startOfWeek->format('Y-W'); 48 | } 49 | 50 | public static function lastWorkingDayOfWeek() 51 | { 52 | $now = now(config('app.report_timezone')); 53 | $candidate = Carbon::parse('Friday this week', config('app.report_timezone')); 54 | 55 | try { 56 | $country = Yasumi::getProviders()[config('app.report_country_code')]; 57 | 58 | return Yasumi::prevWorkingDay($country, $candidate->addDay()); 59 | } catch (\Exception $e) { 60 | return $candidate; 61 | } 62 | } 63 | 64 | public function scopePublished($query) 65 | { 66 | return $query->where('week_number', '<=', self::latestPublishedWeek()); 67 | } 68 | 69 | public function scopeForWeek($query, $week) 70 | { 71 | return $query->where('week_number', $week); 72 | } 73 | 74 | public function projectObject() 75 | { 76 | return config('app.projects')->where('name', $this->project)->first(); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /app/Slack.php: -------------------------------------------------------------------------------- 1 | chat->postMessage(compact('channel', 'text')); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/User.php: -------------------------------------------------------------------------------- 1 | 'datetime', 37 | ]; 38 | } 39 | -------------------------------------------------------------------------------- /artisan: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | make(Illuminate\Contracts\Console\Kernel::class); 34 | 35 | $status = $kernel->handle( 36 | $input = new Symfony\Component\Console\Input\ArgvInput, 37 | new Symfony\Component\Console\Output\ConsoleOutput 38 | ); 39 | 40 | /* 41 | |-------------------------------------------------------------------------- 42 | | Shutdown The Application 43 | |-------------------------------------------------------------------------- 44 | | 45 | | Once Artisan has finished running, we will fire off the shutdown events 46 | | so that any final work may be done by the application before we shut 47 | | down the process. This is the last thing to happen to the request. 48 | | 49 | */ 50 | 51 | $kernel->terminate($input, $status); 52 | 53 | exit($status); 54 | -------------------------------------------------------------------------------- /bootstrap/app.php: -------------------------------------------------------------------------------- 1 | singleton( 30 | Illuminate\Contracts\Http\Kernel::class, 31 | App\Http\Kernel::class 32 | ); 33 | 34 | $app->singleton( 35 | Illuminate\Contracts\Console\Kernel::class, 36 | App\Console\Kernel::class 37 | ); 38 | 39 | $app->singleton( 40 | Illuminate\Contracts\Debug\ExceptionHandler::class, 41 | App\Exceptions\Handler::class 42 | ); 43 | 44 | /* 45 | |-------------------------------------------------------------------------- 46 | | Return The Application 47 | |-------------------------------------------------------------------------- 48 | | 49 | | This script returns the application instance. The instance is given to 50 | | the calling script so we can separate the building of the instances 51 | | from the actual running of the application and sending responses. 52 | | 53 | */ 54 | 55 | return $app; 56 | -------------------------------------------------------------------------------- /bootstrap/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"entrepreneur-interet-general/bulletins", 3 | "type":"project", 4 | "description":"Bulletins is a simple weekly retrospective tool for multiple projects or teams", 5 | "keywords":[ 6 | "project-management", 7 | "reporting", 8 | "reports", 9 | "retrospective", 10 | "standup", 11 | "archive", 12 | "bulletin", 13 | "bulletins", 14 | "laravel" 15 | ], 16 | "license":"AGPL-3.0", 17 | "authors":[ 18 | { 19 | "name":"Antoine Augusti", 20 | "email":"antoine.augusti@data.gouv.fr", 21 | "homepage":"https://www.antoine-augusti.fr", 22 | "role":"Developer" 23 | } 24 | ], 25 | "support":{ 26 | "issues":"https://github.com/entrepreneur-interet-general/bulletins/issues" 27 | }, 28 | "require":{ 29 | "php": "^7.2.5", 30 | "azuyalabs/yasumi": "^2.2", 31 | "bugsnag/bugsnag-laravel": "^2.0", 32 | "fideloper/proxy": "^4.0", 33 | "fruitcake/laravel-cors": "^1.0", 34 | "laravel/framework": "^7.0", 35 | "laravel/tinker": "^2.2", 36 | "symfony/yaml": "^4.3", 37 | "wrapi/slack": "^1.0" 38 | }, 39 | "require-dev": { 40 | "filp/whoops": "^2.0", 41 | "fzaninotto/faker": "^1.4", 42 | "mockery/mockery": "^1.0", 43 | "nunomaduro/collision": "^4.1", 44 | "phpunit/phpunit": "^8.0" 45 | }, 46 | "config":{ 47 | "optimize-autoloader":true, 48 | "preferred-install":"dist", 49 | "sort-packages":true 50 | }, 51 | "extra":{ 52 | "laravel":{ 53 | "dont-discover":[ 54 | 55 | ] 56 | } 57 | }, 58 | "autoload":{ 59 | "psr-4":{ 60 | "App\\":"app/" 61 | }, 62 | "classmap":[ 63 | "database/seeds", 64 | "database/factories" 65 | ] 66 | }, 67 | "autoload-dev":{ 68 | "psr-4":{ 69 | "Tests\\":"tests/" 70 | } 71 | }, 72 | "minimum-stability":"dev", 73 | "prefer-stable":true, 74 | "scripts":{ 75 | "post-autoload-dump":[ 76 | "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", 77 | "php artisan package:discover --ansi" 78 | ], 79 | "post-root-package-install":[ 80 | "php -r \"file_exists('.env') || copy('.env.example', '.env');\"" 81 | ], 82 | "post-create-project-cmd":[ 83 | "php artisan key:generate --ansi" 84 | ] 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /config/auth.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'guard' => 'web', 18 | 'passwords' => 'users', 19 | ], 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Authentication Guards 24 | |-------------------------------------------------------------------------- 25 | | 26 | | Next, you may define every authentication guard for your application. 27 | | Of course, a great default configuration has been defined for you 28 | | here which uses session storage and the Eloquent user provider. 29 | | 30 | | All authentication drivers have a user provider. This defines how the 31 | | users are actually retrieved out of your database or other storage 32 | | mechanisms used by this application to persist your user's data. 33 | | 34 | | Supported: "session", "token" 35 | | 36 | */ 37 | 38 | 'guards' => [ 39 | 'web' => [ 40 | 'driver' => 'session', 41 | 'provider' => 'users', 42 | ], 43 | 44 | 'api' => [ 45 | 'driver' => 'token', 46 | 'provider' => 'users', 47 | 'hash' => false, 48 | ], 49 | ], 50 | 51 | /* 52 | |-------------------------------------------------------------------------- 53 | | User Providers 54 | |-------------------------------------------------------------------------- 55 | | 56 | | All authentication drivers have a user provider. This defines how the 57 | | users are actually retrieved out of your database or other storage 58 | | mechanisms used by this application to persist your user's data. 59 | | 60 | | If you have multiple user tables or models you may configure multiple 61 | | sources which represent each model / table. These sources may then 62 | | be assigned to any extra authentication guards you have defined. 63 | | 64 | | Supported: "database", "eloquent" 65 | | 66 | */ 67 | 68 | 'providers' => [ 69 | 'users' => [ 70 | 'driver' => 'eloquent', 71 | 'model' => App\User::class, 72 | ], 73 | 74 | // 'users' => [ 75 | // 'driver' => 'database', 76 | // 'table' => 'users', 77 | // ], 78 | ], 79 | 80 | /* 81 | |-------------------------------------------------------------------------- 82 | | Resetting Passwords 83 | |-------------------------------------------------------------------------- 84 | | 85 | | You may specify multiple password reset configurations if you have more 86 | | than one user table or model in the application and you want to have 87 | | separate password reset settings based on the specific user types. 88 | | 89 | | The expire time is the number of minutes that the reset token should be 90 | | considered valid. This security feature keeps tokens short-lived so 91 | | they have less time to be guessed. You may change this as needed. 92 | | 93 | */ 94 | 95 | 'passwords' => [ 96 | 'users' => [ 97 | 'provider' => 'users', 98 | 'table' => 'password_resets', 99 | 'expire' => 60, 100 | ], 101 | ], 102 | 103 | ]; 104 | -------------------------------------------------------------------------------- /config/broadcasting.php: -------------------------------------------------------------------------------- 1 | env('BROADCAST_DRIVER', 'null'), 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Broadcast Connections 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may define all of the broadcast connections that will be used 26 | | to broadcast events to other systems or over websockets. Samples of 27 | | each available type of connection are provided inside this array. 28 | | 29 | */ 30 | 31 | 'connections' => [ 32 | 33 | 'pusher' => [ 34 | 'driver' => 'pusher', 35 | 'key' => env('PUSHER_APP_KEY'), 36 | 'secret' => env('PUSHER_APP_SECRET'), 37 | 'app_id' => env('PUSHER_APP_ID'), 38 | 'options' => [ 39 | 'cluster' => env('PUSHER_APP_CLUSTER'), 40 | 'encrypted' => true, 41 | ], 42 | ], 43 | 44 | 'redis' => [ 45 | 'driver' => 'redis', 46 | 'connection' => 'default', 47 | ], 48 | 49 | 'log' => [ 50 | 'driver' => 'log', 51 | ], 52 | 53 | 'null' => [ 54 | 'driver' => 'null', 55 | ], 56 | 57 | ], 58 | 59 | ]; 60 | -------------------------------------------------------------------------------- /config/cache.php: -------------------------------------------------------------------------------- 1 | env('CACHE_DRIVER', 'file'), 22 | 23 | /* 24 | |-------------------------------------------------------------------------- 25 | | Cache Stores 26 | |-------------------------------------------------------------------------- 27 | | 28 | | Here you may define all of the cache "stores" for your application as 29 | | well as their drivers. You may even define multiple stores for the 30 | | same cache driver to group types of items stored in your caches. 31 | | 32 | */ 33 | 34 | 'stores' => [ 35 | 36 | 'apc' => [ 37 | 'driver' => 'apc', 38 | ], 39 | 40 | 'array' => [ 41 | 'driver' => 'array', 42 | ], 43 | 44 | 'database' => [ 45 | 'driver' => 'database', 46 | 'table' => 'cache', 47 | 'connection' => null, 48 | ], 49 | 50 | 'file' => [ 51 | 'driver' => 'file', 52 | 'path' => storage_path('framework/cache/data'), 53 | ], 54 | 55 | 'memcached' => [ 56 | 'driver' => 'memcached', 57 | 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), 58 | 'sasl' => [ 59 | env('MEMCACHED_USERNAME'), 60 | env('MEMCACHED_PASSWORD'), 61 | ], 62 | 'options' => [ 63 | // Memcached::OPT_CONNECT_TIMEOUT => 2000, 64 | ], 65 | 'servers' => [ 66 | [ 67 | 'host' => env('MEMCACHED_HOST', '127.0.0.1'), 68 | 'port' => env('MEMCACHED_PORT', 11211), 69 | 'weight' => 100, 70 | ], 71 | ], 72 | ], 73 | 74 | 'redis' => [ 75 | 'driver' => 'redis', 76 | 'connection' => 'cache', 77 | ], 78 | 79 | 'dynamodb' => [ 80 | 'driver' => 'dynamodb', 81 | 'key' => env('AWS_ACCESS_KEY_ID'), 82 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 83 | 'region' => env('AWS_REGION', 'us-east-1'), 84 | 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), 85 | ], 86 | 87 | ], 88 | 89 | /* 90 | |-------------------------------------------------------------------------- 91 | | Cache Key Prefix 92 | |-------------------------------------------------------------------------- 93 | | 94 | | When utilizing a RAM based store such as APC or Memcached, there might 95 | | be other applications utilizing the same cache. So, we'll specify a 96 | | value to get prefixed to all our keys so we can avoid collisions. 97 | | 98 | */ 99 | 100 | 'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache'), 101 | 102 | ]; 103 | -------------------------------------------------------------------------------- /config/cors.php: -------------------------------------------------------------------------------- 1 | ['api/*'], 19 | 20 | 'allowed_methods' => ['*'], 21 | 22 | 'allowed_origins' => ['*'], 23 | 24 | 'allowed_origins_patterns' => [], 25 | 26 | 'allowed_headers' => ['*'], 27 | 28 | 'exposed_headers' => false, 29 | 30 | 'max_age' => false, 31 | 32 | 'supports_credentials' => false, 33 | 34 | ]; 35 | -------------------------------------------------------------------------------- /config/database.php: -------------------------------------------------------------------------------- 1 | env('DB_CONNECTION', 'mysql'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Database Connections 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Here are each of the database connections setup for your application. 24 | | Of course, examples of configuring each database platform that is 25 | | supported by Laravel is shown below to make development simple. 26 | | 27 | | 28 | | All database work in Laravel is done through the PHP PDO facilities 29 | | so make sure you have the driver for your particular database of 30 | | choice installed on your machine before you begin development. 31 | | 32 | */ 33 | 34 | 'connections' => [ 35 | 36 | 'sqlite' => [ 37 | 'driver' => 'sqlite', 38 | 'database' => env('DB_DATABASE', database_path('database.sqlite')), 39 | 'prefix' => '', 40 | 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), 41 | ], 42 | 43 | 'mysql' => [ 44 | 'driver' => 'mysql', 45 | 'host' => env('DB_HOST', '127.0.0.1'), 46 | 'port' => env('DB_PORT', '3306'), 47 | 'database' => env('DB_DATABASE', 'forge'), 48 | 'username' => env('DB_USERNAME', 'forge'), 49 | 'password' => env('DB_PASSWORD', ''), 50 | 'unix_socket' => env('DB_SOCKET', ''), 51 | 'charset' => 'utf8mb4', 52 | 'collation' => 'utf8mb4_unicode_ci', 53 | 'prefix' => '', 54 | 'prefix_indexes' => true, 55 | 'strict' => true, 56 | 'engine' => null, 57 | 'options' => array_filter([ 58 | // PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), 59 | ]), 60 | ], 61 | 62 | 'pgsql' => [ 63 | 'driver' => 'pgsql', 64 | 'host' => env('DB_HOST', '127.0.0.1'), 65 | 'port' => env('DB_PORT', '5432'), 66 | 'database' => env('DB_DATABASE', 'forge'), 67 | 'username' => env('DB_USERNAME', 'forge'), 68 | 'password' => env('DB_PASSWORD', ''), 69 | 'charset' => 'utf8', 70 | 'prefix' => '', 71 | 'prefix_indexes' => true, 72 | 'schema' => 'public', 73 | 'sslmode' => 'prefer', 74 | ], 75 | 76 | 'sqlsrv' => [ 77 | 'driver' => 'sqlsrv', 78 | 'host' => env('DB_HOST', 'localhost'), 79 | 'port' => env('DB_PORT', '1433'), 80 | 'database' => env('DB_DATABASE', 'forge'), 81 | 'username' => env('DB_USERNAME', 'forge'), 82 | 'password' => env('DB_PASSWORD', ''), 83 | 'charset' => 'utf8', 84 | 'prefix' => '', 85 | 'prefix_indexes' => true, 86 | ], 87 | 88 | ], 89 | 90 | /* 91 | |-------------------------------------------------------------------------- 92 | | Migration Repository Table 93 | |-------------------------------------------------------------------------- 94 | | 95 | | This table keeps track of all the migrations that have already run for 96 | | your application. Using this information, we can determine which of 97 | | the migrations on disk haven't actually been run in the database. 98 | | 99 | */ 100 | 101 | 'migrations' => 'migrations', 102 | 103 | /* 104 | |-------------------------------------------------------------------------- 105 | | Redis Databases 106 | |-------------------------------------------------------------------------- 107 | | 108 | | Redis is an open source, fast, and advanced key-value store that also 109 | | provides a richer body of commands than a typical key-value system 110 | | such as APC or Memcached. Laravel makes it easy to dig right in. 111 | | 112 | */ 113 | 114 | 'redis' => [ 115 | 116 | 'client' => env('REDIS_CLIENT', 'phpredis'), 117 | 118 | 'options' => [ 119 | 'cluster' => env('REDIS_CLUSTER', 'redis'), 120 | ], 121 | 122 | 'default' => [ 123 | 'host' => env('REDIS_HOST', '127.0.0.1'), 124 | 'password' => env('REDIS_PASSWORD', null), 125 | 'port' => env('REDIS_PORT', 6379), 126 | 'database' => env('REDIS_DB', 0), 127 | ], 128 | 129 | 'cache' => [ 130 | 'host' => env('REDIS_HOST', '127.0.0.1'), 131 | 'password' => env('REDIS_PASSWORD', null), 132 | 'port' => env('REDIS_PORT', 6379), 133 | 'database' => env('REDIS_CACHE_DB', 1), 134 | ], 135 | 136 | ], 137 | 138 | ]; 139 | -------------------------------------------------------------------------------- /config/filesystems.php: -------------------------------------------------------------------------------- 1 | env('FILESYSTEM_DRIVER', 'local'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Default Cloud Filesystem Disk 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Many applications store files both locally and in the cloud. For this 24 | | reason, you may specify a default "cloud" driver here. This driver 25 | | will be bound as the Cloud disk implementation in the container. 26 | | 27 | */ 28 | 29 | 'cloud' => env('FILESYSTEM_CLOUD', 's3'), 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | Filesystem Disks 34 | |-------------------------------------------------------------------------- 35 | | 36 | | Here you may configure as many filesystem "disks" as you wish, and you 37 | | may even configure multiple disks of the same driver. Defaults have 38 | | been setup for each driver as an example of the required options. 39 | | 40 | | Supported Drivers: "local", "ftp", "sftp", "s3", "rackspace" 41 | | 42 | */ 43 | 44 | 'disks' => [ 45 | 46 | 'local' => [ 47 | 'driver' => 'local', 48 | 'root' => storage_path('app'), 49 | ], 50 | 51 | 'public' => [ 52 | 'driver' => 'local', 53 | 'root' => storage_path('app/public'), 54 | 'url' => env('APP_URL').'/storage', 55 | 'visibility' => 'public', 56 | ], 57 | 58 | 's3' => [ 59 | 'driver' => 's3', 60 | 'key' => env('AWS_ACCESS_KEY_ID'), 61 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 62 | 'region' => env('AWS_DEFAULT_REGION'), 63 | 'bucket' => env('AWS_BUCKET'), 64 | 'url' => env('AWS_URL'), 65 | ], 66 | 67 | ], 68 | 69 | ]; 70 | -------------------------------------------------------------------------------- /config/hashing.php: -------------------------------------------------------------------------------- 1 | 'bcrypt', 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Bcrypt Options 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may specify the configuration options that should be used when 26 | | passwords are hashed using the Bcrypt algorithm. This will allow you 27 | | to control the amount of time it takes to hash the given password. 28 | | 29 | */ 30 | 31 | 'bcrypt' => [ 32 | 'rounds' => env('BCRYPT_ROUNDS', 10), 33 | ], 34 | 35 | /* 36 | |-------------------------------------------------------------------------- 37 | | Argon Options 38 | |-------------------------------------------------------------------------- 39 | | 40 | | Here you may specify the configuration options that should be used when 41 | | passwords are hashed using the Argon algorithm. These will allow you 42 | | to control the amount of time it takes to hash the given password. 43 | | 44 | */ 45 | 46 | 'argon' => [ 47 | 'memory' => 1024, 48 | 'threads' => 2, 49 | 'time' => 2, 50 | ], 51 | 52 | ]; 53 | -------------------------------------------------------------------------------- /config/logging.php: -------------------------------------------------------------------------------- 1 | env('LOG_CHANNEL', 'stack'), 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Log Channels 24 | |-------------------------------------------------------------------------- 25 | | 26 | | Here you may configure the log channels for your application. Out of 27 | | the box, Laravel uses the Monolog PHP logging library. This gives 28 | | you a variety of powerful log handlers / formatters to utilize. 29 | | 30 | | Available Drivers: "single", "daily", "slack", "syslog", 31 | | "errorlog", "monolog", 32 | | "custom", "stack" 33 | | 34 | */ 35 | 36 | 'channels' => [ 37 | 'stack' => [ 38 | 'driver' => 'stack', 39 | 'channels' => ['single', 'bugsnag'], 40 | 'ignore_exceptions' => false, 41 | ], 42 | 43 | 'bugsnag' => [ 44 | 'driver' => 'bugsnag', 45 | ], 46 | 47 | 'single' => [ 48 | 'driver' => 'single', 49 | 'path' => storage_path('logs/laravel.log'), 50 | 'level' => 'debug', 51 | ], 52 | 53 | 'daily' => [ 54 | 'driver' => 'daily', 55 | 'path' => storage_path('logs/laravel.log'), 56 | 'level' => 'debug', 57 | 'days' => 14, 58 | ], 59 | 60 | 'slack' => [ 61 | 'driver' => 'slack', 62 | 'url' => env('LOG_SLACK_WEBHOOK_URL'), 63 | 'username' => 'Laravel Log', 64 | 'emoji' => ':boom:', 65 | 'level' => 'critical', 66 | ], 67 | 68 | 'papertrail' => [ 69 | 'driver' => 'monolog', 70 | 'level' => 'debug', 71 | 'handler' => SyslogUdpHandler::class, 72 | 'handler_with' => [ 73 | 'host' => env('PAPERTRAIL_URL'), 74 | 'port' => env('PAPERTRAIL_PORT'), 75 | ], 76 | ], 77 | 78 | 'stderr' => [ 79 | 'driver' => 'monolog', 80 | 'handler' => StreamHandler::class, 81 | 'formatter' => env('LOG_STDERR_FORMATTER'), 82 | 'with' => [ 83 | 'stream' => 'php://stderr', 84 | ], 85 | ], 86 | 87 | 'syslog' => [ 88 | 'driver' => 'syslog', 89 | 'level' => 'debug', 90 | ], 91 | 92 | 'errorlog' => [ 93 | 'driver' => 'errorlog', 94 | 'level' => 'debug', 95 | ], 96 | ], 97 | 98 | ]; 99 | -------------------------------------------------------------------------------- /config/mail.php: -------------------------------------------------------------------------------- 1 | env('MAIL_DRIVER', 'smtp'), 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | SMTP Host Address 24 | |-------------------------------------------------------------------------- 25 | | 26 | | Here you may provide the host address of the SMTP server used by your 27 | | applications. A default option is provided that is compatible with 28 | | the Mailgun mail service which will provide reliable deliveries. 29 | | 30 | */ 31 | 32 | 'host' => env('MAIL_HOST', 'smtp.mailgun.org'), 33 | 34 | /* 35 | |-------------------------------------------------------------------------- 36 | | SMTP Host Port 37 | |-------------------------------------------------------------------------- 38 | | 39 | | This is the SMTP port used by your application to deliver e-mails to 40 | | users of the application. Like the host we have set this value to 41 | | stay compatible with the Mailgun e-mail application by default. 42 | | 43 | */ 44 | 45 | 'port' => env('MAIL_PORT', 587), 46 | 47 | /* 48 | |-------------------------------------------------------------------------- 49 | | Global "From" Address 50 | |-------------------------------------------------------------------------- 51 | | 52 | | You may wish for all e-mails sent by your application to be sent from 53 | | the same address. Here, you may specify a name and address that is 54 | | used globally for all e-mails that are sent by your application. 55 | | 56 | */ 57 | 58 | 'from' => [ 59 | 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), 60 | 'name' => env('MAIL_FROM_NAME', 'Example'), 61 | ], 62 | 63 | /* 64 | |-------------------------------------------------------------------------- 65 | | E-Mail Encryption Protocol 66 | |-------------------------------------------------------------------------- 67 | | 68 | | Here you may specify the encryption protocol that should be used when 69 | | the application send e-mail messages. A sensible default using the 70 | | transport layer security protocol should provide great security. 71 | | 72 | */ 73 | 74 | 'encryption' => env('MAIL_ENCRYPTION', 'tls'), 75 | 76 | /* 77 | |-------------------------------------------------------------------------- 78 | | SMTP Server Username 79 | |-------------------------------------------------------------------------- 80 | | 81 | | If your SMTP server requires a username for authentication, you should 82 | | set it here. This will get used to authenticate with your server on 83 | | connection. You may also set the "password" value below this one. 84 | | 85 | */ 86 | 87 | 'username' => env('MAIL_USERNAME'), 88 | 89 | 'password' => env('MAIL_PASSWORD'), 90 | 91 | /* 92 | |-------------------------------------------------------------------------- 93 | | Sendmail System Path 94 | |-------------------------------------------------------------------------- 95 | | 96 | | When using the "sendmail" driver to send e-mails, we will need to know 97 | | the path to where Sendmail lives on this server. A default path has 98 | | been provided here, which will work well on most of your systems. 99 | | 100 | */ 101 | 102 | 'sendmail' => '/usr/sbin/sendmail -bs', 103 | 104 | /* 105 | |-------------------------------------------------------------------------- 106 | | Markdown Mail Settings 107 | |-------------------------------------------------------------------------- 108 | | 109 | | If you are using Markdown based email rendering, you may configure your 110 | | theme and component paths here, allowing you to customize the design 111 | | of the emails. Or, you may simply stick with the Laravel defaults! 112 | | 113 | */ 114 | 115 | 'markdown' => [ 116 | 'theme' => 'default', 117 | 118 | 'paths' => [ 119 | resource_path('views/vendor/mail'), 120 | ], 121 | ], 122 | 123 | /* 124 | |-------------------------------------------------------------------------- 125 | | Log Channel 126 | |-------------------------------------------------------------------------- 127 | | 128 | | If you are using the "log" driver, you may specify the logging channel 129 | | if you prefer to keep mail messages separate from other log entries 130 | | for simpler reading. Otherwise, the default channel will be used. 131 | | 132 | */ 133 | 134 | 'log_channel' => env('MAIL_LOG_CHANNEL'), 135 | 136 | ]; 137 | -------------------------------------------------------------------------------- /config/projects.example.yml: -------------------------------------------------------------------------------- 1 | - 2 | name: "Example project" 3 | notification: slack 4 | logo: images/logos/example.png 5 | members: 6 | - UEMA8DE9Y 7 | - UEN897F5U 8 | - 9 | name: "Bar" 10 | notification: email 11 | logo: images/logos/bar.png 12 | members: 13 | - bar@example.com 14 | - 15 | name: "Inactive project" 16 | notification: email 17 | logo: images/logos/example.png 18 | members: 19 | - inactive@example.com 20 | ends_on: "2000-01-01" 21 | -------------------------------------------------------------------------------- /config/queue.php: -------------------------------------------------------------------------------- 1 | env('QUEUE_CONNECTION', 'sync'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Queue Connections 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Here you may configure the connection information for each server that 24 | | is used by your application. A default configuration has been added 25 | | for each back-end shipped with Laravel. You are free to add more. 26 | | 27 | | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null" 28 | | 29 | */ 30 | 31 | 'connections' => [ 32 | 33 | 'sync' => [ 34 | 'driver' => 'sync', 35 | ], 36 | 37 | 'database' => [ 38 | 'driver' => 'database', 39 | 'table' => 'jobs', 40 | 'queue' => 'default', 41 | 'retry_after' => 90, 42 | ], 43 | 44 | 'beanstalkd' => [ 45 | 'driver' => 'beanstalkd', 46 | 'host' => 'localhost', 47 | 'queue' => 'default', 48 | 'retry_after' => 90, 49 | 'block_for' => 0, 50 | ], 51 | 52 | 'sqs' => [ 53 | 'driver' => 'sqs', 54 | 'key' => env('AWS_ACCESS_KEY_ID'), 55 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 56 | 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), 57 | 'queue' => env('SQS_QUEUE', 'your-queue-name'), 58 | 'region' => env('AWS_REGION', 'us-east-1'), 59 | ], 60 | 61 | 'redis' => [ 62 | 'driver' => 'redis', 63 | 'connection' => 'default', 64 | 'queue' => env('REDIS_QUEUE', 'default'), 65 | 'retry_after' => 90, 66 | 'block_for' => null, 67 | ], 68 | 69 | ], 70 | 71 | /* 72 | |-------------------------------------------------------------------------- 73 | | Failed Queue Jobs 74 | |-------------------------------------------------------------------------- 75 | | 76 | | These options configure the behavior of failed queue job logging so you 77 | | can control which database and table are used to store the jobs that 78 | | have failed. You may change them to any database / table you wish. 79 | | 80 | */ 81 | 82 | 'failed' => [ 83 | 'driver' => env('QUEUE_FAILED_DRIVER', 'database'), 84 | 'database' => env('DB_CONNECTION', 'mysql'), 85 | 'table' => 'failed_jobs', 86 | ], 87 | 88 | ]; 89 | -------------------------------------------------------------------------------- /config/services.php: -------------------------------------------------------------------------------- 1 | [ 18 | 'domain' => env('MAILGUN_DOMAIN'), 19 | 'secret' => env('MAILGUN_SECRET'), 20 | 'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'), 21 | ], 22 | 23 | 'postmark' => [ 24 | 'token' => env('POSTMARK_TOKEN'), 25 | ], 26 | 27 | 'ses' => [ 28 | 'key' => env('AWS_ACCESS_KEY_ID'), 29 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 30 | 'region' => env('AWS_REGION', 'us-east-1'), 31 | ], 32 | 33 | 'sparkpost' => [ 34 | 'secret' => env('SPARKPOST_SECRET'), 35 | ], 36 | 37 | 'stripe' => [ 38 | 'model' => App\User::class, 39 | 'key' => env('STRIPE_KEY'), 40 | 'secret' => env('STRIPE_SECRET'), 41 | 'webhook' => [ 42 | 'secret' => env('STRIPE_WEBHOOK_SECRET'), 43 | 'tolerance' => env('STRIPE_WEBHOOK_TOLERANCE', 300), 44 | ], 45 | ], 46 | 47 | 'slack' => [ 48 | 'token' => env('SLACK_TOKEN'), 49 | ], 50 | 51 | ]; 52 | -------------------------------------------------------------------------------- /config/session.php: -------------------------------------------------------------------------------- 1 | env('SESSION_DRIVER', 'file'), 22 | 23 | /* 24 | |-------------------------------------------------------------------------- 25 | | Session Lifetime 26 | |-------------------------------------------------------------------------- 27 | | 28 | | Here you may specify the number of minutes that you wish the session 29 | | to be allowed to remain idle before it expires. If you want them 30 | | to immediately expire on the browser closing, set that option. 31 | | 32 | */ 33 | 34 | 'lifetime' => env('SESSION_LIFETIME', 120), 35 | 36 | 'expire_on_close' => false, 37 | 38 | /* 39 | |-------------------------------------------------------------------------- 40 | | Session Encryption 41 | |-------------------------------------------------------------------------- 42 | | 43 | | This option allows you to easily specify that all of your session data 44 | | should be encrypted before it is stored. All encryption will be run 45 | | automatically by Laravel and you can use the Session like normal. 46 | | 47 | */ 48 | 49 | 'encrypt' => false, 50 | 51 | /* 52 | |-------------------------------------------------------------------------- 53 | | Session File Location 54 | |-------------------------------------------------------------------------- 55 | | 56 | | When using the native session driver, we need a location where session 57 | | files may be stored. A default has been set for you but a different 58 | | location may be specified. This is only needed for file sessions. 59 | | 60 | */ 61 | 62 | 'files' => storage_path('framework/sessions'), 63 | 64 | /* 65 | |-------------------------------------------------------------------------- 66 | | Session Database Connection 67 | |-------------------------------------------------------------------------- 68 | | 69 | | When using the "database" or "redis" session drivers, you may specify a 70 | | connection that should be used to manage these sessions. This should 71 | | correspond to a connection in your database configuration options. 72 | | 73 | */ 74 | 75 | 'connection' => env('SESSION_CONNECTION', null), 76 | 77 | /* 78 | |-------------------------------------------------------------------------- 79 | | Session Database Table 80 | |-------------------------------------------------------------------------- 81 | | 82 | | When using the "database" session driver, you may specify the table we 83 | | should use to manage the sessions. Of course, a sensible default is 84 | | provided for you; however, you are free to change this as needed. 85 | | 86 | */ 87 | 88 | 'table' => 'sessions', 89 | 90 | /* 91 | |-------------------------------------------------------------------------- 92 | | Session Cache Store 93 | |-------------------------------------------------------------------------- 94 | | 95 | | When using the "apc", "memcached", or "dynamodb" session drivers you may 96 | | list a cache store that should be used for these sessions. This value 97 | | must match with one of the application's configured cache "stores". 98 | | 99 | */ 100 | 101 | 'store' => env('SESSION_STORE', null), 102 | 103 | /* 104 | |-------------------------------------------------------------------------- 105 | | Session Sweeping Lottery 106 | |-------------------------------------------------------------------------- 107 | | 108 | | Some session drivers must manually sweep their storage location to get 109 | | rid of old sessions from storage. Here are the chances that it will 110 | | happen on a given request. By default, the odds are 2 out of 100. 111 | | 112 | */ 113 | 114 | 'lottery' => [2, 100], 115 | 116 | /* 117 | |-------------------------------------------------------------------------- 118 | | Session Cookie Name 119 | |-------------------------------------------------------------------------- 120 | | 121 | | Here you may change the name of the cookie used to identify a session 122 | | instance by ID. The name specified here will get used every time a 123 | | new session cookie is created by the framework for every driver. 124 | | 125 | */ 126 | 127 | 'cookie' => env( 128 | 'SESSION_COOKIE', 129 | Str::slug(env('APP_NAME', 'laravel'), '_').'_session' 130 | ), 131 | 132 | /* 133 | |-------------------------------------------------------------------------- 134 | | Session Cookie Path 135 | |-------------------------------------------------------------------------- 136 | | 137 | | The session cookie path determines the path for which the cookie will 138 | | be regarded as available. Typically, this will be the root path of 139 | | your application but you are free to change this when necessary. 140 | | 141 | */ 142 | 143 | 'path' => '/', 144 | 145 | /* 146 | |-------------------------------------------------------------------------- 147 | | Session Cookie Domain 148 | |-------------------------------------------------------------------------- 149 | | 150 | | Here you may change the domain of the cookie used to identify a session 151 | | in your application. This will determine which domains the cookie is 152 | | available to in your application. A sensible default has been set. 153 | | 154 | */ 155 | 156 | 'domain' => env('SESSION_DOMAIN', null), 157 | 158 | /* 159 | |-------------------------------------------------------------------------- 160 | | HTTPS Only Cookies 161 | |-------------------------------------------------------------------------- 162 | | 163 | | By setting this option to true, session cookies will only be sent back 164 | | to the server if the browser has a HTTPS connection. This will keep 165 | | the cookie from being sent to you if it can not be done securely. 166 | | 167 | */ 168 | 169 | 'secure' => env('SESSION_SECURE_COOKIE', null), 170 | 171 | 'same_site' => 'lax', 172 | 173 | /* 174 | |-------------------------------------------------------------------------- 175 | | HTTP Access Only 176 | |-------------------------------------------------------------------------- 177 | | 178 | | Setting this value to true will prevent JavaScript from accessing the 179 | | value of the cookie and the cookie will only be accessible through 180 | | the HTTP protocol. You are free to modify this option if needed. 181 | | 182 | */ 183 | 184 | 'http_only' => true, 185 | 186 | /* 187 | |-------------------------------------------------------------------------- 188 | | Same-Site Cookies 189 | |-------------------------------------------------------------------------- 190 | | 191 | | This option determines how your cookies behave when cross-site requests 192 | | take place, and can be used to mitigate CSRF attacks. By default, we 193 | | do not enable this as other CSRF protection services are in place. 194 | | 195 | | Supported: "lax", "strict" 196 | | 197 | */ 198 | 199 | 'same_site' => null, 200 | 201 | ]; 202 | -------------------------------------------------------------------------------- /config/view.php: -------------------------------------------------------------------------------- 1 | [ 17 | resource_path('views'), 18 | ], 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Compiled View Path 23 | |-------------------------------------------------------------------------- 24 | | 25 | | This option determines where all the compiled Blade templates will be 26 | | stored for your application. Typically, this is within the storage 27 | | directory. However, as usual, you are free to change this value. 28 | | 29 | */ 30 | 31 | 'compiled' => env( 32 | 'VIEW_COMPILED_PATH', 33 | realpath(storage_path('framework/views')) 34 | ), 35 | 36 | ]; 37 | -------------------------------------------------------------------------------- /database/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite 2 | -------------------------------------------------------------------------------- /database/factories/DateFactory.php: -------------------------------------------------------------------------------- 1 | define(Date::class, function (Faker $faker) { 8 | return [ 9 | 'project' => $faker->randomElement(config('app.projects')->names()), 10 | 'date' => $faker->dateTimeThisYear(now()->addYear(1))->format('Y-m-d'), 11 | 'description' => $faker->text(100), 12 | ]; 13 | }); 14 | -------------------------------------------------------------------------------- /database/factories/ProjectFactory.php: -------------------------------------------------------------------------------- 1 | define(Project::class, function (Faker $faker) { 9 | return [ 10 | 'name' => 'The name', 11 | 'channel' => 'slack', 12 | 'members' => [], 13 | 'logoUrl' => 'img.jpg', 14 | 'endsOn' => Carbon::tomorrow(), 15 | ]; 16 | }); 17 | 18 | $factory->state(Project::class, 'inactive', [ 19 | 'endsOn' => Carbon::yesterday(), 20 | ]); 21 | -------------------------------------------------------------------------------- /database/factories/ReportFactory.php: -------------------------------------------------------------------------------- 1 | define(Report::class, function (Faker $faker) { 8 | return [ 9 | 'project' => $faker->randomElement(config('app.projects')->names()), 10 | 'week_number' => now()->subWeek(1)->format('Y-W'), 11 | 'spirit' => $faker->randomElement(['☹️', '😐', '🙂', '😀']), 12 | 'priorities' => $faker->text(300), 13 | 'victories' => $faker->text(300), 14 | 'help' => $faker->randomElement([$faker->text(300), null]), 15 | ]; 16 | }); 17 | -------------------------------------------------------------------------------- /database/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | define(User::class, function (Faker $faker) { 20 | return [ 21 | 'name' => $faker->name, 22 | 'email' => $faker->unique()->safeEmail, 23 | 'email_verified_at' => now(), 24 | 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password 25 | 'remember_token' => Str::random(10), 26 | ]; 27 | }); 28 | -------------------------------------------------------------------------------- /database/migrations/2019_02_28_102730_create_reports_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->string('project', 100); 19 | $table->string('week_number', 7); 20 | $table->string('spirit', 10); 21 | $table->string('priorities', 500); 22 | $table->string('victories', 500); 23 | $table->string('help', 500)->nullable(); 24 | $table->timestamps(); 25 | 26 | $table->unique(['project', 'week_number']); 27 | }); 28 | } 29 | 30 | /** 31 | * Reverse the migrations. 32 | * 33 | * @return void 34 | */ 35 | public function down() 36 | { 37 | Schema::dropIfExists('reports'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /database/migrations/2019_05_22_193204_create_dates_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->string('project', 100); 19 | $table->date('date'); 20 | $table->string('description', 200); 21 | $table->timestamps(); 22 | 23 | $table->unique(['project', 'date']); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::dropIfExists('dates'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /database/migrations/2019_08_19_000000_create_failed_jobs_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->text('connection'); 19 | $table->text('queue'); 20 | $table->longText('payload'); 21 | $table->longText('exception'); 22 | $table->timestamp('failed_at')->useCurrent(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | * 29 | * @return void 30 | */ 31 | public function down() 32 | { 33 | Schema::dropIfExists('failed_jobs'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /database/seeds/DatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | names() as $name) { 16 | foreach ($this->weeks() as $week) { 17 | if ($faker->numberBetween(1, 100) <= 80) { 18 | factory(Report::class)->create([ 19 | 'project' => $name, 20 | 'week_number' => $week, 21 | ]); 22 | } 23 | } 24 | 25 | for ($i = 0; $i < $faker->numberBetween(3, 10); $i++) { 26 | factory(Date::class)->create([ 27 | 'project' => $name, 28 | 'date' => $this->randomDate($faker), 29 | ]); 30 | } 31 | } 32 | } 33 | 34 | private function weeks() 35 | { 36 | $thisFriday = new Carbon('friday'); 37 | $weeks = [$thisFriday->format('Y-W')]; 38 | 39 | foreach (range(1, 30) as $weekSub) { 40 | $weeks[] = $thisFriday->copy()->subWeek($weekSub)->format('Y-W'); 41 | } 42 | 43 | return $weeks; 44 | } 45 | 46 | private function randomDate($faker) 47 | { 48 | return now()->addDays($faker->unique()->numberBetween(-200, 200))->toDateString(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.0" 2 | services: 3 | web: 4 | image: nginx:latest 5 | restart: always 6 | ports: 7 | - "8080:80" 8 | volumes: 9 | - .:/var/www 10 | - ./docker/nginx.conf:/etc/nginx/conf.d/default.conf 11 | - ./docker/nginx-log.conf:/usr/local/etc/php-fpm.d/zz-log.conf 12 | links: 13 | - php 14 | php: 15 | image: php:7-fpm 16 | restart: always 17 | build: 18 | context: . 19 | dockerfile: Dockerfile 20 | environment: 21 | - DB_CONNECTION="sqlite" 22 | - REPORT_SECRET="password" 23 | - APP_LOCALE="fr" 24 | - APP_ENV="production" 25 | # volumes: 26 | # - .:/var/www # dev only 27 | -------------------------------------------------------------------------------- /docker/nginx-log.conf: -------------------------------------------------------------------------------- 1 | php_admin_flag[log_errors] = on 2 | php_flag[display_errors] = off -------------------------------------------------------------------------------- /docker/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | index index.php index.html; 4 | server_name localhost; 5 | error_log /var/log/nginx/error.log; 6 | access_log /var/log/nginx/access.log; 7 | root /var/www/public; 8 | 9 | location ~ \.php$ { 10 | try_files $uri =404; 11 | fastcgi_split_path_info ^(.+\.php)(/.+)$; 12 | fastcgi_pass php:9000; 13 | fastcgi_index index.php; 14 | include fastcgi_params; 15 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 16 | fastcgi_param PATH_INFO $fastcgi_path_info; 17 | } 18 | location / { 19 | try_files $uri $uri/ /index.php?$query_string; 20 | gzip_static on; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "npm run development", 5 | "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", 6 | "watch": "npm run development -- --watch", 7 | "watch-poll": "npm run watch -- --watch-poll", 8 | "hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js", 9 | "prod": "npm run production", 10 | "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js" 11 | }, 12 | "devDependencies": { 13 | "cross-env": "^5.1", 14 | "laravel-mix": "^4.0.7", 15 | "vue-template-compiler": "^2.6.8" 16 | }, 17 | "dependencies": { 18 | "simplemde": "^1.11.2" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | ./tests/Unit 14 | 15 | 16 | 17 | ./tests/Feature 18 | 19 | 20 | 21 | 22 | ./app 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | 3 | Options -MultiViews -Indexes 4 | 5 | 6 | RewriteEngine On 7 | 8 | # Handle Authorization Header 9 | RewriteCond %{HTTP:Authorization} . 10 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 11 | 12 | # Redirect Trailing Slashes If Not A Folder... 13 | RewriteCond %{REQUEST_FILENAME} !-d 14 | RewriteCond %{REQUEST_URI} (.+)/$ 15 | RewriteRule ^ %1 [L,R=301] 16 | 17 | # Handle Front Controller... 18 | RewriteCond %{REQUEST_FILENAME} !-d 19 | RewriteCond %{REQUEST_FILENAME} !-f 20 | RewriteRule ^ index.php [L] 21 | 22 | -------------------------------------------------------------------------------- /public/css/all.css: -------------------------------------------------------------------------------- 1 | body{display:flex;flex-direction:column}main{flex:1}.navbar{border-bottom:0}.hero{background:url(/images/circuit-board.svg) repeat,linear-gradient(155deg,#523993,var(--theme-primary));background-size:150px 150px,100%}.hero__container{min-height:25vh}.slope_from_white:before{content:" ";display:block;width:100%;height:4vw}.slope_from_white:before{background:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSIgd2lkdGg9IjEwMCIgaGVpZ2h0PSIxMCIgdmlld0JveD0iMCAwIDEwMCAxMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICAgIDxwb2x5Z29uIGZpbGw9IiNmZmYiIHBvaW50cz0iMCwwIDEwMCwwIDAsMTAiLz4KPC9zdmc+) no-repeat top;background-size:100% 100%}.panel+*{margin-top:1.5em}.panel__header h1{display:inline-block;margin:0}.project_logo{vertical-align:middle;width:32px}a.header-anchor{font-size:.9em;float:left;margin-left:-.87em;padding-right:.23em;margin-top:-.1em;opacity:0;text-decoration:none}h1:hover .header-anchor,h2:hover .header-anchor,h3:hover .header-anchor,h4:hover .header-anchor,h5:hover .header-anchor,h6:hover .header-anchor{opacity:1}.date_grid{margin-bottom:3em}.date_card{max-width:30em}.CodeMirror,.CodeMirror-scroll{min-height:70px}@media (max-width:749px){.dashboard .main{padding:2em 0}} 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/entrepreneur-interet-general/bulletins/4f8429ddba064b7c4c7d8c095a91e67502e8aa87/public/favicon.ico -------------------------------------------------------------------------------- /public/images/circuit-board.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /public/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/entrepreneur-interet-general/bulletins/4f8429ddba064b7c4c7d8c095a91e67502e8aa87/public/images/favicon.png -------------------------------------------------------------------------------- /public/images/logos/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/entrepreneur-interet-general/bulletins/4f8429ddba064b7c4c7d8c095a91e67502e8aa87/public/images/logos/.gitkeep -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | define('LARAVEL_START', microtime(true)); 9 | 10 | /* 11 | |-------------------------------------------------------------------------- 12 | | Register The Auto Loader 13 | |-------------------------------------------------------------------------- 14 | | 15 | | Composer provides a convenient, automatically generated class loader for 16 | | our application. We just need to utilize it! We'll simply require it 17 | | into the script here so that we don't have to worry about manual 18 | | loading any of our classes later on. It feels great to relax. 19 | | 20 | */ 21 | 22 | require __DIR__.'/../vendor/autoload.php'; 23 | 24 | /* 25 | |-------------------------------------------------------------------------- 26 | | Turn On The Lights 27 | |-------------------------------------------------------------------------- 28 | | 29 | | We need to illuminate PHP development, so let us turn on the lights. 30 | | This bootstraps the framework and gets it ready for use, then it 31 | | will load up this application so that we can run it and send 32 | | the responses back to the browser and delight our users. 33 | | 34 | */ 35 | 36 | $app = require_once __DIR__.'/../bootstrap/app.php'; 37 | 38 | /* 39 | |-------------------------------------------------------------------------- 40 | | Run The Application 41 | |-------------------------------------------------------------------------- 42 | | 43 | | Once we have the application, we can handle the incoming request 44 | | through the kernel, and send the associated response back to 45 | | the client's browser allowing them to enjoy the creative 46 | | and wonderful application we have prepared for them. 47 | | 48 | */ 49 | 50 | $kernel = $app->make(Illuminate\Contracts\Http\Kernel::class); 51 | 52 | $response = $kernel->handle( 53 | $request = Illuminate\Http\Request::capture() 54 | ); 55 | 56 | $response->send(); 57 | 58 | $kernel->terminate($request, $response); 59 | -------------------------------------------------------------------------------- /public/mix-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "/js/app.js": "/js/app.js?id=bac45c2b944cb6c32883", 3 | "/css/all.css": "/css/all.css?id=ac3946565a3241ea6e3a" 4 | } 5 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / 3 | -------------------------------------------------------------------------------- /public/web.config: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /resources/css/all.css: -------------------------------------------------------------------------------- 1 | body { 2 | display: flex; 3 | flex-direction: column; 4 | } 5 | 6 | main { 7 | flex: 1; 8 | } 9 | 10 | .navbar { 11 | border-bottom: 0; 12 | } 13 | 14 | .hero { 15 | background: url(/images/circuit-board.svg) repeat, linear-gradient(155deg, #523993, var(--theme-primary)); 16 | background-size: 150px 150px, 100%; 17 | } 18 | 19 | .hero__container { 20 | min-height: 25vh; 21 | } 22 | 23 | .slope_from_white:before{ 24 | content: " "; 25 | display: block; 26 | width: 100%; 27 | height: 4vw; 28 | } 29 | 30 | .slope_from_white:before{ 31 | background: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSIgd2lkdGg9IjEwMCIgaGVpZ2h0PSIxMCIgdmlld0JveD0iMCAwIDEwMCAxMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICAgIDxwb2x5Z29uIGZpbGw9IiNmZmYiIHBvaW50cz0iMCwwIDEwMCwwIDAsMTAiLz4KPC9zdmc+") no-repeat top; 32 | background-size: 100% 100%; 33 | } 34 | 35 | .panel + * { 36 | margin-top: 1.5em; 37 | } 38 | 39 | .panel__header h1 { 40 | display: inline-block; 41 | margin: 0; 42 | } 43 | 44 | .project_logo { 45 | vertical-align: middle; 46 | width: 32px; 47 | } 48 | 49 | a.header-anchor { 50 | font-size: .9em; 51 | float: left; 52 | margin-left: -.87em; 53 | padding-right: .23em; 54 | margin-top: -.1em; 55 | opacity: 0; 56 | text-decoration: none; 57 | } 58 | 59 | h1:hover .header-anchor, 60 | h2:hover .header-anchor, 61 | h3:hover .header-anchor, 62 | h4:hover .header-anchor, 63 | h5:hover .header-anchor, 64 | h6:hover .header-anchor { 65 | opacity: 1; 66 | } 67 | 68 | .date_grid { 69 | margin-bottom: 3em; 70 | } 71 | 72 | .date_card { 73 | max-width: 30em; 74 | } 75 | 76 | .CodeMirror, .CodeMirror-scroll { 77 | min-height: 70px; 78 | } 79 | 80 | @media (max-width:749px) { 81 | .dashboard .main { 82 | padding: 2em 0; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /resources/js/TextareaAutoResize.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Auto Resizing for Textarea Elements. 3 | * 4 | * This automatically resizes textarea elemens when the content gets 5 | * longer than the initial height of the element to ease writing for users. 6 | */ 7 | export default class TextareaAutoResize { 8 | constructor(element) { 9 | this.element = element 10 | this.element.addEventListener('input', this.textareaDidChange.bind(this)) 11 | 12 | this.textareaDidChange() 13 | } 14 | 15 | /** 16 | * Applies the scrollHeight to the element if content exceeds, otherwise 'auto'. 17 | */ 18 | textareaDidChange() { 19 | this.element.style.height = 'auto' 20 | this.element.style.height = this.element.scrollHeight + 'px' 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /resources/js/TextareaMarkdown.js: -------------------------------------------------------------------------------- 1 | import * as SimpleMDE from "SimpleMDE" 2 | 3 | export default class TextareaMarkdown { 4 | constructor(element) { 5 | new SimpleMDE({ 6 | element: element, 7 | status: false, 8 | spellChecker: false, 9 | toolbar: ["bold", "italic", "link", "|", "quote", "unordered-list", "|", "preview"] 10 | }) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /resources/js/app.js: -------------------------------------------------------------------------------- 1 | import TextareaAutoResize from "./TextareaAutoResize.js" 2 | import TextareaMarkdown from "./TextareaMarkdown.js" 3 | 4 | document.addEventListener('DOMContentLoaded', function() { 5 | const textareaElements = document.querySelectorAll('textarea') 6 | for (const textareaElement of textareaElements) { 7 | new TextareaAutoResize(textareaElement) 8 | } 9 | 10 | const simplemdeElements = document.querySelectorAll(".js-simplemde") 11 | for (const textareaElement of simplemdeElements) { 12 | new TextareaMarkdown(textareaElement) 13 | } 14 | }); 15 | 16 | function copy() { 17 | var copyText = document.querySelector("#js-copy-target") 18 | copyText.select() 19 | document.execCommand("copy") 20 | document.querySelector("#js-copy-btn").classList.remove('secondary') 21 | } 22 | 23 | if (document.getElementById('js-copy-btn')) { 24 | document.getElementById('js-copy-btn').addEventListener("click", copy) 25 | } 26 | -------------------------------------------------------------------------------- /resources/js/bootstrap.js: -------------------------------------------------------------------------------- 1 | 2 | window._ = require('lodash'); 3 | 4 | /** 5 | * We'll load jQuery and the Bootstrap jQuery plugin which provides support 6 | * for JavaScript based Bootstrap features such as modals and tabs. This 7 | * code may be modified to fit the specific needs of your application. 8 | */ 9 | 10 | try { 11 | window.Popper = require('popper.js').default; 12 | window.$ = window.jQuery = require('jquery'); 13 | 14 | require('bootstrap'); 15 | } catch (e) {} 16 | 17 | /** 18 | * We'll load the axios HTTP library which allows us to easily issue requests 19 | * to our Laravel back-end. This library automatically handles sending the 20 | * CSRF token as a header based on the value of the "XSRF" token cookie. 21 | */ 22 | 23 | window.axios = require('axios'); 24 | 25 | window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; 26 | 27 | /** 28 | * Next we will register the CSRF Token as a common header with Axios so that 29 | * all outgoing HTTP requests automatically have it attached. This is just 30 | * a simple convenience so we don't have to attach every token manually. 31 | */ 32 | 33 | let token = document.head.querySelector('meta[name="csrf-token"]'); 34 | 35 | if (token) { 36 | window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content; 37 | } else { 38 | console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token'); 39 | } 40 | 41 | /** 42 | * Echo exposes an expressive API for subscribing to channels and listening 43 | * for events that are broadcast by Laravel. Echo and event broadcasting 44 | * allows your team to easily build robust real-time web applications. 45 | */ 46 | 47 | // import Echo from 'laravel-echo' 48 | 49 | // window.Pusher = require('pusher-js'); 50 | 51 | // window.Echo = new Echo({ 52 | // broadcaster: 'pusher', 53 | // key: process.env.MIX_PUSHER_APP_KEY, 54 | // cluster: process.env.MIX_PUSHER_APP_CLUSTER, 55 | // encrypted: true 56 | // }); 57 | -------------------------------------------------------------------------------- /resources/js/components/ExampleComponent.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 24 | -------------------------------------------------------------------------------- /resources/lang/en/about.php: -------------------------------------------------------------------------------- 1 | 'About this tool', 5 | 'subtitle' => "

This tool is an asynchronous alternative to stand-up meetings—also called status meetings, or updates.

", 6 | 'feature_title' => 'What this tool does', 7 | 'feature_description' => " 8 |

9 | During stand-up meetings, some coworkers come together at the same time, in the same place, and they speak one at a time about what's going on. In other words, stand-up meetings require several people to stop what they're doing so that everybody can hear the exact same thing at the exact same time. The problem is: some things can wait. The kind of things shared during status meetings can usually wait a few hours for people to be ready to absorb it. 10 |

11 |

12 | So instead of pulling people away from from their work at the exact same time, this tool allows each team to write up a weekly statut report on their own schedule. Status repots are posted to a form so that each team can write it up whenever they're ready. When every team has posted its status report, the reports are aggregated into a newsletter that is sent to all the teams. Just like a status meeting would do, but without breaking people's productivity. 13 |

14 | ", 15 | 'works_title' => 'How this tool works', 16 | 'works_description' => ' 17 |
    18 |
  1. Each teams fills out its weekly status report using a dedicated form;
  2. 19 |
  3. Status reports are aggregated and turned into a newsletter sent every Friday at 3PM.
  4. 20 |
21 | ', 22 | 'sharing_title' => 'How reports are shared', 23 | 'sharing_description' => " 24 |

25 | Although this tool's code is open source, status reports posted to it are not public. That being said, status reports are written up to be shared with some people. Therefore, this tool acts as a permanent home for your team's status report, which allow you to: 26 |

31 |

32 | ", 33 | 'deadline_title' => 'Weekly deadlines', 34 | 'deadline_description' => ' 35 |

36 | Keep in mind the following deadlines: 37 |

42 |

43 | ', 44 | ]; 45 | -------------------------------------------------------------------------------- /resources/lang/en/auth.php: -------------------------------------------------------------------------------- 1 | 'These credentials do not match our records.', 17 | 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /resources/lang/en/dates.php: -------------------------------------------------------------------------------- 1 | 'Key dates', 6 | ]; 7 | -------------------------------------------------------------------------------- /resources/lang/en/emails.php: -------------------------------------------------------------------------------- 1 | 'This week in the team.', 5 | 'help_requests' => 'Help requests', 6 | 'upcoming_key_dates' => 'Key dates next week', 7 | 'key_date' => ':date: :project – :description', 8 | 'news' => 'News of the week', 9 | 'mood' => 'Team mood:', 10 | 'priority' => 'Priorities:', 11 | 'ups_and_downs' => 'Ups and downs:', 12 | 'help' => 'Help requests:', 13 | 'no_news' => "Unfortunately, we haven't heard of the following projects: :projects 😢.", 14 | 'all_filled' => 'Everybody filled their bulletins. 💪 Thanks! 🙏', 15 | 'outro' => 'Have a nice weekend! 🏝', 16 | 'subject' => 'Bulettins of the week :week', 17 | 'footer' => "To save trees, eat beavers and don't print this email.", 18 | ]; 19 | -------------------------------------------------------------------------------- /resources/lang/en/form.php: -------------------------------------------------------------------------------- 1 | 'Bulletins of the week :week', 5 | 'description' => 'Take a look back at this week. 5 minutes, 300 characters per field — a bit more than a tweet.', 6 | 'previous_bulletins.week' => 'Your answers will be shared by email to the team on Friday at 3PM.', 7 | 'project' => 'Your project', 8 | 'select_project' => '-- Select your project --', 9 | 'already_filled' => '(already filled)', 10 | 'team_mood' => 'Your team mood', 11 | 'priority' => 'Your priority', 12 | 'priority.placeholder' => 'The most important thing of your week', 13 | 'ups_and_downs' => 'Your ups and downs', 14 | 'ups_and_downs.placeholder' => 'What went great and what was harder', 15 | 'help' => 'Help requests', 16 | 'help.placeholder' => 'Ask the rest of the team to give you a hand', 17 | 'key_date' => 'Key date coming', 18 | 'key_date.placeholder' => "What's going to happen this day?", 19 | 'optional' => 'Optional', 20 | 'save' => 'Save', 21 | 'cant_fill' => "It's not possible to share a bulletin for this week. See you next week!", 22 | ]; 23 | -------------------------------------------------------------------------------- /resources/lang/en/layout.php: -------------------------------------------------------------------------------- 1 | 'Previous bulletins', 5 | 'previous_bulletins.project' => 'By project', 6 | 'previous_bulletins.week' => 'By week', 7 | 'about' => 'About', 8 | 'source_code' => 'Source code', 9 | 'hero_title' => 'Bulletins', 10 | 'hero_subtitle' => 'How was your week?', 11 | 'language' => 'Language', 12 | 'language_fr' => 'French', 13 | 'language_en' => 'English', 14 | 'documentation' => 'Documentation', 15 | 'dates' => 'Key dates', 16 | ]; 17 | -------------------------------------------------------------------------------- /resources/lang/en/login.php: -------------------------------------------------------------------------------- 1 | 'Access to previous bulletins', 5 | 'description' => 'You should log in first.', 6 | 'password' => 'Password', 7 | 'login' => 'Log in', 8 | ]; 9 | -------------------------------------------------------------------------------- /resources/lang/en/notifications.php: -------------------------------------------------------------------------------- 1 | " :wave: Hello! Good news, it's nearly the end of the week! :tada: It's time to share your news to the rest of team (careful, the email is sent at 3PM :rocket:) :point_right: :url", 5 | 'individual_reminder' => "For the time being we haven't heard of :project :sob: Your mission: share your news to the rest of the team in less than one hour. Careful, the email is sent at 3PM! :rocket: :point_right: :url", 6 | 'individual_reminder_mail' => "For the time being we haven't heard of :project. Your mission: share your news to the rest of the team in less than one hour. Careful, the email is sent at 3PM!", 7 | 'individual_reminder_mail.button' => 'Fill my news', 8 | ]; 9 | -------------------------------------------------------------------------------- /resources/lang/en/pagination.php: -------------------------------------------------------------------------------- 1 | '« Previous', 17 | 'next' => 'Next »', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /resources/lang/en/passwords.php: -------------------------------------------------------------------------------- 1 | 'Passwords must be at least eight characters and match the confirmation.', 17 | 'reset' => 'Your password has been reset!', 18 | 'sent' => 'We have e-mailed your password reset link!', 19 | 'token' => 'This password reset token is invalid.', 20 | 'user' => "We can't find a user with that e-mail address.", 21 | 22 | ]; 23 | -------------------------------------------------------------------------------- /resources/lang/en/reports.php: -------------------------------------------------------------------------------- 1 | 'Ended on :date_str', 6 | 'ends_on' => 'Ends on :date_str', 7 | 'by_month' => 'Bulletins by month', 8 | 'past_dates' => 'Past key dates', 9 | 'upcoming_dates' => 'Upcoming key dates', 10 | 'actions' => 'Actions', 11 | 'share' => 'Share', 12 | 'csv_export' => 'Export in CSV', 13 | 'time_period' => 'From :start to :end', 14 | 'mood' => 'Team mood:', 15 | 'priority' => 'Priorities:', 16 | 'ups_and_downs' => 'Ups and downs:', 17 | 'help' => 'Help requests:', 18 | 'share_title' => 'Share these bulletins', 19 | 'share_description' => 'In order to share only these bulletins of the project :project, you can share this unique link.', 20 | 'copy' => 'Copy', 21 | 'close' => 'Close', 22 | 23 | // reports.week_index 24 | 'previous_reports' => 'Previous bulletins', 25 | ]; 26 | -------------------------------------------------------------------------------- /resources/lang/en/success.php: -------------------------------------------------------------------------------- 1 | 'Your bulletin has been saved!', 5 | 'thanks' => 'Thanks for taking the time to tell us how went your week! Have a nice day!', 6 | ]; 7 | -------------------------------------------------------------------------------- /resources/lang/fr/about.php: -------------------------------------------------------------------------------- 1 | 'Pourquoi cet outil', 5 | 'subtitle' => "

Cet outil est une alternative asynchrone et écrite aux réunions de suivi hebdomadaires, aussi appelées « stand-up meetings »

", 6 | 'feature_title' => 'Principe', 7 | 'feature_description' => ' 8 |

9 | Les stand-up sont des réunions durant lesquels plusieurs personnes se réunissent au même endroit, au même moment, pour dire à tour de rôle ce sur quoi elles travaillent. Ce sont des points d’étapes récurrents, qui servent à faire circuler des informations au sein d’une équipe. Problème : les stand-up demandent à plusieurs personnes d’interrompre leur travail respectif au même moment pour absorber des informations qui ne leur seront sans doute pas utiles au même moment. 10 |

11 | 12 |

13 | Au lieu d’interrompre le travail de tout le monde, ceci permet à chaque équipe de faire un bilan écrit de la semaine écoulée. Les bilans sont saisis dans un formulaire, que chaque équipe remplit quand bon lui semble — du moment qu’elle le remplit à temps. Une fois renseignés, les bilans sont ensuite agrégés dans un e-mail qui est envoyé à tout le monde, et que chacun peut lire quand bon lui semble. Les informations circulent ainsi sans nécessiter de réunion. 14 |

15 | ', 16 | 'works_title' => 'Fonctionnement', 17 | 'works_description' => ' 18 |
    19 |
  1. Chaque équipe entre le bilan de la semaine en complétant le formulaire de suivi, à raison d’un bilan par équipe ;
  2. 20 |
  3. Les bilans saisis sont agrégés dans un e-mail qui est envoyé automatiquement chaque vendredi à 15 heures.
  4. 21 |
22 | ', 23 | 'sharing_title' => 'Visibilité des réponses', 24 | 'sharing_description' => " 25 |

26 | Bien que le code de l’application soit ouvert, les réponses saisies dans le formulaire de l'application ne sont pas publiques. Toutefois, ces informations sont utiles pour chaque équipe : c'est pourquoi il est possible d'accéder à un historique de ses bilans, partager les bilans à l'aide d'un lien unique et d'exporter les données. 27 |

28 | ", 29 | 'deadline_title' => 'Échéances hebdomadaires', 30 | 'deadline_description' => ' 31 |

32 | Les échéances à garder en tête, chaque semaine : 33 |

38 |

39 | ', 40 | ]; 41 | -------------------------------------------------------------------------------- /resources/lang/fr/auth.php: -------------------------------------------------------------------------------- 1 | 'Ces identifiants ne correspondent pas à nos enregistrements', 16 | 'throttle' => 'Tentatives de connexion trop nombreuses. Veuillez essayer de nouveau dans :seconds secondes.', 17 | ]; 18 | -------------------------------------------------------------------------------- /resources/lang/fr/dates.php: -------------------------------------------------------------------------------- 1 | 'Dates clés', 6 | ]; 7 | -------------------------------------------------------------------------------- /resources/lang/fr/emails.php: -------------------------------------------------------------------------------- 1 | "Cette semaine, dans l'équipe.", 5 | 'help_requests' => "Demandes d'aide", 6 | 'upcoming_key_dates' => 'Dates importantes la semaine prochaine', 7 | 'key_date' => ':date : :project – :description', 8 | 'news' => 'Nouvelles de la semaine', 9 | 'mood' => "État d'esprit :", 10 | 'priority' => 'Priorité :', 11 | 'ups_and_downs' => 'Victoire / Difficulté :', 12 | 'help' => 'Besoin :', 13 | 'no_news' => "Malheureusement, nous n'avons pas de nouvelles pour ces projets : :projects 😢.", 14 | 'all_filled' => 'Tout le monde a rempli son bilan ! 💪 Merci 🙏', 15 | 'outro' => 'Passez un bon week-end ! 🏝', 16 | 'subject' => 'Bilan de la semaine :week', 17 | 'footer' => 'Pour sauver des arbres, mangez des castors et n’imprimez pas cet e-mail.', 18 | ]; 19 | -------------------------------------------------------------------------------- /resources/lang/fr/form.php: -------------------------------------------------------------------------------- 1 | 'Bilan de la semaine :week', 5 | 'description' => "Faites le point sur la semaine qui vient de s'écouler. 5 minutes, 300 caractères par champ — un peu plus d'un tweet.", 6 | 'previous_bulletins.week' => "Vos réponses seront partagées par e-mail à toute l'équipe le vendredi à 15h.", 7 | 'project' => 'Votre projet', 8 | 'select_project' => '-- Choisissez votre projet --', 9 | 'already_filled' => '(déjà renseigné)', 10 | 'team_mood' => "Votre état d'esprit", 11 | 'priority' => 'Votre priorité', 12 | 'priority.placeholder' => 'Le sujet le plus important de votre semaine', 13 | 'ups_and_downs' => 'Vos hauts et vos bas', 14 | 'ups_and_downs.placeholder' => 'Ce qui a fonctionné et ce qui vous a donné du fil à retordre', 15 | 'help' => "Demande d'aide", 16 | 'help.placeholder' => 'Faites appel à la communauté pour surmonter un blocage', 17 | 'key_date' => 'Date clé à venir', 18 | 'key_date.placeholder' => 'Quel événement va-t-il se dérouler à cette date ?', 19 | 'optional' => 'Optionnel', 20 | 'save' => 'Enregistrer', 21 | 'cant_fill' => "Il n'est plus possible de partager ses informations pour cette semaine. À la semaine prochaine !", 22 | ]; 23 | -------------------------------------------------------------------------------- /resources/lang/fr/layout.php: -------------------------------------------------------------------------------- 1 | 'Historique des bilans', 5 | 'previous_bulletins.project' => 'Par projet', 6 | 'previous_bulletins.week' => 'Par semaine', 7 | 'about' => 'À propos', 8 | 'source_code' => 'Code source', 9 | 'hero_title' => 'Stand-up hebdomadaire', 10 | 'hero_subtitle' => 'Comment s’est passée votre semaine ?', 11 | 'language' => 'Langage', 12 | 'language_fr' => 'Français', 13 | 'language_en' => 'Anglais', 14 | 'documentation' => 'Documentation', 15 | 'dates' => 'Dates clés', 16 | ]; 17 | -------------------------------------------------------------------------------- /resources/lang/fr/login.php: -------------------------------------------------------------------------------- 1 | "Accès à l'historique des bilans", 5 | 'description' => 'Vous devez vous authentifier préalablement.', 6 | 'password' => 'Mot de passe', 7 | 'login' => "S'authentifier", 8 | ]; 9 | -------------------------------------------------------------------------------- /resources/lang/fr/notifications.php: -------------------------------------------------------------------------------- 1 | " :wave: Hello ! Bonne nouvelle, c'est bientôt la fin de la semaine ! :tada: C'est le moment de partager nos réussites et nos besoins à l'équipe (attention, l'e-mail part à 15h :rocket:) :point_right: :url", 5 | 'individual_reminder' => "Pour le moment nous n'avons pas de nouvelles du projet :project :sob: Votre mission : partager les nouvelles de la semaine à l'équipe en moins d'une heure. Attention, décollage à 15h ! :rocket: :point_right: :url", 6 | 'individual_reminder_mail' => "Pour le moment nous n'avons pas de nouvelles du projet :project. Votre mission : partager les nouvelles de la semaine aux autres en moins d'une heure. Attention, décollage à 15h !", 7 | 'individual_reminder_mail.button' => 'Remplir ma rétrospective', 8 | ]; 9 | -------------------------------------------------------------------------------- /resources/lang/fr/pagination.php: -------------------------------------------------------------------------------- 1 | '« Précédent', 16 | 'next' => 'Suivant »', 17 | ]; 18 | -------------------------------------------------------------------------------- /resources/lang/fr/passwords.php: -------------------------------------------------------------------------------- 1 | 'Les mots de passe doivent contenir au moins six caractères et être identiques.', 16 | 'reset' => 'Votre mot de passe a été réinitialisé !', 17 | 'sent' => 'Nous vous avons envoyé par email le lien de réinitialisation du mot de passe !', 18 | 'token' => "Ce jeton de réinitialisation du mot de passe n'est pas valide.", 19 | 'user' => "Aucun utilisateur n'a été trouvé avec cette adresse email.", 20 | ]; 21 | -------------------------------------------------------------------------------- /resources/lang/fr/reports.php: -------------------------------------------------------------------------------- 1 | 'Terminé depuis le :date_str', 6 | 'ends_on' => 'Fin prévue le :date_str', 7 | 'by_month' => 'Bilans par mois', 8 | 'past_dates' => 'Dates importantes passées', 9 | 'upcoming_dates' => 'Dates importantes futures', 10 | 'actions' => 'Actions', 11 | 'share' => 'Partager', 12 | 'csv_export' => 'Exporter en CSV', 13 | 'time_period' => 'Du :start au :end', 14 | 'mood' => "État d'esprit :", 15 | 'priority' => 'Priorité :', 16 | 'ups_and_downs' => 'Hauts et bas :', 17 | 'help' => "Demande d'aide :", 18 | 'share_title' => "Partager l'historique des bilans", 19 | 'share_description' => "Pour partager uniquement l'historique des bilans du projet :project, vous pouvez partager ce lien unique.", 20 | 'copy' => 'Copier', 21 | 'close' => 'Fermer', 22 | 23 | // reports.week_index 24 | 'previous_reports' => 'Bilans précédents', 25 | ]; 26 | -------------------------------------------------------------------------------- /resources/lang/fr/success.php: -------------------------------------------------------------------------------- 1 | 'Tout est bien enregistré !', 5 | 'thanks' => "Merci d'avoir pris quelques minutes pour nous dire ce qu'il s'est passé cette semaine ! Passe une bonne journée.", 6 | ]; 7 | -------------------------------------------------------------------------------- /resources/sass/_variables.scss: -------------------------------------------------------------------------------- 1 | 2 | // Body 3 | $body-bg: #f8fafc; 4 | 5 | // Typography 6 | $font-family-sans-serif: "Nunito", sans-serif; 7 | $font-size-base: 0.9rem; 8 | $line-height-base: 1.6; 9 | 10 | // Colors 11 | $blue: #3490dc; 12 | $indigo: #6574cd; 13 | $purple: #9561e2; 14 | $pink: #f66D9b; 15 | $red: #e3342f; 16 | $orange: #f6993f; 17 | $yellow: #ffed4a; 18 | $green: #38c172; 19 | $teal: #4dc0b5; 20 | $cyan: #6cb2eb; 21 | -------------------------------------------------------------------------------- /resources/sass/app.scss: -------------------------------------------------------------------------------- 1 | 2 | // Fonts 3 | @import url('https://fonts.googleapis.com/css?family=Nunito'); 4 | 5 | // Variables 6 | @import 'variables'; 7 | 8 | // Bootstrap 9 | @import '~bootstrap/scss/bootstrap'; 10 | 11 | .navbar-laravel { 12 | background-color: #fff; 13 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.04); 14 | } 15 | -------------------------------------------------------------------------------- /resources/views/about.blade.php: -------------------------------------------------------------------------------- 1 | @extends('master') 2 | 3 | @section('content') 4 |
5 |

{{ trans('about.title') }}

6 | @lang('about.subtitle') 7 | 8 | @foreach (['feature', 'works', 'sharing', 'deadline'] as $section) 9 |

@lang("about.{$section}_title")

10 | @lang("about.{$section}_description") 11 | @endforeach 12 |
13 | 14 | @endsection 15 | -------------------------------------------------------------------------------- /resources/views/dates/index.blade.php: -------------------------------------------------------------------------------- 1 | @extends('master') 2 | 3 | @section('section-class') 4 | section-grey 5 | @endsection 6 | 7 | @section('content') 8 |
9 |
10 |
11 |
12 |

{{ trans('dates.title') }}

13 |
14 |
15 | @foreach($data as $month => $dates) 16 |

{{ $month }}

17 | 18 |
19 | @foreach($dates as $date) 20 |
21 |
22 |

{{ $date->project }} {{ $date->project }}

23 |
{{ $date->date->isoFormat('LL') }}
24 |

{{ $date->description }}

25 |
26 |
27 | @endforeach 28 |
29 | 30 | @endforeach 31 |
32 |
33 |
34 |
35 | @endsection 36 | -------------------------------------------------------------------------------- /resources/views/emails/fill_reminder.blade.php: -------------------------------------------------------------------------------- 1 | @component('mail::message') 2 | 3 | {{ trans('notifications.individual_reminder_mail', ['project' => $project]) }} 4 | 5 | @component('mail::button', ['url' => $url]) 6 | {{ trans('notifications.individual_reminder_mail.button') }} 7 | @endcomponent 8 | 9 | @endcomponent 10 | -------------------------------------------------------------------------------- /resources/views/emails/report.blade.php: -------------------------------------------------------------------------------- 1 | @component('mail::message') 2 | 3 | # {{ trans('form.retrospective_title', ['week' => $weekNumber]) }} 4 | {{ trans('emails.intro') }} 5 | 6 | @if($helpRequests->count() > 0) 7 | ## {{ trans('emails.help_requests') }} 8 | @component('mail::panel') 9 | @foreach($helpRequests as $project => $help) 10 | - **{{ $project }} :** {{ $help }} 11 | @endforeach 12 | @endcomponent 13 | @endif 14 | 15 | @if(! $upcomingDates->isEmpty()) 16 | ## {{ trans('emails.upcoming_key_dates') }} 17 | @component('mail::panel') 18 | @foreach($upcomingDates as $upcomingDate) 19 | - {{ trans('emails.key_date', ['date' => $upcomingDate->date->isoFormat('LL'), 'project' => $upcomingDate->project, 'description' => $upcomingDate->description])}} 20 | @endforeach 21 | @endcomponent 22 | @endif 23 | 24 | ## {{ trans('emails.news') }} 25 | @foreach ($reports as $report) 26 | @component('mail::panel') 27 | ## {{ $report->project }} {{ $report->project }} 28 | 29 | - **{{ trans('emails.mood') }}** {{ $report->spirit }} 30 | - **{{ trans('emails.priority') }}** {{ $report->priorities }} 31 | - **{{ trans('emails.ups_and_downs') }}** {{ $report->victories }} 32 | @if (isset($report->help)) 33 | - **{{ trans('emails.help') }}** {{ $report->help }} 34 | @endif 35 | @endcomponent 36 | @endforeach 37 | 38 | @if ($projectsNoInfo->count() > 0) 39 | {{ trans('emails.no_news', ['projects' => $projectsNoInfo->implode(', ')]) }} 40 | @else 41 | {{ trans('emails.all_filled') }} 42 | @endif 43 | 44 | {{ trans('emails.outro') }} 45 | 46 | @endcomponent 47 | -------------------------------------------------------------------------------- /resources/views/index.blade.php: -------------------------------------------------------------------------------- 1 | @extends('master') 2 | 3 | @section('content') 4 | @if ($canBeFilled and ! $allFilled) 5 |
6 | @csrf 7 |

{{ trans('form.retrospective_title', compact('week')) }}

8 |

{{ trans('form.description') }}

9 | 10 |
11 |

@lang('form.previous_bulletins.week')

12 |
13 | 14 | @if ($errors->any()) 15 |
16 | @foreach ($errors->all() as $error) 17 | {{ $error }}
18 | @endforeach 19 |
20 | @endif 21 | 22 |
23 | 24 | 36 |
37 | 38 |
39 |
40 | {{ trans('form.team_mood') }} 41 | 42 | 43 | 44 | 45 |
46 |
47 | 48 |
49 | 50 | 51 |
52 | 53 |
54 | 55 | 56 |
57 | 58 |
59 | 60 | 61 |
62 | 63 |
64 | 65 | 66 | 67 |
68 | 69 |
70 | 71 |
72 |
73 | @else 74 |

{{ trans('form.retrospective_title', compact('week')) }}

75 |

76 | {{ trans('form.cant_fill') }} 77 |

78 | @endif 79 | @endsection 80 | -------------------------------------------------------------------------------- /resources/views/login.blade.php: -------------------------------------------------------------------------------- 1 | @extends('master') 2 | 3 | @section('content') 4 |
5 | @csrf 6 |

{{ trans('login.title') }}

7 | 8 |

{{ trans('login.description') }}

9 | 10 | @if (session()->has('error')) 11 |
12 | {{ session('error') }} 13 |
14 | @endif 15 | 16 | @if ($passwordHint) 17 |
18 |

{{ $passwordHint }}

19 |
20 | @endif 21 | 22 |
23 | 24 | 25 |
26 | 27 |
28 | 29 |
30 |
31 | @endsection 32 | -------------------------------------------------------------------------------- /resources/views/reports/_info.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | {{ $projectName }} 5 |

6 | 7 | @if(! $currentProject->isActive()) 8 | 9 | {{ trans('reports.ended_on', ['date_str' => $currentProject->endsOn->isoFormat('LL')]) }} 10 | 11 | @elseif($currentProject->isActive() && $currentProject->endsOn) 12 | 13 | {{ trans('reports.ends_on', ['date_str' => $currentProject->endsOn->isoFormat('LL')]) }} 14 | 15 | @endif 16 |
17 | 18 | 19 | @if($reports->count() > 1) 20 |
21 | 22 | 27 |
28 | @endif 29 | 30 | @if($pastDates->count() > 0) 31 |
32 | 33 | 38 |
39 | @endif 40 | 41 | @if($upcomingDates->count() > 0) 42 |
43 | 44 | 49 |
50 | @endif 51 | 52 |
53 | 54 | 55 | 56 | {{ trans('reports.share') }} 57 | 58 | 59 | 60 | 61 | {{ trans('reports.csv_export') }} 62 | 63 | 64 |
65 |
66 | -------------------------------------------------------------------------------- /resources/views/reports/index.blade.php: -------------------------------------------------------------------------------- 1 | @extends('master') 2 | 3 | @section('section-class') 4 | section-grey 5 | @endsection 6 | 7 | @section('content') 8 | @php 9 | $projectName = $currentProject->name; 10 | @endphp 11 | 12 |
13 | 24 |
25 | @include('reports._info') 26 | 27 | @foreach($reports as $month => $reports) 28 |
29 | 30 | {{ ucfirst($month) }} 31 |
32 | @foreach($reports as $report) 33 |
34 |
35 |

36 | 37 | {{ $report->week_number }} 38 |

39 | 40 | {{ trans('reports.time_period', ['start' => $report->startOfWeek->isoFormat('LL'), 'end' => $report->endOfWeek->isoFormat('LL')]) }} 41 | 42 |
43 |
    44 |
  • {{ trans('reports.mood') }} {{ $report->spirit }}
  • 45 |
  • {{ trans('reports.priority') }} {!! nl2br(e($report->priorities)) !!}
  • 46 |
  • {{ trans('reports.ups_and_downs') }} {!! nl2br(e($report->victories)) !!}
  • 47 | @if ($report->help) 48 |
  • {{ trans('reports.help') }} {!! nl2br(e($report->help)) !!}
  • 49 | @endif 50 |
51 |
52 | @endforeach 53 | @endforeach 54 | 55 |
56 | 73 |
74 | @endsection 75 | -------------------------------------------------------------------------------- /resources/views/reports/week_index.blade.php: -------------------------------------------------------------------------------- 1 | @extends('master') 2 | 3 | @section('section-class') 4 | section-grey 5 | @endsection 6 | 7 | @section('content') 8 |
9 |
10 |
11 |
12 |

{{ trans('reports.previous_reports') }}

13 |
14 |
15 | @foreach($data as $month => $reports) 16 | 17 |
    18 | @foreach($reports->pluck('week_number') as $week) 19 |
  • 20 | {{ $week }} 21 |
  • 22 | @endforeach 23 |
24 | @endforeach 25 | 26 |
27 |
28 |
29 |
30 | @endsection 31 | -------------------------------------------------------------------------------- /resources/views/success.blade.php: -------------------------------------------------------------------------------- 1 | @extends('master') 2 | 3 | @section('content') 4 |

{{ trans('form.retrospective_title', compact('week')) }}

5 |
{{ trans('success.saved') }}
6 | 7 |

{{ trans('success.thanks') }}

8 | 9 |

10 | @endsection 11 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/button.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/footer.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/header.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @if (trim($slot) === 'Laravel') 5 | 6 | @else 7 | {{ $slot }} 8 | @endif 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/layout.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 25 | 26 | 27 | 28 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/message.blade.php: -------------------------------------------------------------------------------- 1 | @component('mail::layout') 2 | {{-- Header --}} 3 | @slot('header') 4 | @component('mail::header', ['url' => config('app.url')]) 5 | {{ config('app.name') }} 6 | @endcomponent 7 | @endslot 8 | 9 | {{-- Body --}} 10 | {{ $slot }} 11 | 12 | {{-- Subcopy --}} 13 | @isset($subcopy) 14 | @slot('subcopy') 15 | @component('mail::subcopy') 16 | {{ $subcopy }} 17 | @endcomponent 18 | @endslot 19 | @endisset 20 | 21 | {{-- Footer --}} 22 | @slot('footer') 23 | @component('mail::footer') 24 | © {{ date('Y') }} {{ config('app.name') }}.
{{ trans('emails.footer') }} 25 | @endcomponent 26 | @endslot 27 | @endcomponent 28 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/panel.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/subcopy.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/table.blade.php: -------------------------------------------------------------------------------- 1 |
2 | {{ Illuminate\Mail\Markdown::parse($slot) }} 3 |
4 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/html/themes/default.css: -------------------------------------------------------------------------------- 1 | /* Base */ 2 | 3 | body, 4 | body *:not(html):not(style):not(br):not(tr):not(code) { 5 | box-sizing: border-box; 6 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 7 | 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; 8 | position: relative; 9 | } 10 | 11 | body { 12 | -webkit-text-size-adjust: none; 13 | background-color: #ffffff; 14 | color: #718096; 15 | height: 100%; 16 | line-height: 1.4; 17 | margin: 0; 18 | padding: 0; 19 | width: 100% !important; 20 | } 21 | 22 | p, 23 | ul, 24 | ol, 25 | blockquote { 26 | line-height: 1.4; 27 | text-align: left; 28 | } 29 | 30 | a { 31 | color: #3869d4; 32 | } 33 | 34 | a img { 35 | border: none; 36 | } 37 | 38 | /* Typography */ 39 | 40 | h1 { 41 | color: #3d4852; 42 | font-size: 18px; 43 | font-weight: bold; 44 | margin-top: 0; 45 | text-align: left; 46 | } 47 | 48 | h2 { 49 | font-size: 16px; 50 | font-weight: bold; 51 | margin-top: 0; 52 | text-align: left; 53 | } 54 | 55 | h3 { 56 | font-size: 14px; 57 | font-weight: bold; 58 | margin-top: 0; 59 | text-align: left; 60 | } 61 | 62 | p { 63 | font-size: 16px; 64 | line-height: 1.5em; 65 | margin-top: 0; 66 | text-align: left; 67 | } 68 | 69 | p.sub { 70 | font-size: 12px; 71 | } 72 | 73 | img { 74 | max-width: 100%; 75 | } 76 | 77 | /* Layout */ 78 | 79 | .wrapper { 80 | -premailer-cellpadding: 0; 81 | -premailer-cellspacing: 0; 82 | -premailer-width: 100%; 83 | background-color: #edf2f7; 84 | margin: 0; 85 | padding: 0; 86 | width: 100%; 87 | } 88 | 89 | .content { 90 | -premailer-cellpadding: 0; 91 | -premailer-cellspacing: 0; 92 | -premailer-width: 100%; 93 | margin: 0; 94 | padding: 0; 95 | width: 100%; 96 | } 97 | 98 | /* Header */ 99 | 100 | .header { 101 | padding: 25px 0; 102 | text-align: center; 103 | } 104 | 105 | .header a { 106 | color: #3d4852; 107 | font-size: 19px; 108 | font-weight: bold; 109 | text-decoration: none; 110 | } 111 | 112 | /* Logo */ 113 | 114 | .logo { 115 | height: 75px; 116 | width: 75px; 117 | } 118 | 119 | /* Body */ 120 | 121 | .body { 122 | -premailer-cellpadding: 0; 123 | -premailer-cellspacing: 0; 124 | -premailer-width: 100%; 125 | background-color: #edf2f7; 126 | border-bottom: 1px solid #edf2f7; 127 | border-top: 1px solid #edf2f7; 128 | margin: 0; 129 | padding: 0; 130 | width: 100%; 131 | } 132 | 133 | .inner-body { 134 | -premailer-cellpadding: 0; 135 | -premailer-cellspacing: 0; 136 | -premailer-width: 570px; 137 | background-color: #ffffff; 138 | border-color: #e8e5ef; 139 | border-radius: 2px; 140 | border-width: 1px; 141 | box-shadow: 0 2px 0 rgba(0, 0, 150, 0.025), 2px 4px 0 rgba(0, 0, 150, 0.015); 142 | margin: 0 auto; 143 | padding: 0; 144 | width: 570px; 145 | } 146 | 147 | /* Subcopy */ 148 | 149 | .subcopy { 150 | border-top: 1px solid #e8e5ef; 151 | margin-top: 25px; 152 | padding-top: 25px; 153 | } 154 | 155 | .subcopy p { 156 | font-size: 14px; 157 | } 158 | 159 | /* Footer */ 160 | 161 | .footer { 162 | -premailer-cellpadding: 0; 163 | -premailer-cellspacing: 0; 164 | -premailer-width: 570px; 165 | margin: 0 auto; 166 | padding: 0; 167 | text-align: center; 168 | width: 570px; 169 | } 170 | 171 | .footer p { 172 | color: #b0adc5; 173 | font-size: 12px; 174 | text-align: center; 175 | } 176 | 177 | .footer a { 178 | color: #b0adc5; 179 | text-decoration: underline; 180 | } 181 | 182 | /* Tables */ 183 | 184 | .table table { 185 | -premailer-cellpadding: 0; 186 | -premailer-cellspacing: 0; 187 | -premailer-width: 100%; 188 | margin: 30px auto; 189 | width: 100%; 190 | } 191 | 192 | .table th { 193 | border-bottom: 1px solid #edeff2; 194 | margin: 0; 195 | padding-bottom: 8px; 196 | } 197 | 198 | .table td { 199 | color: #74787e; 200 | font-size: 15px; 201 | line-height: 18px; 202 | margin: 0; 203 | padding: 10px 0; 204 | } 205 | 206 | .content-cell { 207 | max-width: 100vw; 208 | padding: 32px; 209 | } 210 | 211 | /* Buttons */ 212 | 213 | .action { 214 | -premailer-cellpadding: 0; 215 | -premailer-cellspacing: 0; 216 | -premailer-width: 100%; 217 | margin: 30px auto; 218 | padding: 0; 219 | text-align: center; 220 | width: 100%; 221 | } 222 | 223 | .button { 224 | -webkit-text-size-adjust: none; 225 | border-radius: 4px; 226 | color: #fff; 227 | display: inline-block; 228 | overflow: hidden; 229 | text-decoration: none; 230 | text-decoration: none; 231 | } 232 | 233 | .button-blue, 234 | .button-primary { 235 | background-color: #2d3748; 236 | border-bottom: 8px solid #2d3748; 237 | border-left: 18px solid #2d3748; 238 | border-right: 18px solid #2d3748; 239 | border-top: 8px solid #2d3748; 240 | } 241 | 242 | .button-green, 243 | .button-success { 244 | background-color: #48bb78; 245 | border-bottom: 8px solid #48bb78; 246 | border-left: 18px solid #48bb78; 247 | border-right: 18px solid #48bb78; 248 | border-top: 8px solid #48bb78; 249 | } 250 | 251 | .button-red, 252 | .button-error { 253 | background-color: #e53e3e; 254 | border-bottom: 8px solid #e53e3e; 255 | border-left: 18px solid #e53e3e; 256 | border-right: 18px solid #e53e3e; 257 | border-top: 8px solid #e53e3e; 258 | } 259 | 260 | /* Panels */ 261 | 262 | .panel { 263 | margin: 21px 0; 264 | } 265 | 266 | .panel-content { 267 | background-color: #edf2f7; 268 | color: #718096; 269 | padding: 16px; 270 | } 271 | 272 | .panel-content p { 273 | color: #718096; 274 | } 275 | 276 | .panel-item { 277 | padding: 0; 278 | } 279 | 280 | .panel-item p:last-of-type { 281 | margin-bottom: 0; 282 | padding-bottom: 0; 283 | } 284 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/button.blade.php: -------------------------------------------------------------------------------- 1 | {{ $slot }}: {{ $url }} 2 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/footer.blade.php: -------------------------------------------------------------------------------- 1 | {{ $slot }} 2 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/header.blade.php: -------------------------------------------------------------------------------- 1 | [{{ $slot }}]({{ $url }}) 2 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/layout.blade.php: -------------------------------------------------------------------------------- 1 | {!! strip_tags($header) !!} 2 | 3 | {!! strip_tags($slot) !!} 4 | @isset($subcopy) 5 | 6 | {!! strip_tags($subcopy) !!} 7 | @endisset 8 | 9 | {!! strip_tags($footer) !!} 10 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/message.blade.php: -------------------------------------------------------------------------------- 1 | @component('mail::layout') 2 | {{-- Header --}} 3 | @slot('header') 4 | @component('mail::header', ['url' => config('app.url')]) 5 | {{ config('app.name') }} 6 | @endcomponent 7 | @endslot 8 | 9 | {{-- Body --}} 10 | {{ $slot }} 11 | 12 | {{-- Subcopy --}} 13 | @isset($subcopy) 14 | @slot('subcopy') 15 | @component('mail::subcopy') 16 | {{ $subcopy }} 17 | @endcomponent 18 | @endslot 19 | @endisset 20 | 21 | {{-- Footer --}} 22 | @slot('footer') 23 | @component('mail::footer') 24 | © {{ date('Y') }} {{ config('app.name') }}. {{ trans('emails.footer') }} 25 | @endcomponent 26 | @endslot 27 | @endcomponent 28 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/panel.blade.php: -------------------------------------------------------------------------------- 1 | {{ $slot }} 2 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/subcopy.blade.php: -------------------------------------------------------------------------------- 1 | {{ $slot }} 2 | -------------------------------------------------------------------------------- /resources/views/vendor/mail/text/table.blade.php: -------------------------------------------------------------------------------- 1 | {{ $slot }} 2 | -------------------------------------------------------------------------------- /routes/api.php: -------------------------------------------------------------------------------- 1 | get('/user', function (Request $request) { 17 | return $request->user(); 18 | }); 19 | -------------------------------------------------------------------------------- /routes/channels.php: -------------------------------------------------------------------------------- 1 | id === (int) $id; 16 | }); 17 | -------------------------------------------------------------------------------- /routes/console.php: -------------------------------------------------------------------------------- 1 | comment(Inspiring::quote()); 18 | })->describe('Display an inspiring quote'); 19 | -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | name('home'); 4 | Route::get('/login', 'HomeController@login')->name('login'); 5 | Route::post('/login', 'HomeController@authenticate')->name('login'); 6 | Route::post('/reports/store', 'ReportsController@store')->name('reports.store'); 7 | Route::get('/reports', 'ReportsController@choose')->name('reports.choose'); 8 | Route::get('/reports/week', 'ReportsController@weekIndex')->name('reports.week_index')->middleware('logged_in'); 9 | Route::get('/reports/{reports}', 'ReportsController@index')->name('reports.index')->middleware('logged_in'); 10 | Route::get('/dates', 'DatesController@index')->name('dates.index')->middleware('logged_in'); 11 | Route::get('/reports/{reports}/export', 'ReportsController@export')->name('reports.export')->middleware('logged_in'); 12 | Route::view('/about', 'about')->name('about'); 13 | Route::get('/email/{week?}', function ($week = null) { 14 | return (new App\Mail\WeeklyReport($week))->render(); 15 | })->name('email_report')->middleware('logged_in'); 16 | Route::get('/locale/{locale}', 'LanguagesController@setLocale')->name('setLocale'); 17 | -------------------------------------------------------------------------------- /server.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | $uri = urldecode( 9 | parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) 10 | ); 11 | 12 | // This file allows us to emulate Apache's "mod_rewrite" functionality from the 13 | // built-in PHP web server. This provides a convenient way to test a Laravel 14 | // application without having installed a "real" web server software here. 15 | if ($uri !== '/' && file_exists(__DIR__.'/public'.$uri)) { 16 | return false; 17 | } 18 | 19 | require_once __DIR__.'/public/index.php'; 20 | -------------------------------------------------------------------------------- /storage/app/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !public/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/app/public/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/.gitignore: -------------------------------------------------------------------------------- 1 | config.php 2 | routes.php 3 | schedule-* 4 | compiled.php 5 | services.json 6 | events.scanned.php 7 | routes.scanned.php 8 | down 9 | -------------------------------------------------------------------------------- /storage/framework/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !data/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/framework/cache/data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/sessions/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/testing/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/views/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /tests/CreatesApplication.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class)->bootstrap(); 19 | 20 | return $app; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/Feature/KeyDatesHistoryTest.php: -------------------------------------------------------------------------------- 1 | get(route('dates.index'))->assertRedirect(route('login')); 16 | 17 | $this 18 | ->withSession(['logged_in' => true]) 19 | ->get(route('dates.index')) 20 | ->assertNotFound(); 21 | } 22 | 23 | public function testDatesIndex() 24 | { 25 | factory(Date::class)->create(); 26 | 27 | $this->get(route('dates.index'))->assertRedirect(route('login')); 28 | 29 | $this 30 | ->withSession(['logged_in' => true]) 31 | ->get(route('dates.index')) 32 | ->assertOk(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/Feature/LoginTest.php: -------------------------------------------------------------------------------- 1 | 'Password hint']); 12 | 13 | $this->get(route('login'))->assertOk()->assertSee('Password hint'); 14 | 15 | config(['app.reports_password_hint' => null]); 16 | 17 | $this->get(route('login'))->assertOk()->assertDontSee('text-quote'); 18 | } 19 | 20 | public function testInvalidSubmitForm() 21 | { 22 | session()->setPreviousUrl(route('login')); 23 | 24 | $this 25 | ->post(route('login'), ['password' => 'nope']) 26 | ->assertRedirect(route('login')) 27 | ->assertSessionHas('error'); 28 | 29 | $this->assertFalse(session()->has('logged_in')); 30 | } 31 | 32 | public function testLoginSuccess() 33 | { 34 | $this 35 | ->post(route('login'), ['password' => config('app.report_secret')]) 36 | ->assertSessionMissing('error') 37 | ->assertRedirect(route('reports.choose')); 38 | 39 | $this->assertTrue(session()->has('logged_in')); 40 | } 41 | 42 | public function testRedirectedIntended() 43 | { 44 | redirect()->setIntendedUrl(route('about')); 45 | 46 | $this 47 | ->post(route('login'), ['password' => config('app.report_secret')]) 48 | ->assertSessionMissing('error') 49 | ->assertRedirect(route('about')); 50 | 51 | $this->assertTrue(session()->has('logged_in')); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/Feature/PagesTest.php: -------------------------------------------------------------------------------- 1 | get(route('about'))->assertStatus(200); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/Feature/ReportsHistoryByProjectTest.php: -------------------------------------------------------------------------------- 1 | get(route('reports.choose'))->assertStatus(404); 19 | 20 | $report = factory(Report::class)->create(); 21 | 22 | $this->get(route('reports.choose')) 23 | ->assertRedirect(route('reports.index', $report->project)); 24 | } 25 | 26 | public function testIndexNotLoggedIn() 27 | { 28 | $this->get(route('reports.index', $this->projectNames()[0]))->assertRedirect(route('login')); 29 | } 30 | 31 | public function testIndexLoggedIn() 32 | { 33 | Carbon::setTestNow(Carbon::create(2019, 3, 15, 14)); 34 | 35 | $this 36 | ->withSession(['logged_in' => true]) 37 | ->get(route('reports.index', $this->projectNames()[0]))->assertStatus(404); 38 | 39 | $notPublished = factory(Report::class)->create([ 40 | 'project' => $this->projectNames()[0], 41 | 'week_number' => '2019-11', 42 | ]); 43 | 44 | $dummy = factory(Report::class)->create(['project' => $this->projectNames()[1]]); 45 | 46 | $report = factory(Report::class)->create([ 47 | 'project' => $this->projectNames()[0], 48 | 'week_number' => '2019-10', 49 | ]); 50 | factory(Date::class, 5)->create(['project' => $this->projectNames()[1]]); 51 | factory(Date::class, 5)->create(['project' => $report->project]); 52 | 53 | $response = $this 54 | ->withSession(['logged_in' => true]) 55 | ->get(route('reports.index', $report->project)); 56 | 57 | $response 58 | ->assertStatus(200) 59 | ->assertViewIs('reports.index') 60 | ->assertViewHas('projects', collect([$dummy->project, $report->project])->sort()->values()) 61 | ->assertViewHas('currentProject', $report->projectObject()) 62 | ->assertViewHas('shareUrl', URL::signedRoute('reports.index', $report->project)) 63 | ->assertViewHas('downloadUrl', URL::signedRoute('reports.export', $report->project)) 64 | ->assertViewHas('reports', Report::where('project', $report->project)->where('week_number', '2019-10')->get()->groupBy->month) 65 | ->assertViewHas('upcomingDates', Date::forProject($report->project)->upcoming()->get()) 66 | ->assertViewHas('pastDates', Date::forProject($report->project)->past()->get()); 67 | } 68 | 69 | public function testIndexWithSignature() 70 | { 71 | $report = factory(Report::class)->create(); 72 | 73 | $this->get(URL::signedRoute('reports.index', $report->project)) 74 | ->assertOk() 75 | ->assertHeader('Referrer-Policy', 'no-referrer'); 76 | } 77 | 78 | private function projectNames() 79 | { 80 | return config('app.projects')->map->name; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /tests/Feature/ReportsHistoryByWeekTest.php: -------------------------------------------------------------------------------- 1 | create(); 16 | 17 | $this->get(route('reports.week_index'))->assertRedirect(route('login')); 18 | // Intended URL has been saved in session 19 | $this->assertEquals(route('reports.week_index'), session()->get('url.intended')); 20 | 21 | $this 22 | ->withSession(['logged_in' => true]) 23 | ->get(route('reports.week_index')) 24 | ->assertOk() 25 | ->assertViewHas('data'); 26 | } 27 | 28 | public function testWeekHistory() 29 | { 30 | $report = factory(Report::class)->create(); 31 | 32 | $this 33 | ->get(route('email_report', $report->week_number)) 34 | ->assertRedirect(route('login')); 35 | 36 | $response = $this 37 | ->withSession(['logged_in' => true]) 38 | ->get(route('email_report', $report->week_number)); 39 | 40 | $response->assertOk(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/Feature/ReportsProjectExportTest.php: -------------------------------------------------------------------------------- 1 | create(); 16 | 17 | $this 18 | ->get(route('reports.export', $report->project)) 19 | ->assertRedirect(route('login')); 20 | 21 | $response = $this 22 | ->withSession(['logged_in' => true]) 23 | ->get(route('reports.export', $report->project)); 24 | 25 | $response->assertOk(); 26 | $response->assertHeader('content-disposition'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/Feature/SwitchLocaleTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(app()->getLocale(), 'en'); 15 | 16 | $this->get(route('home')) 17 | ->assertOk() 18 | ->assertSee(trans('layout.previous_bulletins', [], 'en')); 19 | 20 | $this->get(route('setLocale', 'fr')); 21 | 22 | $this->get(route('home')) 23 | ->assertOk() 24 | ->assertSee(trans('layout.previous_bulletins', [], 'fr')); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Feature/WeeklyReportTest.php: -------------------------------------------------------------------------------- 1 | expectException(UnexpectedValueException::class); 20 | 21 | new WeeklyReport('foo'); 22 | } 23 | 24 | public function testNoReports() 25 | { 26 | $this->expectException(InvalidArgumentException::class); 27 | 28 | (new WeeklyReport('2019-01'))->build(); 29 | } 30 | 31 | public function testHasReports() 32 | { 33 | $class = new WeeklyReport('2019-01'); 34 | 35 | $this->assertFalse($class->hasReports()); 36 | 37 | factory(Report::class)->create(['week_number' => '2019-01']); 38 | 39 | $this->assertTrue($class->hasReports()); 40 | } 41 | 42 | public function testForWeek() 43 | { 44 | $week = '2019-20'; 45 | $names = config('app.projects')->names(); 46 | 47 | factory(Report::class)->create(['week_number' => $week, 'project' => $names[0]]); 48 | factory(Report::class)->create(['week_number' => $week, 'project' => $names[1]]); 49 | factory(Report::class)->create(['week_number' => '2018-01']); 50 | 51 | factory(Date::class)->create(['date' => '2019-05-17']); 52 | factory(Date::class)->create(['date' => '2019-05-24']); 53 | factory(Date::class)->create(['date' => '2019-05-25']); 54 | 55 | $view = (new WeeklyReport($week))->build(); 56 | 57 | $this->assertEquals($view->subject, 'Bulettins of the week 2019-20'); 58 | 59 | $this->assertEquals($view->viewData['weekNumber'], $week); 60 | $this->assertEquals($view->viewData['reports']->pluck('id')->sort()->values(), Report::forWeek($week)->pluck('id')->sort()->values()); 61 | $this->assertEquals($view->viewData['upcomingDates']->pluck('id'), Date::where('date', '2019-05-24')->pluck('id')); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | $value) { 14 | self::assertArrayHasKey($key, $array); 15 | self::assertEquals($value, $array[$key]); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/Unit/DateTest.php: -------------------------------------------------------------------------------- 1 | create(['project' => 'A']); 16 | factory(Date::class)->create(['project' => 'B', 'date' => '2015-01-01']); 17 | factory(Date::class)->create(['project' => 'B', 'date' => '2015-01-02']); 18 | 19 | $this->assertEquals(1, Date::forProject('A')->count()); 20 | $this->assertEquals(2, Date::forProject('B')->count()); 21 | } 22 | 23 | public function testUpcoming() 24 | { 25 | $a = factory(Date::class)->create(['date' => now()]); 26 | $b = factory(Date::class)->create(['date' => now()->addDays(2)]); 27 | $c = factory(Date::class)->create(['date' => now()->subDays(1)]); 28 | 29 | $this->assertEquals(2, Date::upcoming()->count()); 30 | $this->assertEquals(1, Date::past()->count()); 31 | 32 | $this->assertTrue(Date::upcoming()->get()->contains($a)); 33 | $this->assertTrue(Date::upcoming()->get()->contains($b)); 34 | $this->assertTrue(Date::past()->get()->contains($c)); 35 | } 36 | 37 | public function testProjectObject() 38 | { 39 | $date = factory(Date::class)->create(); 40 | 41 | $this->assertInstanceOf(\App\Project::class, $date->projectObject()); 42 | $this->assertEquals($date->project, $date->projectObject()->name); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/Unit/ProjectTest.php: -------------------------------------------------------------------------------- 1 | expectException(UnexpectedValueException::class); 14 | factory(Project::class)->make(['channel' => 'foobar']); 15 | } 16 | 17 | public function testIsActive() 18 | { 19 | $project = factory(Project::class)->make(); 20 | $this->assertTrue($project->isActive()); 21 | 22 | $project = factory(Project::class)->make(['ends_on' => null]); 23 | $this->assertTrue($project->isActive()); 24 | 25 | $project = factory(Project::class)->state('inactive')->make(); 26 | $this->assertFalse($project->isActive()); 27 | } 28 | 29 | /** 30 | * @doesNotPerformAssertions 31 | */ 32 | public function testNotifyNoChannel() 33 | { 34 | $project = factory(Project::class)->make(); 35 | $project->notify(); 36 | } 37 | 38 | /** 39 | * @doesNotPerformAssertions 40 | */ 41 | public function testNotifySlackChannel() 42 | { 43 | $project = factory(Project::class)->make(['channel' => 'slack']); 44 | $project->notify(); 45 | } 46 | 47 | /** 48 | * @doesNotPerformAssertions 49 | */ 50 | public function testNotifyEmailChannel() 51 | { 52 | $project = factory(Project::class)->make(['channel' => 'email']); 53 | $project->notify(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/Unit/ProjectsTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(config('app.projects')->map->name, config('app.projects')->names()); 16 | } 17 | 18 | public function testIsActive() 19 | { 20 | $this->assertCount(3, config('app.projects')); 21 | $this->assertCount(2, config('app.projects')->active()); 22 | } 23 | 24 | public function testFilledProjectsFor() 25 | { 26 | $this->assertCount(0, config('app.projects')->filledProjectsFor('2019-10')); 27 | 28 | $report = factory(Report::class)->create(); 29 | 30 | $this->assertCount(1, config('app.projects')->filledProjectsFor($report->week_number)); 31 | $this->assertCount(0, config('app.projects')->filledProjectsFor('2000-10')); 32 | } 33 | 34 | public function testUnfilledProjectsFor() 35 | { 36 | $totalProjects = config('app.projects')->count(); 37 | 38 | $this->assertCount($totalProjects - 0, config('app.projects')->unfilledProjectsFor('2019-10')); 39 | 40 | $report = factory(Report::class)->create(); 41 | 42 | $this->assertCount($totalProjects - 1, config('app.projects')->unfilledProjectsFor($report->week_number)); 43 | $this->assertCount($totalProjects - 0, config('app.projects')->unfilledProjectsFor('2000-10')); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/Unit/ReportTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($expected, Report::canBeFilled()); 34 | } 35 | } 36 | 37 | public function testLastWorkingDayOfWeek() 38 | { 39 | $tests = [ 40 | ['FR', Carbon::create(2019, 3, 3), Carbon::create(2019, 3, 1)], 41 | ['FR', Carbon::create(2019, 3, 4), Carbon::create(2019, 3, 8)], 42 | ['FR', Carbon::create(2019, 3, 5), Carbon::create(2019, 3, 8)], 43 | ['FR', Carbon::create(2019, 3, 6), Carbon::create(2019, 3, 8)], 44 | ['FR', Carbon::create(2019, 3, 7), Carbon::create(2019, 3, 8)], 45 | ['FR', Carbon::create(2019, 3, 8), Carbon::create(2019, 3, 8)], 46 | [null, Carbon::create(2019, 3, 8), Carbon::create(2019, 3, 8)], 47 | ['FR', Carbon::create(2019, 3, 9), Carbon::create(2019, 3, 8)], 48 | ['FR', Carbon::create(2019, 3, 10), Carbon::create(2019, 3, 8)], 49 | ['FR', Carbon::create(2019, 3, 11), Carbon::create(2019, 3, 15)], 50 | ['FR', Carbon::create(2020, 4, 30), Carbon::create(2020, 4, 30)], 51 | ['FR', Carbon::create(2020, 5, 1), Carbon::create(2020, 4, 30)], 52 | ['CA', Carbon::create(2020, 5, 1), Carbon::create(2020, 5, 1)], 53 | ]; 54 | 55 | foreach ($tests as $test) { 56 | [$country, $date, $expected] = $test; 57 | Carbon::setTestNow($date); 58 | config(['app.report_country_code' => $country]); 59 | 60 | $this->assertEquals($expected, Report::lastWorkingDayOfWeek()); 61 | } 62 | } 63 | 64 | public function testlatestPublishedWeek() 65 | { 66 | $tests = [ 67 | [Carbon::create(2019, 3, 4), '2019-09'], 68 | [Carbon::create(2019, 3, 5), '2019-09'], 69 | [Carbon::create(2019, 3, 7), '2019-09'], 70 | [Carbon::create(2019, 3, 8, 14, 59), '2019-09'], 71 | [Carbon::create(2019, 3, 8, 15, 4), '2019-09'], 72 | [Carbon::create(2019, 3, 8, 15, 5), '2019-10'], 73 | [Carbon::create(2019, 3, 9), '2019-10'], 74 | [Carbon::create(2019, 3, 10), '2019-10'], 75 | [Carbon::create(2019, 3, 11), '2019-10'], 76 | ]; 77 | 78 | foreach ($tests as $test) { 79 | [$date, $expected] = $test; 80 | Carbon::setTestNow($date); 81 | 82 | $message = 'Wrong week for '.$date; 83 | 84 | $this->assertEquals($expected, Report::latestPublishedWeek(), $message); 85 | } 86 | } 87 | 88 | public function testProjectObject() 89 | { 90 | $report = factory(Report::class)->create(); 91 | 92 | $this->assertInstanceOf(\App\Project::class, $report->projectObject()); 93 | $this->assertEquals($report->project, $report->projectObject()->name); 94 | } 95 | 96 | public function testStartOfWeek() 97 | { 98 | $report = factory(Report::class)->make(['week_number' => '2019-10']); 99 | $report2 = factory(Report::class)->make(['week_number' => '2019-01']); 100 | 101 | $this->assertEquals(Carbon::create(2019, 3, 4), $report->startOfWeek); 102 | $this->assertEquals(Carbon::create(2018, 12, 31), $report2->startOfWeek); 103 | } 104 | 105 | public function testEndOfWeek() 106 | { 107 | $report = factory(Report::class)->make(['week_number' => '2019-10']); 108 | $report2 = factory(Report::class)->make(['week_number' => '2019-01']); 109 | 110 | $this->assertEquals(Carbon::create(2019, 3, 8), $report->endOfWeek); 111 | $this->assertEquals(Carbon::create(2019, 1, 4), $report2->endOfWeek); 112 | } 113 | 114 | public function testMonth() 115 | { 116 | $report = factory(Report::class)->make(['week_number' => '2019-10']); 117 | $report2 = factory(Report::class)->make(['week_number' => '2019-01']); 118 | 119 | $this->assertEquals('March 2019', $report->month); 120 | $this->assertEquals('December 2018', $report2->month); 121 | } 122 | 123 | public function testScopeWeek() 124 | { 125 | $report = factory(Report::class)->create(['week_number' => '2019-10']); 126 | $report2 = factory(Report::class)->create(['week_number' => '2019-01']); 127 | 128 | $this->assertEquals(collect([$report->id]), Report::forWeek('2019-10')->pluck('id')); 129 | $this->assertEquals(collect([$report2->id]), Report::forWeek('2019-01')->pluck('id')); 130 | } 131 | 132 | public function testScopePublished() 133 | { 134 | $report = factory(Report::class)->create(['week_number' => '2019-09']); 135 | $report2 = factory(Report::class)->create(['week_number' => '2019-10']); 136 | 137 | $tests = [ 138 | [Carbon::create(2019, 3, 4), [$report->id]], 139 | [Carbon::create(2019, 3, 5), [$report->id]], 140 | [Carbon::create(2019, 3, 7), [$report->id]], 141 | [Carbon::create(2019, 3, 8, 14, 59), [$report->id]], 142 | [Carbon::create(2019, 3, 8, 15, 4), [$report->id]], 143 | [Carbon::create(2019, 3, 8, 15, 5), [$report->id, $report2->id]], 144 | [Carbon::create(2019, 3, 9), [$report->id, $report2->id]], 145 | [Carbon::create(2019, 3, 10), [$report->id, $report2->id]], 146 | [Carbon::create(2019, 3, 11), [$report->id, $report2->id]], 147 | ]; 148 | 149 | foreach ($tests as $test) { 150 | [$date, $expected] = $test; 151 | Carbon::setTestNow($date); 152 | 153 | $this->assertEquals(collect($expected), Report::published()->orderBy('id')->pluck('id')); 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /tests/boostrap.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class))->bootstrap(); 24 | 25 | foreach ($commands as $command) { 26 | $console->call($command); 27 | } 28 | -------------------------------------------------------------------------------- /webpack.mix.js: -------------------------------------------------------------------------------- 1 | const mix = require('laravel-mix'); 2 | 3 | mix.js('resources/js/app.js', 'public/js').version(); 4 | 5 | mix.styles([ 6 | 'resources/css/all.css', 7 | ], 'public/css/all.css'); 8 | --------------------------------------------------------------------------------