├── log
└── .keep
├── tmp
└── .keep
├── lib
└── tasks
│ └── .keep
├── .ruby-version
├── client
├── static
│ ├── .gitkeep
│ ├── favicon.ico
│ ├── back-home.png
│ ├── icon-256.png
│ ├── logo-text.png
│ ├── logo-text-inverse.png
│ ├── illustrations
│ │ ├── colors.jpg
│ │ ├── grid.jpg
│ │ ├── logo.png
│ │ ├── visuals.jpg
│ │ ├── wording.jpg
│ │ ├── components.jpg
│ │ └── typography.jpg
│ ├── permanentmarker-regular.woff2
│ └── font-awesome-4.7.0
│ │ ├── fonts
│ │ ├── FontAwesome.otf
│ │ ├── fontawesome-webfont.eot
│ │ ├── fontawesome-webfont.ttf
│ │ ├── fontawesome-webfont.woff
│ │ └── fontawesome-webfont.woff2
│ │ └── HELP-US-OUT.txt
├── src
│ ├── styles
│ │ ├── _grid.scss
│ │ ├── variables
│ │ │ ├── _index.scss
│ │ │ ├── _dimensions.scss
│ │ │ └── colors.json
│ │ ├── _links.scss
│ │ ├── _fonts.css
│ │ ├── _forms.scss
│ │ ├── app.scss
│ │ └── _typography.scss
│ ├── api
│ │ ├── root.js
│ │ ├── features.js
│ │ ├── terms_of_services.js
│ │ ├── tasks.js
│ │ └── projects.js
│ ├── locales
│ │ ├── en
│ │ │ ├── index.js
│ │ │ └── formats.js
│ │ ├── fr
│ │ │ ├── index.js
│ │ │ └── formats.js
│ │ └── index.js
│ ├── components
│ │ ├── Ly
│ │ │ ├── LyColumns
│ │ │ │ ├── index.js
│ │ │ │ ├── LyColumn.vue
│ │ │ │ └── LyColumns.vue
│ │ │ ├── LyList
│ │ │ │ ├── LyListItemAdapt.vue
│ │ │ │ ├── index.js
│ │ │ │ └── LyListGroup.vue
│ │ │ ├── LyPopover
│ │ │ │ ├── LyPopoverSeparator.vue
│ │ │ │ ├── index.js
│ │ │ │ └── LyPopoverItem.vue
│ │ │ ├── LyForm
│ │ │ │ ├── index.js
│ │ │ │ ├── LyFormGroup.vue
│ │ │ │ ├── LyForm.vue
│ │ │ │ └── LyFormTextarea.vue
│ │ │ ├── LyTextContainer.vue
│ │ │ ├── LyIcon.vue
│ │ │ ├── LyCardDeck.vue
│ │ │ └── LySection.vue
│ │ ├── App
│ │ │ ├── App.vue
│ │ │ ├── AppPage.vue
│ │ │ └── AppLogo.vue
│ │ ├── projects
│ │ │ ├── ProjectsHeader.vue
│ │ │ ├── ProjectsStartNewModal.vue
│ │ │ ├── ProjectStartModal.vue
│ │ │ ├── ProjectEditDueDateModal.vue
│ │ │ ├── ProjectFinishModal.vue
│ │ │ ├── ProjectEditPage.vue
│ │ │ ├── ProjectDeleteModal.vue
│ │ │ ├── ProjectItem.vue
│ │ │ ├── ProjectItemFinished.vue
│ │ │ ├── ProjectCardDeck.vue
│ │ │ ├── ProjectContainer.vue
│ │ │ ├── ProjectEditDueDateForm.vue
│ │ │ ├── ProjectFinishForm.vue
│ │ │ ├── ProjectStartForm.vue
│ │ │ └── ProjectCard.vue
│ │ ├── general
│ │ │ ├── TermsOfServiceModal.vue
│ │ │ ├── NotFoundPage.vue
│ │ │ └── LoadingPage.vue
│ │ ├── tasks
│ │ │ ├── TaskSelectList.vue
│ │ │ ├── TaskAttachProjectModal.vue
│ │ │ ├── TaskSelectableItem.vue
│ │ │ ├── TaskConfirmAbandonModal.vue
│ │ │ ├── TaskIndicators.vue
│ │ │ ├── TaskTransformInProjectModal.vue
│ │ │ ├── TasksPage.vue
│ │ │ ├── TaskList.vue
│ │ │ └── TasksPlanModal.vue
│ │ ├── users
│ │ │ ├── UserLoginPage.vue
│ │ │ ├── UserPasswordNewPage.vue
│ │ │ ├── UserActivatePage.vue
│ │ │ ├── UserPasswordNewForm.vue
│ │ │ ├── UserPopover.vue
│ │ │ ├── UserPasswordResetForm.vue
│ │ │ └── UserRegisterForm.vue
│ │ ├── layouts
│ │ │ ├── LayoutEmpty.vue
│ │ │ ├── LayoutDesign.vue
│ │ │ └── LayoutProfile.vue
│ │ ├── design
│ │ │ ├── DesignGridPage.vue
│ │ │ ├── DesignWordingPage.vue
│ │ │ └── DesignVisualsPage.vue
│ │ ├── mixins
│ │ │ ├── ErrorsHandler.js
│ │ │ └── ResourcesLoader.js
│ │ └── profile
│ │ │ └── ProfileDeleteAccount.vue
│ ├── utils
│ │ ├── object.js
│ │ ├── array.js
│ │ └── color.js
│ ├── store
│ │ ├── modules
│ │ │ ├── features.js
│ │ │ ├── terms_of_services.js
│ │ │ └── global.js
│ │ ├── index.js
│ │ └── plugins
│ │ │ └── cable.js
│ └── auth.js
├── .eslintignore
├── config
│ ├── prod.env.js
│ ├── test.env.js
│ └── dev.env.js
├── spec
│ ├── setup.js
│ ├── jest.conf.js
│ └── components
│ │ └── Ly
│ │ └── LyButton.spec.js
├── .gitignore
├── .editorconfig
├── build
│ ├── vue-loader.conf.js
│ ├── webpack.test.conf.js
│ ├── build.js
│ └── check-versions.js
├── .babelrc
├── index.html
├── .eslintrc.js
└── .scss.yml
├── spec
├── factories
│ ├── .keep
│ ├── terms_of_service.rb
│ ├── user.rb
│ ├── projects.rb
│ └── tasks.rb
├── support
│ ├── json_matchers.rb
│ ├── factory_bot.rb
│ └── api
│ │ └── schemas
│ │ ├── tasks
│ │ ├── create.json
│ │ ├── show.json
│ │ ├── update.json
│ │ ├── update_state.json
│ │ ├── index.json
│ │ └── update_order.json
│ │ ├── users
│ │ ├── me.json
│ │ ├── create.json
│ │ ├── passwords
│ │ │ └── create.json
│ │ ├── activations
│ │ │ └── create.json
│ │ ├── authorizations
│ │ │ └── create.json
│ │ ├── accept_tos.json
│ │ ├── user.json
│ │ └── update.json
│ │ ├── projects
│ │ ├── show.json
│ │ ├── create.json
│ │ ├── update.json
│ │ ├── update_state.json
│ │ └── index.json
│ │ ├── welcome
│ │ └── index.json
│ │ ├── features
│ │ └── index.json
│ │ ├── terms_of_services
│ │ └── current.json
│ │ └── errors.json
├── shared_examples_for_failures.rb
├── json_web_token_spec.rb
├── mailers
│ └── user_mailer_spec.rb
└── requests
│ └── api
│ └── terms_of_services_request_spec.rb
├── app
├── models
│ ├── concerns
│ │ └── .keep
│ ├── application_record.rb
│ ├── flipper_gate.rb
│ ├── flipper_feature.rb
│ ├── terms_of_service.rb
│ └── project.rb
├── controllers
│ ├── concerns
│ │ └── .keep
│ ├── application_controller.rb
│ ├── api
│ │ ├── users
│ │ │ ├── features_controller.rb
│ │ │ ├── activation_emails_controller.rb
│ │ │ ├── activations_controller.rb
│ │ │ ├── password_resets_controller.rb
│ │ │ ├── authorizations_controller.rb
│ │ │ ├── projects_controller.rb
│ │ │ ├── passwords_controller.rb
│ │ │ └── tasks_controller.rb
│ │ ├── welcome_controller.rb
│ │ ├── terms_of_services_controller.rb
│ │ ├── users_controller.rb
│ │ ├── tasks_controller.rb
│ │ └── projects_controller.rb
│ └── admin
│ │ ├── users_controller.rb
│ │ ├── application_controller.rb
│ │ ├── terms_of_services_controller.rb
│ │ └── user_sessions_controller.rb
├── jobs
│ └── application_job.rb
├── views
│ ├── layouts
│ │ ├── mailer.text.erb
│ │ └── mailer.html.erb
│ ├── api
│ │ ├── tasks
│ │ │ ├── show.jbuilder
│ │ │ ├── update.jbuilder
│ │ │ ├── update_state.jbuilder
│ │ │ ├── update_order.jbuilder
│ │ │ └── _task.jbuilder
│ │ ├── users
│ │ │ ├── show.jbuilder
│ │ │ ├── tasks
│ │ │ │ ├── create.jbuilder
│ │ │ │ └── index.jbuilder
│ │ │ ├── projects
│ │ │ │ ├── create.jbuilder
│ │ │ │ └── index.jbuilder
│ │ │ ├── create.jbuilder
│ │ │ ├── features
│ │ │ │ └── index.jbuilder
│ │ │ ├── activations
│ │ │ │ └── create.jbuilder
│ │ │ ├── authorizations
│ │ │ │ └── create.jbuilder
│ │ │ ├── passwords
│ │ │ │ └── create.jbuilder
│ │ │ ├── accept_tos.jbuilder
│ │ │ ├── update.jbuilder
│ │ │ └── _user.jbuilder
│ │ ├── projects
│ │ │ ├── show.jbuilder
│ │ │ ├── update.jbuilder
│ │ │ ├── update_state.jbuilder
│ │ │ └── _project.jbuilder
│ │ ├── welcome
│ │ │ └── index.jbuilder
│ │ ├── errors.jbuilder
│ │ └── terms_of_services
│ │ │ └── current.jbuilder
│ ├── user_mailer
│ │ ├── activation_success_email.text.erb
│ │ ├── activation_needed_email.text.erb
│ │ └── reset_password_email.text.erb
│ └── admin
│ │ ├── application
│ │ └── _navigation.html.erb
│ │ └── user_sessions
│ │ └── new.html.erb
├── channels
│ ├── application_cable
│ │ ├── channel.rb
│ │ └── connection.rb
│ └── notifications_channel.rb
├── lib
│ ├── core_extensions
│ │ └── action_controller
│ │ │ └── resource_parameter_missing.rb
│ ├── json_web_token.rb
│ └── flipper_migration.rb
├── mailers
│ ├── application_mailer.rb
│ └── user_mailer.rb
└── dashboards
│ └── terms_of_service_dashboard.rb
├── .rspec
├── docs
├── screenshots
│ └── dashboard.png
├── backend
│ └── index.md
├── frontend
│ ├── index.md
│ └── writing_tests.md
├── api
│ ├── root.md
│ ├── index.md
│ ├── terms_of_service.md
│ └── authorizations.md
├── pull_request_template.md
└── tests.md
├── Procfile
├── bin
├── bundle
├── rake
├── rails
├── spring
├── update
└── setup
├── config
├── initializers
│ ├── core_extensions.rb
│ ├── mime_types.rb
│ ├── application_controller_renderer.rb
│ ├── filter_parameter_logging.rb
│ ├── flipper.rb
│ ├── backtrace_silencers.rb
│ ├── json_param_key_transform.rb
│ ├── wrap_parameters.rb
│ ├── cors.rb
│ ├── inflections.rb
│ └── new_framework_defaults.rb
├── spring.rb
├── boot.rb
├── cable.yml
├── environment.rb
├── locales
│ └── en.yml
├── database.yml
├── application.rb
└── secrets.yml
├── config.ru
├── db
└── migrate
│ ├── 20170529210621_add_abandoned_at_to_task.rb
│ ├── 20170116065712_add_finished_at_to_project.rb
│ ├── 20170116212138_add_stopped_at_to_project.rb
│ ├── 20170113231928_add_description_to_project.rb
│ ├── 20171006170245_add_state_to_project.rb
│ ├── 20170716102855_add_project_references_to_task.rb
│ ├── 20171006180251_rename_project_stopped_at_in_paused_at.rb
│ ├── 20170531195512_add_restarted_count_to_task.rb
│ ├── 20180128172912_add_admin_to_users.rb
│ ├── 20170115112412_add_dates_to_project.rb
│ ├── 20190513154840_add_time_zone_to_users.rb
│ ├── 20161228214904_add_username_to_user.rb
│ ├── 20170114113847_add_index_on_project_name.rb
│ ├── 20171226101352_add_slug_to_project.rb
│ ├── 20180629162317_add_terms_of_service_reference_to_users.rb
│ ├── 20170109212722_create_projects.rb
│ ├── 20171001201530_rename_tasks_restarted_count_in_started_count.rb
│ ├── 20170201210318_create_tasks.rb
│ ├── 20161226223150_sorcery_core.rb
│ ├── 20180617164753_create_feature_registration_feature_flag.rb
│ ├── 20180628165422_create_terms_of_services.rb
│ ├── 20171224225137_update_task_state_on_not_started_projects.rb
│ ├── 20161226223151_sorcery_user_activation.rb
│ ├── 20171007004320_set_project_state.rb
│ ├── 20180901081437_sorcery_reset_password.rb
│ ├── 20170601094145_add_order_to_task.rb
│ ├── 20180205083244_create_flipper_tables.rb
│ └── 20171006212950_add_state_to_task.rb
├── .hound.yml
├── public
└── robots.txt
├── Rakefile
├── .travis.yml
├── Dockerfile.dev
├── .gitignore
├── docker-compose-dev.yml
├── .rubocop.yml
├── Dockerfile
├── LICENSE
├── CONTRIBUTORS.md
├── Makefile
└── Gemfile
/log/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tmp/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/tasks/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.ruby-version:
--------------------------------------------------------------------------------
1 | 2.4
2 |
--------------------------------------------------------------------------------
/client/static/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/factories/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/models/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/src/styles/_grid.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/controllers/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --color
2 | --require spec_helper
3 |
--------------------------------------------------------------------------------
/client/.eslintignore:
--------------------------------------------------------------------------------
1 | build/*.js
2 | config/*.js
3 |
--------------------------------------------------------------------------------
/app/jobs/application_job.rb:
--------------------------------------------------------------------------------
1 | class ApplicationJob < ActiveJob::Base
2 | end
3 |
--------------------------------------------------------------------------------
/app/views/layouts/mailer.text.erb:
--------------------------------------------------------------------------------
1 | <%= yield %>
2 |
3 | Have a wonderful day!
4 |
--------------------------------------------------------------------------------
/client/config/prod.env.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | NODE_ENV: '"production"'
3 | }
4 |
--------------------------------------------------------------------------------
/client/spec/setup.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 |
3 | Vue.config.productionTip = false
4 |
--------------------------------------------------------------------------------
/app/views/api/tasks/show.jbuilder:
--------------------------------------------------------------------------------
1 | json.data @task, partial: 'api/tasks/task', as: :task
2 |
--------------------------------------------------------------------------------
/app/views/api/tasks/update.jbuilder:
--------------------------------------------------------------------------------
1 | json.data @task, partial: 'api/tasks/task', as: :task
2 |
--------------------------------------------------------------------------------
/app/views/api/users/show.jbuilder:
--------------------------------------------------------------------------------
1 | json.data @user, partial: 'api/users/user', as: :user
2 |
--------------------------------------------------------------------------------
/client/src/styles/variables/_index.scss:
--------------------------------------------------------------------------------
1 | @import "colors.json";
2 | @import "dimensions";
3 |
--------------------------------------------------------------------------------
/app/views/api/tasks/update_state.jbuilder:
--------------------------------------------------------------------------------
1 | json.data @task, partial: 'api/tasks/task', as: :task
2 |
--------------------------------------------------------------------------------
/app/views/api/users/tasks/create.jbuilder:
--------------------------------------------------------------------------------
1 | json.data @task, partial: 'api/tasks/task', as: :task
2 |
--------------------------------------------------------------------------------
/client/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | dist/
4 | npm-debug.log
5 | test/unit/coverage
6 |
--------------------------------------------------------------------------------
/app/views/api/projects/show.jbuilder:
--------------------------------------------------------------------------------
1 | json.data @project, partial: 'api/projects/project', as: :project
2 |
--------------------------------------------------------------------------------
/app/views/api/projects/update.jbuilder:
--------------------------------------------------------------------------------
1 | json.data @project, partial: 'api/projects/project', as: :project
2 |
--------------------------------------------------------------------------------
/client/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lessy-community/lessy/HEAD/client/static/favicon.ico
--------------------------------------------------------------------------------
/client/src/styles/variables/_dimensions.scss:
--------------------------------------------------------------------------------
1 | $small-screen-width: 800px;
2 | $medium-screen-width: 1100px;
3 |
--------------------------------------------------------------------------------
/client/static/back-home.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lessy-community/lessy/HEAD/client/static/back-home.png
--------------------------------------------------------------------------------
/client/static/icon-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lessy-community/lessy/HEAD/client/static/icon-256.png
--------------------------------------------------------------------------------
/client/static/logo-text.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lessy-community/lessy/HEAD/client/static/logo-text.png
--------------------------------------------------------------------------------
/spec/support/json_matchers.rb:
--------------------------------------------------------------------------------
1 | JsonMatchers.configure do |config|
2 | config.options[:strict] = true
3 | end
4 |
--------------------------------------------------------------------------------
/app/views/api/projects/update_state.jbuilder:
--------------------------------------------------------------------------------
1 | json.data @project, partial: 'api/projects/project', as: :project
2 |
--------------------------------------------------------------------------------
/app/views/api/users/projects/create.jbuilder:
--------------------------------------------------------------------------------
1 | json.data @project, partial: 'api/projects/project', as: :project
2 |
--------------------------------------------------------------------------------
/docs/screenshots/dashboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lessy-community/lessy/HEAD/docs/screenshots/dashboard.png
--------------------------------------------------------------------------------
/spec/support/factory_bot.rb:
--------------------------------------------------------------------------------
1 | RSpec.configure do |config|
2 | config.include FactoryBot::Syntax::Methods
3 | end
4 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: cd client && npm run dev
2 | api: rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b 0.0.0.0
3 |
--------------------------------------------------------------------------------
/app/models/application_record.rb:
--------------------------------------------------------------------------------
1 | class ApplicationRecord < ActiveRecord::Base
2 | self.abstract_class = true
3 | end
4 |
--------------------------------------------------------------------------------
/client/static/logo-text-inverse.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lessy-community/lessy/HEAD/client/static/logo-text-inverse.png
--------------------------------------------------------------------------------
/client/static/illustrations/colors.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lessy-community/lessy/HEAD/client/static/illustrations/colors.jpg
--------------------------------------------------------------------------------
/client/static/illustrations/grid.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lessy-community/lessy/HEAD/client/static/illustrations/grid.jpg
--------------------------------------------------------------------------------
/client/static/illustrations/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lessy-community/lessy/HEAD/client/static/illustrations/logo.png
--------------------------------------------------------------------------------
/app/channels/application_cable/channel.rb:
--------------------------------------------------------------------------------
1 | module ApplicationCable
2 | class Channel < ActionCable::Channel::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/client/static/illustrations/visuals.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lessy-community/lessy/HEAD/client/static/illustrations/visuals.jpg
--------------------------------------------------------------------------------
/client/static/illustrations/wording.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lessy-community/lessy/HEAD/client/static/illustrations/wording.jpg
--------------------------------------------------------------------------------
/app/views/api/users/create.jbuilder:
--------------------------------------------------------------------------------
1 | json.data @user, partial: 'api/users/user', as: :user
2 | json.meta do
3 | json.token @token
4 | end
5 |
--------------------------------------------------------------------------------
/app/views/api/users/features/index.jbuilder:
--------------------------------------------------------------------------------
1 | json.data @features do |feature|
2 | json.type 'feature'
3 | json.id feature.name
4 | end
5 |
--------------------------------------------------------------------------------
/client/src/api/root.js:
--------------------------------------------------------------------------------
1 | import { get } from './http'
2 |
3 | export default {
4 | listInfo () {
5 | return get('/api')
6 | },
7 | }
8 |
--------------------------------------------------------------------------------
/client/static/illustrations/components.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lessy-community/lessy/HEAD/client/static/illustrations/components.jpg
--------------------------------------------------------------------------------
/client/static/illustrations/typography.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lessy-community/lessy/HEAD/client/static/illustrations/typography.jpg
--------------------------------------------------------------------------------
/client/static/permanentmarker-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lessy-community/lessy/HEAD/client/static/permanentmarker-regular.woff2
--------------------------------------------------------------------------------
/bin/bundle:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
3 | load Gem.bin_path('bundler', 'bundle')
4 |
--------------------------------------------------------------------------------
/config/initializers/core_extensions.rb:
--------------------------------------------------------------------------------
1 | Dir[Rails.root.join('app', 'lib', 'core_extensions', '**', '*.rb')].each do |file|
2 | require file
3 | end
4 |
--------------------------------------------------------------------------------
/config/spring.rb:
--------------------------------------------------------------------------------
1 | %w(
2 | .ruby-version
3 | .rbenv-vars
4 | tmp/restart.txt
5 | tmp/caching-dev.txt
6 | ).each { |path| Spring.watch(path) }
7 |
--------------------------------------------------------------------------------
/app/views/api/users/activations/create.jbuilder:
--------------------------------------------------------------------------------
1 | json.data @user, partial: 'api/users/user', as: :user
2 | json.meta do
3 | json.token @token
4 | end
5 |
--------------------------------------------------------------------------------
/app/views/api/users/authorizations/create.jbuilder:
--------------------------------------------------------------------------------
1 | json.data @user, partial: 'api/users/user', as: :user
2 | json.meta do
3 | json.token @token
4 | end
5 |
--------------------------------------------------------------------------------
/config/boot.rb:
--------------------------------------------------------------------------------
1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
2 |
3 | require 'bundler/setup' # Set up gems listed in the Gemfile.
4 |
--------------------------------------------------------------------------------
/client/src/api/features.js:
--------------------------------------------------------------------------------
1 | import { get } from './http'
2 |
3 | export default {
4 | list () {
5 | return get('/api/users/me/features')
6 | },
7 | }
8 |
--------------------------------------------------------------------------------
/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | require_relative 'config/environment'
4 |
5 | run Rails.application
6 |
--------------------------------------------------------------------------------
/app/channels/notifications_channel.rb:
--------------------------------------------------------------------------------
1 | class NotificationsChannel < ApplicationCable::Channel
2 | def subscribed
3 | stream_for current_user
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/client/static/font-awesome-4.7.0/fonts/FontAwesome.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lessy-community/lessy/HEAD/client/static/font-awesome-4.7.0/fonts/FontAwesome.otf
--------------------------------------------------------------------------------
/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::API
2 |
3 | def client
4 | render file: 'public/index.html'
5 | end
6 |
7 | end
8 |
--------------------------------------------------------------------------------
/app/views/api/users/passwords/create.jbuilder:
--------------------------------------------------------------------------------
1 | json.data @user, partial: 'api/users/user', as: :user
2 | if @token
3 | json.meta do
4 | json.token @token
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/app/views/api/welcome/index.jbuilder:
--------------------------------------------------------------------------------
1 | json.ignore_nil!
2 | json.registration_disabled !Flipper.enabled?(:feature_registration)
3 | json.tos_version TermsOfService.current&.version
4 |
--------------------------------------------------------------------------------
/app/views/user_mailer/activation_success_email.text.erb:
--------------------------------------------------------------------------------
1 | Well done <%= @user.username %>,
2 |
3 | You've enabled your account on <%= root_url %> with success, let's have fun now!
4 |
--------------------------------------------------------------------------------
/client/config/test.env.js:
--------------------------------------------------------------------------------
1 | var merge = require('webpack-merge')
2 | var devEnv = require('./dev.env')
3 |
4 | module.exports = merge(devEnv, {
5 | NODE_ENV: '"testing"'
6 | })
7 |
--------------------------------------------------------------------------------
/client/static/font-awesome-4.7.0/fonts/fontawesome-webfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lessy-community/lessy/HEAD/client/static/font-awesome-4.7.0/fonts/fontawesome-webfont.eot
--------------------------------------------------------------------------------
/client/static/font-awesome-4.7.0/fonts/fontawesome-webfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lessy-community/lessy/HEAD/client/static/font-awesome-4.7.0/fonts/fontawesome-webfont.ttf
--------------------------------------------------------------------------------
/client/static/font-awesome-4.7.0/fonts/fontawesome-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lessy-community/lessy/HEAD/client/static/font-awesome-4.7.0/fonts/fontawesome-webfont.woff
--------------------------------------------------------------------------------
/client/config/dev.env.js:
--------------------------------------------------------------------------------
1 | var merge = require('webpack-merge')
2 | var prodEnv = require('./prod.env')
3 |
4 | module.exports = merge(prodEnv, {
5 | NODE_ENV: '"development"'
6 | })
7 |
--------------------------------------------------------------------------------
/client/static/font-awesome-4.7.0/fonts/fontawesome-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lessy-community/lessy/HEAD/client/static/font-awesome-4.7.0/fonts/fontawesome-webfont.woff2
--------------------------------------------------------------------------------
/client/src/locales/en/index.js:
--------------------------------------------------------------------------------
1 | import messages from './messages'
2 | import formats from './formats'
3 |
4 | export default {
5 | messages,
6 | dateTimeFormats: formats.dateTime,
7 | }
8 |
--------------------------------------------------------------------------------
/client/src/locales/fr/index.js:
--------------------------------------------------------------------------------
1 | import messages from './messages'
2 | import formats from './formats'
3 |
4 | export default {
5 | messages,
6 | dateTimeFormats: formats.dateTime,
7 | }
8 |
--------------------------------------------------------------------------------
/app/views/api/users/accept_tos.jbuilder:
--------------------------------------------------------------------------------
1 | json.data do
2 | json.id @user.id
3 | json.type 'user'
4 | json.attributes do
5 | json.has_accepted_tos @user.accepted_tos?
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/app/views/api/users/update.jbuilder:
--------------------------------------------------------------------------------
1 | json.data do
2 | json.id @user.id
3 | json.type 'user'
4 | json.attributes do
5 | json.extract! @user, :username, :email, :time_zone
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/db/migrate/20170529210621_add_abandoned_at_to_task.rb:
--------------------------------------------------------------------------------
1 | class AddAbandonedAtToTask < ActiveRecord::Migration[5.0]
2 | def change
3 | add_column :tasks, :abandoned_at, :datetime
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/app/views/api/tasks/update_order.jbuilder:
--------------------------------------------------------------------------------
1 | json.data @impacted_tasks do |task|
2 | json.type 'task'
3 | json.id task.id
4 | json.attributes do
5 | json.order task.reload.order
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/client/src/components/Ly/LyColumns/index.js:
--------------------------------------------------------------------------------
1 | import LyColumns from './LyColumns'
2 | import LyColumn from './LyColumn'
3 |
4 | export {
5 | LyColumns,
6 | LyColumn,
7 | }
8 | export default LyColumns
9 |
--------------------------------------------------------------------------------
/config/initializers/mime_types.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new mime types for use in respond_to blocks:
4 | # Mime::Type.register "text/richtext", :rtf
5 |
--------------------------------------------------------------------------------
/db/migrate/20170116065712_add_finished_at_to_project.rb:
--------------------------------------------------------------------------------
1 | class AddFinishedAtToProject < ActiveRecord::Migration[5.0]
2 | def change
3 | add_column :projects, :finished_at, :datetime
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20170116212138_add_stopped_at_to_project.rb:
--------------------------------------------------------------------------------
1 | class AddStoppedAtToProject < ActiveRecord::Migration[5.0]
2 | def change
3 | add_column :projects, :stopped_at, :datetime
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/client/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/client/src/api/terms_of_services.js:
--------------------------------------------------------------------------------
1 | import { get } from './http'
2 |
3 | export default {
4 | getCurrent () {
5 | return get('/api/terms_of_services/current', { authorization: 'none' })
6 | },
7 | }
8 |
--------------------------------------------------------------------------------
/spec/support/api/schemas/tasks/create.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "object",
3 | "required": ["data"],
4 | "properties": {
5 | "data": { "$ref": "task.json" }
6 | },
7 | "additionalProperties": false
8 | }
9 |
--------------------------------------------------------------------------------
/spec/support/api/schemas/tasks/show.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "object",
3 | "required": ["data"],
4 | "properties": {
5 | "data": { "$ref": "task.json" }
6 | },
7 | "additionalProperties": false
8 | }
9 |
--------------------------------------------------------------------------------
/spec/support/api/schemas/tasks/update.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "object",
3 | "required": ["data"],
4 | "properties": {
5 | "data": { "$ref": "task.json" }
6 | },
7 | "additionalProperties": false
8 | }
9 |
--------------------------------------------------------------------------------
/spec/support/api/schemas/users/me.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "object",
3 | "required": ["data"],
4 | "properties": {
5 | "data": { "$ref": "user.json" }
6 | },
7 | "additionalProperties": false
8 | }
9 |
--------------------------------------------------------------------------------
/app/views/api/users/_user.jbuilder:
--------------------------------------------------------------------------------
1 | json.id user.id
2 | json.type 'user'
3 | json.attributes do
4 | json.extract! user, :username, :email, :admin, :time_zone
5 | json.has_accepted_tos user.accepted_tos?
6 | end
7 |
--------------------------------------------------------------------------------
/db/migrate/20170113231928_add_description_to_project.rb:
--------------------------------------------------------------------------------
1 | class AddDescriptionToProject < ActiveRecord::Migration[5.0]
2 | def change
3 | add_column :projects, :description, :text, default: ''
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/spec/support/api/schemas/projects/show.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "object",
3 | "required": ["data"],
4 | "properties": {
5 | "data": { "$ref": "project.json" }
6 | },
7 | "additionalProperties": false
8 | }
9 |
--------------------------------------------------------------------------------
/client/src/components/App/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
10 |
--------------------------------------------------------------------------------
/db/migrate/20171006170245_add_state_to_project.rb:
--------------------------------------------------------------------------------
1 | class AddStateToProject < ActiveRecord::Migration[5.1]
2 | def change
3 | add_column :projects, :state, :string, null: false, default: 'newed'
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/spec/support/api/schemas/projects/create.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "object",
3 | "required": ["data"],
4 | "properties": {
5 | "data": { "$ref": "project.json" }
6 | },
7 | "additionalProperties": false
8 | }
9 |
--------------------------------------------------------------------------------
/spec/support/api/schemas/projects/update.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "object",
3 | "required": ["data"],
4 | "properties": {
5 | "data": { "$ref": "project.json" }
6 | },
7 | "additionalProperties": false
8 | }
9 |
--------------------------------------------------------------------------------
/spec/support/api/schemas/tasks/update_state.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "object",
3 | "required": ["data"],
4 | "properties": {
5 | "data": { "$ref": "task.json" }
6 | },
7 | "additionalProperties": false
8 | }
9 |
--------------------------------------------------------------------------------
/spec/support/api/schemas/projects/update_state.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "object",
3 | "required": ["data"],
4 | "properties": {
5 | "data": { "$ref": "project.json" }
6 | },
7 | "additionalProperties": false
8 | }
9 |
--------------------------------------------------------------------------------
/.hound.yml:
--------------------------------------------------------------------------------
1 | ruby:
2 | config_file: .rubocop.yml
3 | eslint:
4 | enabled: true
5 | config_file: client/.eslintrc
6 | jshint:
7 | enabled: false
8 | scss:
9 | enabled: true
10 | config_file: client/.scss.yml
11 |
--------------------------------------------------------------------------------
/db/migrate/20170716102855_add_project_references_to_task.rb:
--------------------------------------------------------------------------------
1 | class AddProjectReferencesToTask < ActiveRecord::Migration[5.0]
2 | def change
3 | add_reference :tasks, :project, foreign_key: true, null: true
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20171006180251_rename_project_stopped_at_in_paused_at.rb:
--------------------------------------------------------------------------------
1 | class RenameProjectStoppedAtInPausedAt < ActiveRecord::Migration[5.1]
2 | def change
3 | rename_column :projects, :stopped_at, :paused_at
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20170531195512_add_restarted_count_to_task.rb:
--------------------------------------------------------------------------------
1 | class AddRestartedCountToTask < ActiveRecord::Migration[5.0]
2 | def change
3 | add_column :tasks, :restarted_count, :integer, null: false, default: 0
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20180128172912_add_admin_to_users.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddAdminToUsers < ActiveRecord::Migration[5.1]
4 | def change
5 | add_column :users, :admin, :boolean, default: false
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/config/initializers/application_controller_renderer.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # ApplicationController.renderer.defaults.merge!(
4 | # http_host: 'example.org',
5 | # https: false
6 | # )
7 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2 | #
3 | # To ban all spiders from the entire site uncomment the next two lines:
4 | # User-agent: *
5 | # Disallow: /
6 |
--------------------------------------------------------------------------------
/db/migrate/20170115112412_add_dates_to_project.rb:
--------------------------------------------------------------------------------
1 | class AddDatesToProject < ActiveRecord::Migration[5.0]
2 | def change
3 | add_column :projects, :started_at, :datetime
4 | add_column :projects, :due_at, :datetime
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/db/migrate/20190513154840_add_time_zone_to_users.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddTimeZoneToUsers < ActiveRecord::Migration[5.2]
4 | def change
5 | add_column :users, :time_zone, :string, default: 'UTC'
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/db/migrate/20161228214904_add_username_to_user.rb:
--------------------------------------------------------------------------------
1 | class AddUsernameToUser < ActiveRecord::Migration[5.0]
2 | def change
3 | add_column :users, :username, :string, null: true
4 | add_index :users, :username, unique: true
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/db/migrate/20170114113847_add_index_on_project_name.rb:
--------------------------------------------------------------------------------
1 | class AddIndexOnProjectName < ActiveRecord::Migration[5.0]
2 | def change
3 | add_index :projects, 'name'
4 | add_index :projects, ['name', 'user_id'], unique: true
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/app/views/user_mailer/activation_needed_email.text.erb:
--------------------------------------------------------------------------------
1 | Welcome <%= @user.email %>,
2 |
3 | You've just registered on <%= root_url %> but you still need to activate your account by setting a password. Just follow the link:
4 |
5 | <%= @activation_url %>
6 |
--------------------------------------------------------------------------------
/config/initializers/filter_parameter_logging.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Configure sensitive parameters which will be filtered from the log file.
4 | Rails.application.config.filter_parameters += [:password]
5 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # Add your own tasks in files placed in lib/tasks ending in .rake,
2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3 |
4 | require_relative 'config/application'
5 |
6 | Rails.application.load_tasks
7 |
--------------------------------------------------------------------------------
/app/views/api/errors.jbuilder:
--------------------------------------------------------------------------------
1 | json.errors @errors do |error|
2 | json.extract! error, :status, :code, :title, :detail
3 | if error.source_pointer.present?
4 | json.source do
5 | json.pointer error.source_pointer
6 | end
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | begin
3 | load File.expand_path('../spring', __FILE__)
4 | rescue LoadError => e
5 | raise unless e.message.include?('spring')
6 | end
7 | require_relative '../config/boot'
8 | require 'rake'
9 | Rake.application.run
10 |
--------------------------------------------------------------------------------
/db/migrate/20171226101352_add_slug_to_project.rb:
--------------------------------------------------------------------------------
1 | class AddSlugToProject < ActiveRecord::Migration[5.1]
2 | def change
3 | add_column :projects, :slug, :string
4 | Project.update_all 'slug = name'
5 | change_column_null :projects, :slug, false
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/config/cable.yml:
--------------------------------------------------------------------------------
1 | development:
2 | adapter: async
3 |
4 | test:
5 | adapter: test
6 |
7 | production:
8 | adapter: redis
9 | url: redis://<%= ENV.fetch 'REDIS_HOST', 'localhost' %>:<%= ENV.fetch 'REDIS_PORT', 6379 %>/1
10 | channel_prefix: lessy_production
11 |
--------------------------------------------------------------------------------
/app/models/flipper_gate.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class FlipperGate < ApplicationRecord
4 | belongs_to :flipper_feature, foreign_key: :feature_key,
5 | primary_key: :key,
6 | inverse_of: :flipper_gate
7 | end
8 |
--------------------------------------------------------------------------------
/app/views/user_mailer/reset_password_email.text.erb:
--------------------------------------------------------------------------------
1 | Hello <%= @user.username %>,
2 |
3 | Somebody asked to reset your account on <%= root_url %>
4 |
5 | If it was not you, you can safely ignore this email.
6 |
7 | Click the following link to choose a new password: <%= @reset_url %>
8 |
--------------------------------------------------------------------------------
/client/src/utils/object.js:
--------------------------------------------------------------------------------
1 | function objectsToOptions (objects, valueKey, labelKey) {
2 | return objects.map(obj => {
3 | return {
4 | value: obj[valueKey],
5 | label: obj[labelKey],
6 | }
7 | })
8 | }
9 |
10 | export {
11 | objectsToOptions,
12 | }
13 |
--------------------------------------------------------------------------------
/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the Rails application.
2 | require_relative 'application'
3 |
4 | # Initialize the Rails application.
5 | Rails.application.initialize!
6 |
7 | # Make JBuilder lower-cased attributes to be JavaScript-style-compliant
8 | Jbuilder.key_format camelize: :lower
9 |
--------------------------------------------------------------------------------
/config/initializers/flipper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'flipper/adapters/active_record'
4 |
5 | Flipper.configure do |config|
6 | config.default do
7 | adapter = Flipper::Adapters::ActiveRecord.new
8 | Flipper.new(adapter)
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/client/src/components/Ly/LyList/LyListItemAdapt.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
13 |
--------------------------------------------------------------------------------
/db/migrate/20180629162317_add_terms_of_service_reference_to_users.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddTermsOfServiceReferenceToUsers < ActiveRecord::Migration[5.1]
4 | def change
5 | add_reference :users, :terms_of_service, foreign_key: true, null: true
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/spec/support/api/schemas/welcome/index.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "object",
3 | "required": ["registrationDisabled"],
4 | "properties": {
5 | "registrationDisabled": { "type": "boolean" },
6 | "tosVersion": { "type": "string" }
7 | },
8 | "additionalProperties": false
9 | }
10 |
--------------------------------------------------------------------------------
/app/views/api/terms_of_services/current.jbuilder:
--------------------------------------------------------------------------------
1 | json.data do
2 | json.id @terms_of_service.id
3 | json.type 'terms_of_service'
4 | json.attributes do
5 | json.extract! @terms_of_service, :content, :version
6 | json.effective_at @terms_of_service.effective_at.to_i
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/app/controllers/api/users/features_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Api::Users::FeaturesController < ApiController
4 | skip_before_action :require_tos_accepted, only: [:index]
5 |
6 | def index
7 | @features = current_user.features_enabled
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/client/src/components/Ly/LyPopover/LyPopoverSeparator.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
12 |
--------------------------------------------------------------------------------
/client/src/components/Ly/LyPopover/index.js:
--------------------------------------------------------------------------------
1 | import LyPopover from './LyPopover'
2 | import LyPopoverItem from './LyPopoverItem'
3 | import LyPopoverSeparator from './LyPopoverSeparator'
4 |
5 | export {
6 | LyPopover,
7 | LyPopoverItem,
8 | LyPopoverSeparator,
9 | }
10 | export default LyPopover
11 |
--------------------------------------------------------------------------------
/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | begin
3 | load File.expand_path('../spring', __FILE__)
4 | rescue LoadError => e
5 | raise unless e.message.include?('spring')
6 | end
7 | APP_PATH = File.expand_path('../config/application', __dir__)
8 | require_relative '../config/boot'
9 | require 'rails/commands'
10 |
--------------------------------------------------------------------------------
/client/build/vue-loader.conf.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const config = require('../config')
3 |
4 | module.exports = {
5 | cacheBusting: config.dev.cacheBusting,
6 | transformAssetUrls: {
7 | video: ['src', 'poster'],
8 | source: 'src',
9 | img: 'src',
10 | image: 'xlink:href'
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/db/migrate/20170109212722_create_projects.rb:
--------------------------------------------------------------------------------
1 | class CreateProjects < ActiveRecord::Migration[5.0]
2 | def change
3 | create_table :projects do |t|
4 | t.string :name, null: false
5 | t.references :user, foreign_key: true, null: false
6 |
7 | t.timestamps
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/app/lib/core_extensions/action_controller/resource_parameter_missing.rb:
--------------------------------------------------------------------------------
1 | module ActionController
2 | class ResourceParameterMissing < ParameterMissing
3 | attr_reader :resource
4 |
5 | def initialize(resource, param)
6 | @resource = resource
7 | super(param)
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/app/views/layouts/mailer.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
11 | <%= yield %>
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/controllers/api/welcome_controller.rb:
--------------------------------------------------------------------------------
1 | class Api::WelcomeController < ApiController
2 |
3 | skip_before_action :require_login
4 | skip_before_action :require_tos_accepted
5 |
6 | def index
7 | end
8 |
9 | def not_found
10 | render_error ApiErrors::MissingEndpoint.new, :not_found
11 | end
12 |
13 | end
14 |
--------------------------------------------------------------------------------
/app/models/flipper_feature.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class FlipperFeature < ApplicationRecord
4 | has_many :flipper_gates, dependent: :destroy,
5 | foreign_key: :feature_key,
6 | primary_key: :key,
7 | inverse_of: :flipper_feature
8 | end
9 |
--------------------------------------------------------------------------------
/client/src/locales/en/formats.js:
--------------------------------------------------------------------------------
1 | export default {
2 | dateTime: {
3 | abbr: {
4 | month: 'short', day: '2-digit',
5 | },
6 | short: {
7 | year: 'numeric', month: 'short', day: '2-digit',
8 | },
9 | long: {
10 | year: 'numeric', month: 'long', day: '2-digit',
11 | },
12 | },
13 | }
14 |
--------------------------------------------------------------------------------
/client/src/locales/fr/formats.js:
--------------------------------------------------------------------------------
1 | export default {
2 | dateTime: {
3 | abbr: {
4 | month: 'short', day: '2-digit',
5 | },
6 | short: {
7 | year: 'numeric', month: 'short', day: '2-digit',
8 | },
9 | long: {
10 | year: 'numeric', month: 'long', day: '2-digit',
11 | },
12 | },
13 | }
14 |
--------------------------------------------------------------------------------
/app/mailers/application_mailer.rb:
--------------------------------------------------------------------------------
1 | class ApplicationMailer < ActionMailer::Base
2 |
3 | default from: 'noreply@lessy.io'
4 | layout 'mailer'
5 |
6 | protected
7 |
8 | def mail(options={})
9 | options[:subject] = "[Lessy] #{ options[:subject] }" if options.has_key? :subject
10 | super options
11 | end
12 |
13 | end
14 |
--------------------------------------------------------------------------------
/client/src/styles/_links.scss:
--------------------------------------------------------------------------------
1 | a {
2 | color: $ly-color-pine-80;
3 |
4 | &:hover,
5 | &:focus {
6 | color: $ly-color-pine-90;
7 | text-decoration: none;
8 | }
9 | }
10 |
11 | .text-on-dark a {
12 | color: $ly-color-white;
13 |
14 | &:hover,
15 | &:focus {
16 | color: $ly-color-grey-30;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/client/src/components/Ly/LyList/index.js:
--------------------------------------------------------------------------------
1 | import LyList from './LyList'
2 | import LyListGroup from './LyListGroup'
3 | import LyListItem from './LyListItem'
4 | import LyListItemAdapt from './LyListItemAdapt'
5 |
6 | export {
7 | LyList,
8 | LyListGroup,
9 | LyListItem,
10 | LyListItemAdapt,
11 | }
12 | export default LyList
13 |
--------------------------------------------------------------------------------
/client/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "es2015",
4 | "stage-2",
5 | ["env", { "modules": false }]
6 | ],
7 | "plugins": ["transform-runtime"],
8 | "comments": false,
9 | "env": {
10 | "test": {
11 | "presets": [
12 | ["env", { "targets": { "node": "current" }}]
13 | ]
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/db/migrate/20171001201530_rename_tasks_restarted_count_in_started_count.rb:
--------------------------------------------------------------------------------
1 | class RenameTasksRestartedCountInStartedCount < ActiveRecord::Migration[5.1]
2 | class Task < ApplicationRecord
3 | end
4 |
5 | def change
6 | rename_column :tasks, :restarted_count, :started_count
7 | Task.update_all('started_count = started_count + 1')
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/db/migrate/20170201210318_create_tasks.rb:
--------------------------------------------------------------------------------
1 | class CreateTasks < ActiveRecord::Migration[5.0]
2 | def change
3 | create_table :tasks do |t|
4 | t.string :label, null: false
5 | t.datetime :due_at
6 | t.datetime :finished_at
7 | t.references :user, foreign_key: true, null: false
8 |
9 | t.timestamps
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/db/migrate/20161226223150_sorcery_core.rb:
--------------------------------------------------------------------------------
1 | class SorceryCore < ActiveRecord::Migration[4.2]
2 | def change
3 | create_table :users do |t|
4 | t.string :email, :null => false
5 | t.string :crypted_password
6 | t.string :salt
7 |
8 | t.timestamps
9 | end
10 |
11 | add_index :users, :email, unique: true
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/db/migrate/20180617164753_create_feature_registration_feature_flag.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class CreateFeatureRegistrationFeatureFlag < ActiveRecord::Migration[5.1]
4 | include FlipperMigration
5 |
6 | def up
7 | create_flag :feature_registration, enabled: true
8 | end
9 |
10 | def down
11 | destroy_flag :feature_registration
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/client/static/font-awesome-4.7.0/HELP-US-OUT.txt:
--------------------------------------------------------------------------------
1 | I hope you love Font Awesome. If you've found it useful, please do me a favor and check out my latest project,
2 | Fort Awesome (https://fortawesome.com). It makes it easy to put the perfect icons on your website. Choose from our awesome,
3 | comprehensive icon sets or copy and paste your own.
4 |
5 | Please. Check it out.
6 |
7 | -Dave Gandy
8 |
--------------------------------------------------------------------------------
/client/src/components/Ly/LyForm/index.js:
--------------------------------------------------------------------------------
1 | import LyForm from './LyForm'
2 | import LyFormGroup from './LyFormGroup'
3 | import LyFormInput from './LyFormInput'
4 | import LyFormSelect from './LyFormSelect'
5 | import LyFormTextarea from './LyFormTextarea'
6 |
7 | export {
8 | LyForm,
9 | LyFormGroup,
10 | LyFormInput,
11 | LyFormSelect,
12 | LyFormTextarea,
13 | }
14 | export default LyForm
15 |
--------------------------------------------------------------------------------
/db/migrate/20180628165422_create_terms_of_services.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class CreateTermsOfServices < ActiveRecord::Migration[5.1]
4 | def change
5 | create_table :terms_of_services do |t|
6 | t.text :content, null: false
7 | t.string :version, null: false
8 | t.datetime :effective_at, null: false
9 |
10 | t.timestamps
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/db/migrate/20171224225137_update_task_state_on_not_started_projects.rb:
--------------------------------------------------------------------------------
1 | class UpdateTaskStateOnNotStartedProjects < ActiveRecord::Migration[5.1]
2 | def up
3 | Project.not_started.find_each do |project|
4 | project.tasks.started.update_all state: 'newed', started_at: nil
5 | end
6 | end
7 |
8 | def down
9 | Task.newed.update_all "state = 'started', started_at = created_at"
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/client/src/components/projects/ProjectsHeader.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
17 |
--------------------------------------------------------------------------------
/db/migrate/20161226223151_sorcery_user_activation.rb:
--------------------------------------------------------------------------------
1 | class SorceryUserActivation < ActiveRecord::Migration[4.2]
2 | def change
3 | add_column :users, :activation_state, :string, :default => nil
4 | add_column :users, :activation_token, :string, :default => nil
5 | add_column :users, :activation_token_expires_at, :datetime, :default => nil
6 |
7 | add_index :users, :activation_token
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/spec/support/api/schemas/users/create.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "object",
3 | "required": ["data", "meta"],
4 | "properties": {
5 | "data": { "$ref": "user.json" },
6 | "meta": {
7 | "type": "object",
8 | "required": ["token"],
9 | "properties": {
10 | "token": { "type": "string" }
11 | },
12 | "additionalProperties": false
13 | }
14 | },
15 | "additionalProperties": false
16 | }
17 |
--------------------------------------------------------------------------------
/spec/support/api/schemas/users/passwords/create.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "object",
3 | "required": ["data"],
4 | "properties": {
5 | "data": { "$ref": "../user.json" },
6 | "meta": {
7 | "type": "object",
8 | "required": ["token"],
9 | "properties": {
10 | "token": { "type": "string" }
11 | },
12 | "additionalProperties": false
13 | }
14 | },
15 | "additionalProperties": false
16 | }
17 |
--------------------------------------------------------------------------------
/app/controllers/api/terms_of_services_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api
4 | class TermsOfServicesController < ApiController
5 | skip_before_action :require_login, only: [:current]
6 | skip_before_action :require_tos_accepted, only: [:current]
7 |
8 | def current
9 | @terms_of_service = TermsOfService.current
10 | head :no_content if @terms_of_service.nil?
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/spec/factories/terms_of_service.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | factory :terms_of_service do
5 | content { 'ToS content' }
6 | sequence(:version) { |i| "2018-#{i}" }
7 | effective_at { 1.day.ago }
8 |
9 | trait :in_the_past do
10 | effective_at { 1.month.ago }
11 | end
12 |
13 | trait :in_the_future do
14 | effective_at { 1.month.from_now }
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/spec/support/api/schemas/users/activations/create.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "object",
3 | "required": ["data", "meta"],
4 | "properties": {
5 | "data": { "$ref": "../user.json" },
6 | "meta": {
7 | "type": "object",
8 | "required": ["token"],
9 | "properties": {
10 | "token": { "type": "string" }
11 | },
12 | "additionalProperties": false
13 | }
14 | },
15 | "additionalProperties": false
16 | }
17 |
--------------------------------------------------------------------------------
/spec/support/api/schemas/users/authorizations/create.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "object",
3 | "required": ["data", "meta"],
4 | "properties": {
5 | "data": { "$ref": "../user.json" },
6 | "meta": {
7 | "type": "object",
8 | "required": ["token"],
9 | "properties": {
10 | "token": { "type": "string" }
11 | },
12 | "additionalProperties": false
13 | }
14 | },
15 | "additionalProperties": false
16 | }
17 |
--------------------------------------------------------------------------------
/app/models/terms_of_service.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class TermsOfService < ApplicationRecord
4 | has_many :users, dependent: :nullify
5 |
6 | validates :content, :version, :effective_at, presence: true
7 | validates :version, uniqueness: { case_sensitive: false }
8 |
9 | def self.current
10 | TermsOfService
11 | .where('effective_at <= ?', Time.zone.now)
12 | .order(:effective_at)
13 | .last
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/config/initializers/backtrace_silencers.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
5 |
6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
7 | # Rails.backtrace_cleaner.remove_silencers!
8 |
--------------------------------------------------------------------------------
/db/migrate/20171007004320_set_project_state.rb:
--------------------------------------------------------------------------------
1 | class SetProjectState < ActiveRecord::Migration[5.1]
2 | class Project < ApplicationRecord
3 | end
4 |
5 | def up
6 | Project.where.not(finished_at: nil).update_all state: 'finished'
7 | Project.where(finished_at: nil).where.not(paused_at: nil).update_all state: 'paused'
8 | Project.where(finished_at: nil, paused_at: nil).where.not(started_at: nil).update_all state: 'started'
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/spec/shared_examples_for_failures.rb:
--------------------------------------------------------------------------------
1 | RSpec.shared_examples 'API errors' do |http_status, error_body|
2 | it 'fails' do
3 | expect(response).to have_http_status(http_status)
4 | end
5 |
6 | it 'matches errors.json schema' do
7 | expect(response).to match_response_schema('errors')
8 | end
9 |
10 | it 'returns errors' do
11 | body = JSON.parse(response.body, symbolize_names: true)
12 | expect(body).to eq(error_body)
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/client/src/styles/_fonts.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'Permanent Marker';
3 | font-style: normal;
4 | font-weight: 400;
5 | src: local('Permanent Marker Regular'),
6 | local('PermanentMarker-Regular'),
7 | url('/static/permanentmarker-regular.woff2') format('woff2');
8 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
9 | }
10 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 | rvm:
3 | - 2.4
4 |
5 | notifications:
6 | email: false
7 |
8 | cache:
9 | bundler: true
10 | npm: true
11 |
12 | services:
13 | - postgresql
14 |
15 | install:
16 | - bundle install
17 | - nvm install 10
18 | - cd client && npm cache verify && npm install && cd -
19 |
20 | before_script:
21 | - psql -c 'create database lessy_test;' -U postgres
22 |
23 | script:
24 | - bundle exec rspec spec
25 | - cd client && npm run test && cd -
26 |
--------------------------------------------------------------------------------
/app/views/api/users/tasks/index.jbuilder:
--------------------------------------------------------------------------------
1 | json.data @tasks, partial: 'api/tasks/task', as: :task
2 | json.links do
3 | json.first me_tasks_api_users_path(page: 1)
4 | json.last me_tasks_api_users_path(page: @tasks.total_pages)
5 | if !@tasks.first_page? && @tasks.total_pages != 0
6 | json.prev me_tasks_api_users_path(page: @tasks.prev_page)
7 | end
8 | if !@tasks.last_page? && @tasks.total_pages != 0
9 | json.next me_tasks_api_users_path(page: @tasks.next_page)
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/spec/support/api/schemas/features/index.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "object",
3 | "required": ["data"],
4 | "properties": {
5 | "data": {
6 | "type": "array",
7 | "items": {
8 | "type": "object",
9 | "required": ["id", "type"],
10 | "properties": {
11 | "type": { "type": "string" },
12 | "id": { "type": "string" }
13 | },
14 | "additionalProperties": false
15 | }
16 | }
17 | },
18 | "additionalProperties": false
19 | }
20 |
--------------------------------------------------------------------------------
/app/channels/application_cable/connection.rb:
--------------------------------------------------------------------------------
1 | module ApplicationCable
2 | class Connection < ActionCable::Connection::Base
3 | identified_by :current_user
4 |
5 | def connect
6 | self.current_user = find_verified_user
7 | end
8 |
9 | private
10 |
11 | def find_verified_user
12 | if current_user = User.find_by_authorization_token(cookies['Authorization'])
13 | current_user
14 | else
15 | reject_unauthorized_connection
16 | end
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/client/src/components/general/TermsOfServiceModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ $t('general.termsOfServiceModal.intro') }}
5 |
6 |
7 | {{ $t('general.termsOfServiceModal.mustAccept') }}
8 |
9 |
10 |
11 |
12 | {{ $t('general.termsOfServiceModal.read') }}
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/config/initializers/json_param_key_transform.rb:
--------------------------------------------------------------------------------
1 | # From http://stackoverflow.com/a/30557924
2 | # Transform JSON request param keys from JSON-conventional camelCase to
3 | # Rails-conventional snake_case:
4 | ActionDispatch::Request.parameter_parsers[:json] = -> (raw_post) {
5 | # Modified from action_dispatch/http/parameters.rb
6 | data = ActiveSupport::JSON.decode(raw_post)
7 | data = {:_json => data} unless data.is_a?(Hash)
8 |
9 | # Transform camelCase param keys to snake_case:
10 | data.deep_transform_keys!(&:underscore)
11 | }
12 |
--------------------------------------------------------------------------------
/app/views/api/users/projects/index.jbuilder:
--------------------------------------------------------------------------------
1 | json.data @projects, partial: 'api/projects/project', as: :project
2 | json.links do
3 | json.first me_projects_api_users_path(page: 1)
4 | json.last me_projects_api_users_path(page: @projects.total_pages)
5 | if !@projects.first_page? && @projects.total_pages != 0
6 | json.prev me_projects_api_users_path(page: @projects.prev_page)
7 | end
8 | if !@projects.last_page? && @projects.total_pages != 0
9 | json.next me_projects_api_users_path(page: @projects.next_page)
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/app/lib/json_web_token.rb:
--------------------------------------------------------------------------------
1 | class JsonWebToken
2 | def self.encode(data, expiration)
3 | payload = {
4 | data: data,
5 | exp: expiration.to_i,
6 | }
7 | JWT.encode(payload, Rails.application.secrets.secret_key_base, 'HS256')
8 | end
9 |
10 | def self.decode(token)
11 | body = JWT.decode(token, Rails.application.secrets.secret_key_base, 'HS256')[0]
12 | HashWithIndifferentAccess.new body
13 | rescue JWT::DecodeError => e
14 | Rails.logger.warn "JsonWebToken.decode: #{ e.message }"
15 | nil
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/client/src/store/modules/features.js:
--------------------------------------------------------------------------------
1 | import api from '@/api/features'
2 |
3 | const state = {
4 | enabled: [],
5 | }
6 |
7 | const getters = {
8 | }
9 |
10 | const actions = {
11 | list ({ commit }) {
12 | return api.list().then((res) => commit('enableList', res.data))
13 | },
14 | }
15 |
16 | const mutations = {
17 | enableList (state, data) {
18 | state.enabled = data.map((feature) => feature.id)
19 | },
20 | }
21 |
22 | export default {
23 | namespaced: true,
24 | state,
25 | getters,
26 | actions,
27 | mutations,
28 | }
29 |
--------------------------------------------------------------------------------
/db/migrate/20180901081437_sorcery_reset_password.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class SorceryResetPassword < ActiveRecord::Migration[5.2]
4 | def change
5 | add_column :users, :reset_password_token, :string, default: nil
6 | add_column :users, :reset_password_token_expires_at, :datetime, default: nil
7 | add_column :users, :reset_password_email_sent_at, :datetime, default: nil
8 | add_column :users, :access_count_to_reset_password_page, :integer, default: 0
9 |
10 | add_index :users, :reset_password_token
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/client/src/components/tasks/TaskSelectList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
12 |
25 |
--------------------------------------------------------------------------------
/client/src/components/users/UserLoginPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ $t('users.loginPage.title') }}
4 |
5 |
6 |
7 |
8 |
23 |
--------------------------------------------------------------------------------
/bin/spring:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | # This file loads spring without using Bundler, in order to be fast.
4 | # It gets overwritten when you run the `spring binstub` command.
5 |
6 | unless defined?(Spring)
7 | require 'rubygems'
8 | require 'bundler'
9 |
10 | lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read)
11 | if spring = lockfile.specs.detect { |spec| spec.name == "spring" }
12 | Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path
13 | gem 'spring', spring.version
14 | require 'spring/binstub'
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/config/initializers/wrap_parameters.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # This file contains settings for ActionController::ParamsWrapper which
4 | # is enabled by default.
5 |
6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
7 | ActiveSupport.on_load(:action_controller) do
8 | wrap_parameters format: [:json]
9 | end
10 |
11 | # To enable root element in JSON for ActiveRecord objects.
12 | # ActiveSupport.on_load(:active_record) do
13 | # self.include_root_in_json = true
14 | # end
15 |
--------------------------------------------------------------------------------
/app/controllers/api/users/activation_emails_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Api::Users::ActivationEmailsController < ApiController
4 | skip_before_action :require_login, only: [:create]
5 | skip_before_action :require_tos_accepted, only: [:create]
6 |
7 | def create
8 | user = User.find_by!(find_user_params)
9 | UserMailer.activation_needed_email(user).deliver_later if user.inactive?
10 | head :no_content
11 | end
12 |
13 | private
14 |
15 | def find_user_params
16 | fetch_resource_params(:user, [:email])
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/client/src/components/Ly/LyColumns/LyColumn.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
26 |
--------------------------------------------------------------------------------
/config/initializers/cors.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Avoid CORS issues when API is called from the frontend app.
4 | # Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests.
5 |
6 | # Read more: https://github.com/cyu/rack-cors
7 |
8 | # Rails.application.config.middleware.insert_before 0, Rack::Cors do
9 | # allow do
10 | # origins 'example.com'
11 | #
12 | # resource '*',
13 | # headers: :any,
14 | # methods: [:get, :post, :put, :patch, :delete, :options, :head]
15 | # end
16 | # end
17 |
--------------------------------------------------------------------------------
/docs/backend/index.md:
--------------------------------------------------------------------------------
1 | # Backend overview
2 |
3 | Lessy's backend is a very basic Ruby on Rails application, nothing fancy. If
4 | you don't know about Rails, please start by their ["getting started" guide](http://guides.rubyonrails.org/getting_started.html).
5 | It will guide you over all you need to know to start to help us.
6 |
7 | Some points need a specific attention though, there are detailed in the
8 | following documents:
9 |
10 | - [lifecycle and state machine](lifecycle_and_state_machine.md)
11 | - [endpoints' design](endpoints_design.md)
12 | - [writing tests](writing_tests.md)
13 |
--------------------------------------------------------------------------------
/spec/factories/user.rb:
--------------------------------------------------------------------------------
1 | FactoryBot.define do
2 | factory :user do
3 | sequence(:email) { |n| "john+#{ n }@doe.com" }
4 | sequence(:username, 'a') { |n| "john_#{ n }" }
5 |
6 | trait :activated do
7 | after(:create) do |user|
8 | user.activate!
9 | end
10 | end
11 |
12 | trait :inactive do
13 | # nothing on purpose
14 | end
15 |
16 | trait :password_reseted do
17 | after(:create, &:generate_reset_password_token!)
18 | end
19 |
20 | trait :not_accepted_tos do
21 | terms_of_service { nil }
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/app/controllers/api/users/activations_controller.rb:
--------------------------------------------------------------------------------
1 | class Api::Users::ActivationsController < ApiController
2 | skip_before_action :require_login, only: [:create]
3 | skip_before_action :require_tos_accepted, only: [:create]
4 |
5 | def create
6 | @user = User.find_by_sorcery_token!(params[:token], type: :activation)
7 | @user.update! activate_user_params
8 | @user.activate!
9 | @token = @user.token(expiration: 1.month.from_now)
10 | end
11 |
12 | private
13 |
14 | def activate_user_params
15 | fetch_resource_params(:user, [:username, :password])
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/app/views/api/projects/_project.jbuilder:
--------------------------------------------------------------------------------
1 | json.type 'project'
2 | json.id project.id
3 | json.attributes do
4 | json.extract! project, :name, :slug, :description, :state,
5 | :started_at, :due_at, :paused_at, :finished_at,
6 | :created_at, :updated_at
7 | end
8 | json.relationships do
9 | json.user do
10 | json.data do
11 | json.type 'user'
12 | json.id project.user_id
13 | end
14 | end
15 | json.tasks do
16 | json.data project.tasks.not_abandoned do |task|
17 | json.type 'task'
18 | json.id task.id
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/spec/support/api/schemas/tasks/index.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "object",
3 | "required": ["data", "links"],
4 | "properties": {
5 | "data": {
6 | "type": "array",
7 | "items": { "$ref": "task.json" }
8 | },
9 | "links": {
10 | "type": "object",
11 | "required": ["first", "last"],
12 | "properties": {
13 | "first": { "type": "string" },
14 | "last": { "type": "string" },
15 | "prev": { "type": "string" },
16 | "next": { "type": "string" }
17 | },
18 | "additionalProperties": false
19 | }
20 | },
21 | "additionalProperties": false
22 | }
23 |
--------------------------------------------------------------------------------
/spec/support/api/schemas/projects/index.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "object",
3 | "required": ["data", "links"],
4 | "properties": {
5 | "data": {
6 | "type": "array",
7 | "items": { "$ref": "project.json" }
8 | },
9 | "links": {
10 | "type": "object",
11 | "required": ["first", "last"],
12 | "properties": {
13 | "first": { "type": "string" },
14 | "last": { "type": "string" },
15 | "prev": { "type": "string" },
16 | "next": { "type": "string" }
17 | },
18 | "additionalProperties": false
19 | }
20 | },
21 | "additionalProperties": false
22 | }
23 |
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Lessy
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/db/migrate/20170601094145_add_order_to_task.rb:
--------------------------------------------------------------------------------
1 | class AddOrderToTask < ActiveRecord::Migration[5.0]
2 | class Task < ApplicationRecord
3 | end
4 |
5 | def up
6 | add_column :tasks, :order, :integer, null: true
7 |
8 | Task.transaction do
9 | User.all.each do |user|
10 | user.tasks.order(:due_at, :label).each_with_index do |task, index|
11 | task.update! order: (index + 1)
12 | end
13 | end
14 | end
15 |
16 | change_column_null :tasks, :order, null: false
17 | add_index :tasks, :order
18 | end
19 |
20 | def down
21 | remove_column :tasks, :order
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/client/src/components/App/AppPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
21 |
--------------------------------------------------------------------------------
/app/controllers/api/users/password_resets_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Api::Users::PasswordResetsController < ApiController
4 | skip_before_action :require_login, only: [:create]
5 | skip_before_action :require_tos_accepted, only: [:create]
6 |
7 | before_action :set_user
8 | before_action do
9 | require_active_user(@user)
10 | end
11 |
12 | def create
13 | @user.deliver_reset_password_instructions!
14 | head :no_content
15 | end
16 |
17 | private
18 |
19 | def set_user
20 | find_user_params = fetch_resource_params(:user, [:email])
21 | @user = User.find_by!(find_user_params)
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/app/views/api/tasks/_task.jbuilder:
--------------------------------------------------------------------------------
1 | json.type 'task'
2 | json.id task.id
3 | json.attributes do
4 | json.extract! task, :label, :order, :planned_count, :state,
5 | :started_at, :planned_at, :finished_at, :abandoned_at,
6 | :created_at, :updated_at
7 | end
8 | json.relationships do
9 | json.user do
10 | json.data do
11 | json.type 'user'
12 | json.id task.user_id
13 | end
14 | end
15 | json.project do
16 | json.data do
17 | if task.project.nil?
18 | json.nil!
19 | else
20 | json.type 'project'
21 | json.id task.project_id
22 | end
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/client/src/components/Ly/LyTextContainer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
14 |
15 |
33 |
--------------------------------------------------------------------------------
/app/lib/flipper_migration.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module FlipperMigration
4 | class FlipperFeature < ApplicationRecord
5 | has_many :flipper_gates, dependent: :destroy,
6 | foreign_key: :feature_key,
7 | primary_key: :key
8 | end
9 |
10 | class FlipperGate < ApplicationRecord
11 | end
12 |
13 | def create_flag(flag_key, enabled: false)
14 | if enabled
15 | Flipper.enable flag_key
16 | else
17 | Flipper.disable flag_key
18 | end
19 | end
20 |
21 | def destroy_flag(flag_key)
22 | FlipperFeature.find_by!(key: flag_key)&.destroy
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/app/mailers/user_mailer.rb:
--------------------------------------------------------------------------------
1 | class UserMailer < ApplicationMailer
2 | def activation_needed_email(user)
3 | @user = user
4 | @activation_url = "#{ root_url }users/#{ user.activation_token }/activate"
5 | mail to: user.email,
6 | subject: 'Welcome to Lessy!'
7 | end
8 |
9 | def activation_success_email(user)
10 | @user = user
11 | mail to: user.email,
12 | subject: 'Your account is now activated'
13 | end
14 |
15 | def reset_password_email(user)
16 | @user = user
17 | @reset_url = "#{root_url}password/#{user.reset_password_token}/new"
18 | mail to: user.email,
19 | subject: 'Lessy password reset'
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/client/src/components/layouts/LayoutEmpty.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
18 |
19 |
27 |
--------------------------------------------------------------------------------
/client/src/components/Ly/LyIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
17 |
18 |
31 |
--------------------------------------------------------------------------------
/app/controllers/api/users/authorizations_controller.rb:
--------------------------------------------------------------------------------
1 | class Api::Users::AuthorizationsController < ApiController
2 | skip_before_action :require_login, only: [:create]
3 | skip_before_action :require_tos_accepted, only: [:create]
4 |
5 | def create
6 | @user = User.authenticate(params[:username], params[:password])
7 | unless @user
8 | render_error ApiErrors::LoginFailed.new, :unauthorized
9 | return
10 | end
11 | sudo_mode = params[:sudo] ? true : false
12 | token_duration_validity = sudo_mode ? 15.minutes : 1.month
13 | @token = @user.token(
14 | expiration: token_duration_validity.from_now,
15 | sudo: sudo_mode,
16 | )
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/Dockerfile.dev:
--------------------------------------------------------------------------------
1 | FROM ruby:2.4-alpine
2 | MAINTAINER Marien Fressinaud
3 |
4 | EXPOSE 3000 5000
5 |
6 | WORKDIR /app/
7 |
8 | RUN apk add --no-cache \
9 | nodejs \
10 | nodejs-npm \
11 | postgresql-client \
12 | tzdata
13 | RUN apk --update add --virtual build-dependencies \
14 | build-base \
15 | ruby-dev \
16 | postgresql-dev \
17 | libc-dev \
18 | linux-headers \
19 | cmake \
20 | gmp-dev
21 |
22 | RUN gem install bundler
23 |
24 | COPY Gemfile Gemfile.lock /app/
25 | COPY client/package.json client/package-lock.json /app/client/
26 |
27 | VOLUME /app
28 |
29 | CMD ["bundle", "exec", "foreman", "start"]
30 |
--------------------------------------------------------------------------------
/client/src/components/projects/ProjectsStartNewModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
11 |
12 |
13 |
14 |
29 |
--------------------------------------------------------------------------------
/spec/support/api/schemas/users/accept_tos.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "object",
3 | "required": ["data"],
4 | "properties": {
5 | "data": {
6 | "type": "object",
7 | "required": ["id", "type", "attributes"],
8 | "properties": {
9 | "id": { "type": "integer" },
10 | "type": { "type": "string" },
11 | "attributes": {
12 | "type": "object",
13 | "required": ["hasAcceptedTos"],
14 | "properties": {
15 | "hasAcceptedTos": { "type": "boolean" }
16 | },
17 | "additionalProperties": false
18 | }
19 | },
20 | "additionalProperties": false
21 | }
22 | },
23 | "additionalProperties": false
24 | }
25 |
--------------------------------------------------------------------------------
/client/src/components/projects/ProjectStartModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 | {{ project.name }}
8 |
9 |
10 |
15 |
16 |
17 |
18 |
31 |
--------------------------------------------------------------------------------
/client/src/auth.js:
--------------------------------------------------------------------------------
1 | export default {
2 | login (token) {
3 | window.localStorage.setItem('authentication_token', token)
4 | },
5 |
6 | isLoggedIn () {
7 | return this.getToken('normal') != null
8 | },
9 |
10 | sudo (token) {
11 | window.localStorage.setItem('sudo_token', token)
12 | },
13 |
14 | isSudo () {
15 | return this.getToken('sudo') != null
16 | },
17 |
18 | logout () {
19 | window.localStorage.removeItem('authentication_token')
20 | window.localStorage.removeItem('sudo_token')
21 | },
22 |
23 | getToken (mode = 'normal') {
24 | const itemName = mode === 'normal' ? 'authentication_token' : 'sudo_token'
25 | return window.localStorage.getItem(itemName)
26 | },
27 | }
28 |
--------------------------------------------------------------------------------
/app/controllers/api/users/projects_controller.rb:
--------------------------------------------------------------------------------
1 | class Api::Users::ProjectsController < ApiController
2 | skip_before_action :require_tos_accepted, only: [:index]
3 |
4 | def index
5 | @projects = current_user
6 | .projects
7 | .order(:id)
8 | .page(params[:page])
9 | end
10 |
11 | def create
12 | @project = Project.create!(create_project_params)
13 |
14 | NotificationsChannel.broadcast_to(
15 | current_user,
16 | action: 'create#projects',
17 | id: @project.id,
18 | )
19 |
20 | render status: :created
21 | end
22 |
23 | private
24 |
25 | def create_project_params
26 | fetch_resource_params(:project, [:name]).merge(user: current_user)
27 | end
28 |
29 | end
30 |
--------------------------------------------------------------------------------
/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new inflection rules using the following format. Inflections
4 | # are locale specific, and you may define rules for as many different
5 | # locales as you wish. All of these examples are active by default:
6 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
7 | # inflect.plural /^(ox)$/i, '\1en'
8 | # inflect.singular /^(ox)en/i, '\1'
9 | # inflect.irregular 'person', 'people'
10 | # inflect.uncountable %w( fish sheep )
11 | # end
12 |
13 | # These inflection rules are supported but not enabled by default:
14 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
15 | # inflect.acronym 'RESTful'
16 | # end
17 |
--------------------------------------------------------------------------------
/app/controllers/admin/users_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Admin
4 | class UsersController < Admin::ApplicationController
5 | # To customize the behavior of this controller,
6 | # you can overwrite any of the RESTful actions. For example:
7 | #
8 | # def index
9 | # super
10 | # @resources = User.
11 | # page(params[:page]).
12 | # per(10)
13 | # end
14 |
15 | # Define a custom finder by overriding the `find_resource` method:
16 | # def find_resource(param)
17 | # User.find_by!(slug: param)
18 | # end
19 |
20 | # See https://administrate-prototype.herokuapp.com/customizing_controller_actions
21 | # for more information
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Files in the config/locales directory are used for internationalization
2 | # and are automatically loaded by Rails. If you want to use locales other
3 | # than English, add the necessary files in this directory.
4 | #
5 | # To use the locales, use `I18n.t`:
6 | #
7 | # I18n.t 'hello'
8 | #
9 | # In views, this is aliased to just `t`:
10 | #
11 | # <%= t('hello') %>
12 | #
13 | # To use a different locale, set it with `I18n.locale`:
14 | #
15 | # I18n.locale = :es
16 | #
17 | # This would use the information in config/locales/es.yml.
18 | #
19 | # To learn more, please read the Rails Internationalization guide
20 | # available at http://guides.rubyonrails.org/i18n.html.
21 |
22 | en:
23 | hello: "Hello world"
24 |
--------------------------------------------------------------------------------
/client/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex from 'vuex'
3 |
4 | import global from './modules/global'
5 | import users from './modules/users'
6 | import features from './modules/features'
7 | import projects from './modules/projects'
8 | import tasks from './modules/tasks'
9 | import termsOfServices from './modules/terms_of_services'
10 |
11 | import cablePlugin from './plugins/cable'
12 |
13 | Vue.use(Vuex)
14 |
15 | const debug = process.env.NODE_ENV !== 'production'
16 |
17 | export default new Vuex.Store({
18 | modules: {
19 | global,
20 | users,
21 | features,
22 | projects,
23 | tasks,
24 | termsOfServices,
25 | },
26 | plugins: [
27 | cablePlugin,
28 | ],
29 | strict: debug,
30 | })
31 |
--------------------------------------------------------------------------------
/client/spec/jest.conf.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 |
3 | module.exports = {
4 | rootDir: path.resolve(__dirname, '../'),
5 | moduleFileExtensions: [
6 | 'js',
7 | 'json',
8 | 'vue'
9 | ],
10 | moduleNameMapper: {
11 | '^@/(.*)$': '/src/$1'
12 | },
13 | transform: {
14 | '^.+\\.js$': '/node_modules/babel-jest',
15 | '.*\\.(vue)$': '/node_modules/vue-jest'
16 | },
17 | snapshotSerializers: ['/node_modules/jest-serializer-vue'],
18 | setupFiles: ['/spec/setup'],
19 | coverageDirectory: '/spec/coverage',
20 | collectCoverageFrom: [
21 | 'src/**/*.{js,vue}',
22 | '!src/main.js',
23 | '!src/router.js',
24 | '!**/node_modules/**'
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/docs/frontend/index.md:
--------------------------------------------------------------------------------
1 | # Frontend overview
2 |
3 | Lessy's frontend is written with VueJS and is built with NPM and Webpack. You
4 | can find the code under the `client/` directory. Please refer to the [VueJS
5 | guide](https://vuejs.org/v2/guide/) to get started with it.
6 |
7 | Our design guide is accessible at [lessy.io/design](https://lessy.io/design),
8 | please always refer to it when working on the frontend.
9 |
10 | We describe general guidelines to test the frontend in [a specific
11 | document](writing_tests.md).
12 |
13 | This documentation is still under construction. Later, we will document:
14 |
15 | - what is and how to write generic components
16 | - how to organize components
17 | - how to write good store's module
18 | - localisation
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files.
2 | #
3 | # If you find yourself ignoring temporary files generated by your text editor
4 | # or operating system, you probably want to add a global ignore instead:
5 | # git config --global core.excludesfile '~/.gitignore_global'
6 |
7 | # Ignore bundler config.
8 | /.bundle
9 |
10 | # Ignore the default SQLite database.
11 | /db/*.sqlite3
12 | /db/*.sqlite3-journal
13 |
14 | # Ignore all logfiles and tempfiles.
15 | /log/*
16 | /tmp/*
17 | !/log/.keep
18 | !/tmp/.keep
19 |
20 | # Ignore Byebug command history file.
21 | .byebug_history
22 |
23 | # Ignore public/ folder
24 | public/index.html
25 | public/static
26 |
27 | /vendor
28 |
29 | client/spec/coverage/
30 |
--------------------------------------------------------------------------------
/client/src/components/tasks/TaskAttachProjectModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 | « {{ task.label }} »
7 |
8 |
14 |
15 |
16 |
17 |
30 |
--------------------------------------------------------------------------------
/client/src/utils/array.js:
--------------------------------------------------------------------------------
1 | function mapElementsById (elements, fk = 'id') {
2 | let byIds = {}
3 | elements.forEach((element) => {
4 | const id = element[fk]
5 | byIds[id] = element
6 | })
7 | return byIds
8 | }
9 |
10 | function groupByFirstCharacter (array, attribute = null) {
11 | let groups = {}
12 |
13 | array.forEach(element => {
14 | const firstCharacter = attribute == null ? element[0] : element[attribute][0]
15 | const group = firstCharacter.match(/[a-z]/i) ? firstCharacter.toUpperCase() : '#'
16 | if (groups[group] == null) {
17 | groups[group] = []
18 | }
19 | groups[group].push(element)
20 | })
21 |
22 | return groups
23 | }
24 |
25 | export {
26 | mapElementsById,
27 | groupByFirstCharacter,
28 | }
29 |
--------------------------------------------------------------------------------
/config/database.yml:
--------------------------------------------------------------------------------
1 | default: &default
2 | adapter: postgresql
3 | encoding: unicode
4 | pool: 5
5 | username: <%= ENV.fetch 'DATABASE_USERNAME', 'postgres' %>
6 | password: <%= ENV.fetch 'DATABASE_PASSWORD', 'postgres' %>
7 | host: <%= ENV.fetch 'DATABASE_HOST', 'localhost' %>
8 | post: <%= ENV.fetch 'DATABASE_PORT', 5432 %>
9 |
10 | development:
11 | <<: *default
12 | database: lessy_development
13 |
14 | # Warning: The database defined as "test" will be erased and
15 | # re-generated from your development database when you run "rake".
16 | # Do not set this db to the same as development or production.
17 | test:
18 | <<: *default
19 | database: lessy_test
20 |
21 | production:
22 | <<: *default
23 | database: <%= ENV['DATABASE_NAME'] %>
24 |
--------------------------------------------------------------------------------
/config/initializers/new_framework_defaults.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 | #
3 | # This file contains migration options to ease your Rails 5.0 upgrade.
4 | #
5 | # Read the Rails 5.0 release notes for more info on each option.
6 |
7 | # Make Ruby 2.4 preserve the timezone of the receiver when calling `to_time`.
8 | # Previous versions had false.
9 | ActiveSupport.to_time_preserves_timezone = true
10 |
11 | # Require `belongs_to` associations by default. Previous versions had false.
12 | Rails.application.config.active_record.belongs_to_required_by_default = true
13 |
14 | # Configure SSL options to enable HSTS with subdomains. Previous versions had false.
15 | Rails.application.config.ssl_options = { hsts: { subdomains: true } }
16 |
--------------------------------------------------------------------------------
/app/controllers/admin/application_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # All Administrate controllers inherit from this `Admin::ApplicationController`,
4 | # making it the ideal place to put authentication logic or other
5 | # before_actions.
6 | #
7 | # If you want to add pagination or other controller-level concerns,
8 | # you're free to overwrite the RESTful controller actions.
9 | module Admin
10 | class ApplicationController < Administrate::ApplicationController
11 | before_action :authenticate_admin
12 |
13 | def authenticate_admin
14 | redirect_to :admin_login unless admin_signed_in?
15 | end
16 |
17 | private
18 |
19 | def admin_signed_in?
20 | logged_in? && current_user.admin?
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/client/src/components/projects/ProjectEditDueDateModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 | {{ project.name }}
8 |
9 |
10 |
15 |
16 |
17 |
18 |
31 |
--------------------------------------------------------------------------------
/app/controllers/admin/terms_of_services_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Admin
4 | class TermsOfServicesController < Admin::ApplicationController
5 | # To customize the behavior of this controller,
6 | # you can overwrite any of the RESTful actions. For example:
7 | #
8 | # def index
9 | # super
10 | # @resources = TermsOfService.
11 | # page(params[:page]).
12 | # per(10)
13 | # end
14 |
15 | # Define a custom finder by overriding the `find_resource` method:
16 | # def find_resource(param)
17 | # TermsOfService.find_by!(slug: param)
18 | # end
19 |
20 | # See https://administrate-prototype.herokuapp.com/customizing_controller_actions
21 | # for more information
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/client/src/components/projects/ProjectFinishModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 | {{ $t('projects.modals.finishIntro', { projectName: project.name }) }}
8 |
9 |
10 |
15 |
16 |
17 |
18 |
31 |
--------------------------------------------------------------------------------
/docs/api/root.md:
--------------------------------------------------------------------------------
1 | # Root (API)
2 |
3 | ## `GET /api`
4 |
5 | Return information about the server.
6 |
7 | **This endpoint doesn't require an `Authorization` header.**
8 |
9 | Parameters: none.
10 |
11 | Result format:
12 |
13 | | Name | Type | Description | Optional |
14 | |----------------------|--------|-------------------------------------------|----------|
15 | | registrationDisabled | bool | Either if registrations are closed or not | |
16 | | tosVersion | string | Version of current terms of service | yes |
17 |
18 | Example:
19 |
20 | ```console
21 | $ curl https://lessy.io/api
22 | ```
23 |
24 | ```json
25 | {
26 | "registrationDisabled": true,
27 | "tosVersion": "2018-06"
28 | }
29 | ```
30 |
--------------------------------------------------------------------------------
/spec/support/api/schemas/tasks/update_order.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "object",
3 | "required": ["data"],
4 | "properties": {
5 | "data": {
6 | "type": "array",
7 | "items": {
8 | "type": "object",
9 | "required": ["type", "id", "attributes"],
10 | "properties": {
11 | "type": { "type": "string" },
12 | "id": { "type": "integer" },
13 | "attributes": {
14 | "type": "object",
15 | "required": ["order"],
16 | "properties": {
17 | "order": { "type": "integer" }
18 | },
19 | "additionalProperties": false
20 | }
21 | },
22 | "additionalProperties": false
23 | }
24 | }
25 | },
26 | "additionalProperties": false
27 | }
28 |
--------------------------------------------------------------------------------
/client/spec/components/Ly/LyButton.spec.js:
--------------------------------------------------------------------------------
1 | import { mount } from '@vue/test-utils'
2 | import LyCard from '@/components/Ly/LyCard'
3 |
4 | describe('LyCard', () => {
5 | describe('name class', () => {
6 | test('is set to the given prop', () => {
7 | const wrapper = mount(LyCard)
8 |
9 | wrapper.setProps({ name: 'hello' })
10 |
11 | expect(wrapper.classes()).toContain('ly-card-hello')
12 | })
13 | })
14 |
15 | describe('image prop', () => {
16 | test('a background image is set', () => {
17 | const wrapper = mount(LyCard)
18 |
19 | wrapper.setProps({ image: '/static/back.jpg' })
20 |
21 | const renderedCard = wrapper.html()
22 | expect(renderedCard).toMatch(/background-image: url\(\/static\/back.jpg\)/)
23 | })
24 | })
25 | })
26 |
--------------------------------------------------------------------------------
/client/src/store/modules/terms_of_services.js:
--------------------------------------------------------------------------------
1 | import termsOfServiceApi from '@/api/terms_of_services'
2 |
3 | const state = {
4 | current: null,
5 | }
6 |
7 | const getters = {
8 | current (state) {
9 | return state.current
10 | },
11 | }
12 |
13 | const actions = {
14 | getCurrent ({ commit }) {
15 | return termsOfServiceApi.getCurrent()
16 | .then((res) => {
17 | if (res.data) {
18 | commit('setCurrent', res.data)
19 | }
20 | })
21 | },
22 | }
23 |
24 | const mutations = {
25 | setCurrent (state, data) {
26 | state.current = {
27 | id: data.id,
28 | ...data.attributes,
29 | }
30 | },
31 | }
32 |
33 | export default {
34 | namespaced: true,
35 | state,
36 | getters,
37 | actions,
38 | mutations,
39 | }
40 |
--------------------------------------------------------------------------------
/db/migrate/20180205083244_create_flipper_tables.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class CreateFlipperTables < ActiveRecord::Migration[5.1]
4 | def self.up # rubocop:disable Metrics/MethodLength
5 | create_table :flipper_features do |t|
6 | t.string :key, null: false
7 | t.timestamps null: false
8 | end
9 | add_index :flipper_features, :key, unique: true
10 |
11 | create_table :flipper_gates do |t|
12 | t.string :feature_key, null: false
13 | t.string :key, null: false
14 | t.string :value
15 | t.timestamps null: false
16 | end
17 | add_index :flipper_gates, %i[feature_key key value], unique: true
18 | end
19 |
20 | def self.down
21 | drop_table :flipper_gates
22 | drop_table :flipper_features
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/client/src/components/layouts/LayoutDesign.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Grid
5 | Typography
6 | Colors
7 | Visuals
8 | Components
9 | Wording
10 | Logo
11 | Back to home
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/docs/pull_request_template.md:
--------------------------------------------------------------------------------
1 | Closes #
2 |
3 | Changes proposed in this pull request:
4 |
5 | -
6 | -
7 | -
8 |
9 | Pull request checklist:
10 |
11 | - [ ] branch is rebased on `master` \*
12 | - [ ] proper commit messages \*
13 | - [ ] proper coding style \*
14 | - [ ] code is properly tested
15 | - [ ] document API changes both in [technical documentation](https://github.com/lessy-community/lessy/tree/master/docs/api) and [changelog](https://github.com/lessy-community/lessy/blob/master/CHANGELOG.md) (optional)
16 | - [ ] [document migration notes](https://github.com/lessy-community/lessy/blob/master/CHANGELOG.md) (optional)
17 | - [ ] reviewer assigned (@marienfressinaud)
18 |
19 | \* [Additional information in the documentation](https://github.com/lessy-community/lessy/tree/master/docs/pull_request.md).
20 |
--------------------------------------------------------------------------------
/spec/support/api/schemas/users/user.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "object",
3 | "required": ["id", "type", "attributes"],
4 | "properties": {
5 | "id": { "type": "integer" },
6 | "type": { "type": "string" },
7 | "attributes": {
8 | "type": "object",
9 | "required": ["email", "hasAcceptedTos", "timeZone"],
10 | "properties": {
11 | "email": { "type": "string" },
12 | "username": {
13 | "oneOf": [
14 | { "type": "string" },
15 | { "type": "null" }
16 | ]
17 | },
18 | "admin": { "type": "boolean" },
19 | "hasAcceptedTos": { "type": "boolean" },
20 | "timeZone": { "type": "string" }
21 | },
22 | "additionalProperties": false
23 | }
24 | },
25 | "additionalProperties": false
26 | }
27 |
--------------------------------------------------------------------------------
/spec/support/api/schemas/users/update.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "object",
3 | "required": ["data"],
4 | "properties": {
5 | "data": {
6 | "type": "object",
7 | "required": ["id", "type", "attributes"],
8 | "properties": {
9 | "id": { "type": "integer" },
10 | "type": { "type": "string" },
11 | "attributes": {
12 | "type": "object",
13 | "required": ["email", "username", "timeZone"],
14 | "properties": {
15 | "email": { "type": "string" },
16 | "username": { "type": "string" },
17 | "timeZone": { "type": "string" }
18 | },
19 | "additionalProperties": false
20 | }
21 | },
22 | "additionalProperties": false
23 | }
24 | },
25 | "additionalProperties": false
26 | }
27 |
--------------------------------------------------------------------------------
/client/src/components/users/UserPasswordNewPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ $t('users.passwordNewPage.title') }}
4 | {{ $t('users.passwordNewPage.intro') }}
5 |
9 |
10 |
11 |
12 |
33 |
--------------------------------------------------------------------------------
/db/migrate/20171006212950_add_state_to_task.rb:
--------------------------------------------------------------------------------
1 | class AddStateToTask < ActiveRecord::Migration[5.1]
2 | class Task < ApplicationRecord
3 | end
4 |
5 | def up
6 | add_column :tasks, :state, :string, null: false, default: 'newed'
7 | add_column :tasks, :started_at, :datetime
8 | rename_column :tasks, :due_at, :planned_at
9 | rename_column :tasks, :started_count, :planned_count
10 |
11 | Task.update_all "started_at = created_at"
12 | Task.where(planned_at: nil).update_all state: 'started'
13 | Task.where.not(planned_at: nil).update_all state: 'planned'
14 | end
15 |
16 | def down
17 | rename_column :tasks, :planned_count, :started_count
18 | rename_column :tasks, :planned_at, :due_at
19 | remove_column :tasks, :started_at
20 | remove_column :tasks, :state
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/client/src/components/users/UserActivatePage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ $t('users.activatePage.title') }}
4 | {{ $t('users.activatePage.intro') }}
5 |
9 |
10 |
11 |
12 |
33 |
--------------------------------------------------------------------------------
/spec/support/api/schemas/terms_of_services/current.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "object",
3 | "required": ["data"],
4 | "properties": {
5 | "data": {
6 | "type": "object",
7 | "required": ["id", "type", "attributes"],
8 | "properties": {
9 | "id": { "type": "integer" },
10 | "type": { "type": "string" },
11 | "attributes": {
12 | "type": "object",
13 | "required": ["content", "version", "effectiveAt"],
14 | "properties": {
15 | "content": { "type": "string" },
16 | "version": { "type": "string" },
17 | "effectiveAt": { "type": "integer" }
18 | },
19 | "additionalProperties": false
20 | }
21 | },
22 | "additionalProperties": false
23 | }
24 | },
25 | "additionalProperties": false
26 | }
27 |
--------------------------------------------------------------------------------
/client/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | es6: true
4 | },
5 | globals: {
6 | "require": "readonly",
7 | },
8 | root: true,
9 | parser: 'babel-eslint',
10 | parserOptions: {
11 | ecmaVersion: 6,
12 | sourceType: 'module'
13 | },
14 | // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
15 | extends: 'standard',
16 | // required to lint *.vue files
17 | plugins: [
18 | 'html'
19 | ],
20 | // add your custom rules here
21 | 'rules': {
22 | // allow paren-less arrow functions
23 | 'arrow-parens': 0,
24 | // allow async-await
25 | 'generator-star-spacing': 0,
26 | // allow debugger during development
27 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
28 | // allow trailing commas
29 | 'comma-dangle': 0,
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/client/src/components/Ly/LyCardDeck.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
14 |
15 |
42 |
--------------------------------------------------------------------------------
/client/.scss.yml:
--------------------------------------------------------------------------------
1 | scss_files: 'src/**/*.scss'
2 |
3 | linters:
4 | LeadingZero:
5 | style: exclude_zero
6 |
7 | PropertySortOrder:
8 | order:
9 | - position
10 | - top
11 | - right
12 | - bottom
13 | - left
14 | - z-index
15 | -
16 | - display
17 | - float
18 | - width
19 | - height
20 | - margin
21 | - margin-top
22 | - margin-bottom
23 | - margin-left
24 | - margin-right
25 | - padding
26 | - padding-top
27 | - padding-bottom
28 | - padding-left
29 | - padding-right
30 | -
31 | - font
32 | - line-height
33 | - color
34 | - text-align
35 | -
36 | - background
37 | - border
38 | - border-radius
39 | - box-shadow
40 | -
41 | - opacity
42 | separate_groups: true
43 |
--------------------------------------------------------------------------------
/docker-compose-dev.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 | lessy:
5 | image: lessy:dev
6 | restart: unless-stopped
7 | build:
8 | context: .
9 | dockerfile: Dockerfile.dev
10 | depends_on:
11 | - db
12 | links:
13 | - db:db
14 | ports:
15 | - "3000:3000"
16 | - "5000:5000"
17 | volumes:
18 | - .:/app:z
19 | - bundle:/usr/local/bundle
20 | - node_modules:/app/client/node_modules
21 | environment:
22 | DATABASE_USERNAME: postgres
23 | DATABASE_PASSWORD: postgres
24 | DATABASE_PORT: 5432
25 | DATABASE_HOST: db
26 |
27 | db:
28 | image: postgres
29 | restart: unless-stopped
30 | environment:
31 | POSTGRES_USER: postgres
32 | POSTGRES_PASSWORD: postgres
33 | ports:
34 | - "5432:5432"
35 |
36 | volumes:
37 | bundle: {}
38 | node_modules: {}
39 |
--------------------------------------------------------------------------------
/spec/support/api/schemas/errors.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "object",
3 | "required": ["errors"],
4 | "properties": {
5 | "errors": {
6 | "type": "array",
7 | "items": {
8 | "type": "object",
9 | "required": ["status", "code", "title", "detail"],
10 | "properties": {
11 | "status": { "type": "string" },
12 | "code": { "type": "string" },
13 | "title": { "type": "string" },
14 | "detail": { "type": "string" },
15 | "source": {
16 | "type": "object",
17 | "required": ["pointer"],
18 | "properties": {
19 | "pointer": { "type": "string" }
20 | },
21 | "additionalProperties": false
22 | }
23 | },
24 | "additionalProperties": false
25 | }
26 | }
27 | },
28 | "additionalProperties": false
29 | }
30 |
--------------------------------------------------------------------------------
/app/views/admin/application/_navigation.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <% if Rails.env.development? %>
3 | <%= link_to 'Return to Lessy', 'http://localhost:5000/', class: "navigation__link" %>
4 | <% else %>
5 | <%= link_to 'Return to Lessy', '/', class: "navigation__link" %>
6 | <% end %>
7 |
8 | <% if current_user && current_user.admin? %>
9 | <%= link_to 'Logout from admin', admin_logout_path, method: :post, class: "navigation__link" %>
10 |
11 |
12 |
13 | <% Administrate::Namespace.new(namespace).resources.select { |r| r.name != :user_sessions }.each do |resource| %>
14 | <%= link_to(
15 | display_resource_name(resource),
16 | [namespace, resource.path],
17 | class: "navigation__link navigation__link--#{nav_link_state(resource)}"
18 | ) %>
19 | <% end %>
20 | <% end %>
21 |
22 |
--------------------------------------------------------------------------------
/docs/api/index.md:
--------------------------------------------------------------------------------
1 | # API overview
2 |
3 | This document describes ressources accessible through Lessy API. It is still
4 | under heavy development, so if you have questions, please open a ticket on [our
5 | bugtracker](https://github.com/lessy-community/lessy/issues/).
6 |
7 | This API only works with JSON and requests SHOULD use HTTPS only.
8 |
9 | Please note all examples are based on lessy.io official service but you MUST
10 | assume that users are not necessarily hosted on this server.
11 |
12 | - [Root API](root.md)
13 | - [Authorizations](authorizations.md)
14 | - [Users](users.md)
15 | - [Projects](projects.md)
16 | - [Tasks](tasks.md)
17 | - [Terms of service](terms_of_service.md)
18 | - [Errors](errors.md)
19 |
20 | Also, a websocket is accessible to be notified of users' task and project
21 | changes. Please refer to [the dedicated documentation](websocket.md) to know
22 | more.
23 |
--------------------------------------------------------------------------------
/app/controllers/admin/user_sessions_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Admin
4 | class UserSessionsController < ApplicationController
5 | skip_before_action :authenticate_admin, except: [:destroy]
6 |
7 | def new
8 | @user = User.new
9 | end
10 |
11 | def create
12 | @user = login(params[:username], params[:password])
13 | if @user.try :admin?
14 | redirect_back_or_to(:admin_root, notice: 'Login successful')
15 | elsif @user
16 | logout
17 | flash.now[:alert] = 'You are not authorized to access administration, sorry!'
18 | render action: 'new'
19 | else
20 | flash.now[:alert] = 'Login failed'
21 | render action: 'new'
22 | end
23 | end
24 |
25 | def destroy
26 | logout
27 | redirect_to(:admin_root, notice: 'Logged out!')
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/app/models/project.rb:
--------------------------------------------------------------------------------
1 | class Project < ApplicationRecord
2 | include ProjectLifecycle
3 |
4 | belongs_to :user
5 | has_many :tasks, dependent: :nullify
6 |
7 | validates :user, :name, :slug, presence: true
8 | validates :slug, uniqueness: { scope: :user, message: 'must be unique per user' },
9 | format: { with: /\A[\w\-]{1,}\z/, message: 'must contain letters, numbers, underscores (_) and hiphens (-) only' }
10 | validates :name, length: { maximum: 100 }
11 |
12 | paginates_per 25
13 |
14 | before_validation do
15 | self.slug = self.name.parameterize
16 | end
17 |
18 | before_destroy :abandon_tasks, prepend: true
19 |
20 | private
21 |
22 | def abandon_tasks
23 | # rubocop:disable Rails/SkipsModelValidations
24 | tasks.update_all state: :abandoned, abandoned_at: Time.zone.now
25 | # rubocop:enable Rails/SkipsModelValidations
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/bin/update:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require 'pathname'
3 | require 'fileutils'
4 | include FileUtils
5 |
6 | # path to your application root.
7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
8 |
9 | def system!(*args)
10 | system(*args) || abort("\n== Command #{args} failed ==")
11 | end
12 |
13 | chdir APP_ROOT do
14 | # This script is a way to update your development environment automatically.
15 | # Add necessary update steps to this file.
16 |
17 | puts '== Installing dependencies =='
18 | system! 'gem install bundler --conservative'
19 | system('bundle check') || system!('bundle install')
20 |
21 | puts "\n== Updating database =="
22 | system! 'bin/rails db:migrate'
23 |
24 | puts "\n== Removing old logs and tempfiles =="
25 | system! 'bin/rails log:clear tmp:clear'
26 |
27 | puts "\n== Restarting application server =="
28 | system! 'bin/rails restart'
29 | end
30 |
--------------------------------------------------------------------------------
/client/src/components/Ly/LyForm/LyFormGroup.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
15 |
16 |
44 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | Rails:
2 | Enabled: true
3 |
4 | AllCops:
5 | Exclude:
6 | - 'db/schema.rb'
7 | - 'bin/*'
8 | - 'vendor/*'
9 |
10 | Metrics/LineLength:
11 | Max: 110
12 | IgnoreCopDirectives: true
13 |
14 | Documentation:
15 | Enabled: false
16 |
17 | Metrics/BlockLength:
18 | Exclude:
19 | - 'spec/**/*'
20 |
21 | Rails/DynamicFindBy:
22 | Whitelist:
23 | - find_by_authorization_token
24 | - find_by_identifier!
25 | - find_by_sorcery_token!
26 |
27 | Style/ClassAndModuleChildren:
28 | EnforcedStyle: compact
29 |
30 | Style/EmptyMethod:
31 | EnforcedStyle: expanded
32 |
33 | Style/RedundantSelf:
34 | Enabled: false
35 |
36 | Style/TrailingCommaInArguments:
37 | EnforcedStyleForMultiline: comma
38 |
39 | Style/TrailingCommaInArrayLiteral:
40 | EnforcedStyleForMultiline: comma
41 |
42 | Style/TrailingCommaInHashLiteral:
43 | EnforcedStyleForMultiline: comma
44 |
--------------------------------------------------------------------------------
/client/src/components/projects/ProjectEditPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
11 |
12 |
13 |
14 |
36 |
--------------------------------------------------------------------------------
/client/src/components/projects/ProjectDeleteModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 | {{ $t('projects.modals.deleteIntro', { projectName: project.name }) }}
8 |
9 |
10 |
11 | {{ $t('projects.modals.deleteConfirm') }}
12 |
13 |
14 | {{ $t('projects.modals.deleteCancel') }}
15 |
16 |
17 |
18 |
19 |
34 |
--------------------------------------------------------------------------------
/client/src/components/tasks/TaskSelectableItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 | {{ project.name }}
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
38 |
--------------------------------------------------------------------------------
/client/src/components/Ly/LyColumns/LyColumns.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
18 |
19 |
45 |
--------------------------------------------------------------------------------
/app/controllers/api/users/passwords_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Api::Users::PasswordsController < ApiController
4 | skip_before_action :require_login, if: -> { params[:token] }
5 | skip_before_action :require_tos_accepted, if: -> { params[:token] }
6 | before_action :require_sudo, if: -> { !params[:token] }
7 |
8 | before_action :set_user, only: [:create]
9 | before_action do
10 | require_active_user(@user)
11 | end
12 |
13 | def create
14 | @user.change_password! password_param
15 | @token = @user.token(expiration: 1.month.from_now) if params[:token]
16 | end
17 |
18 | private
19 |
20 | def set_user
21 | @user = if params[:token]
22 | User.find_by_sorcery_token!(params[:token], type: :reset_password)
23 | else
24 | current_user
25 | end
26 | end
27 |
28 | def password_param
29 | fetch_resource_params(:user, [:password])[:password]
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/client/src/components/Ly/LyPopover/LyPopoverItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
21 |
22 |
40 |
--------------------------------------------------------------------------------
/client/src/components/tasks/TaskConfirmAbandonModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 | {{ $t('tasks.modals.confirmAbandon', { label: task.label }) }}
8 |
9 |
10 |
11 |
12 | {{ $t('tasks.modals.submitAbandon') }}
13 |
14 |
15 | {{ $t('tasks.modals.cancel') }}
16 |
17 |
18 |
19 |
20 |
21 |
37 |
--------------------------------------------------------------------------------
/spec/factories/projects.rb:
--------------------------------------------------------------------------------
1 | FactoryBot.define do
2 | factory :project, traits: [:newed] do
3 | sequence(:name, 'a') { |n| "My project #{n}" }
4 | user
5 |
6 | trait :newed do
7 | state { 'newed' }
8 | started_at { nil }
9 | paused_at { nil }
10 | due_at { nil }
11 | finished_at { nil }
12 | end
13 |
14 | trait :started do
15 | state { 'started' }
16 | started_at { 15.days.ago }
17 | paused_at { nil }
18 | due_at { 15.days.from_now }
19 | finished_at { nil }
20 | end
21 |
22 | trait :paused do
23 | state { 'paused' }
24 | started_at { 30.days.ago }
25 | paused_at { 15.days.ago }
26 | due_at { 15.days.from_now }
27 | finished_at { nil }
28 | end
29 |
30 | trait :finished do
31 | state { 'finished' }
32 | started_at { 30.days.ago }
33 | paused_at { nil }
34 | due_at { 15.days.ago }
35 | finished_at { 15.days.ago }
36 | end
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/client/src/components/design/DesignGridPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Grid
5 |
6 | Grid is used to make interface more consistent by aligning components
7 | correctly. The grid is based on a 0.25rem square. It is relative to the
8 | font-size defined on the html tag. It corresponds to 5px on
9 | desktop and 4px on mobile.
10 |
11 | Each element must align and be spaced following this grid. For
12 | example, it means you must avoid using border with a width
13 | of 1px since it would break spacing… or you must offset with
14 | corresponding margins. You can simulate a border with a box shadow as
15 | following: box-shadow: 0 0 1px $ly-color-grey-50;. Please
16 | note we use px unit but since it is shadow, it does not count in box
17 | size.
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/client/src/components/Ly/LyList/LyListGroup.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
17 |
18 |
46 |
--------------------------------------------------------------------------------
/client/src/components/general/NotFoundPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ $t('general.notFoundPage.title') }}
4 |
5 |
6 |
7 | {{ $t('general.notFoundPage.intro') }}
8 |
9 | {{ $t('general.notFoundPage.home') }}
10 |
11 |
12 |
13 |
27 |
--------------------------------------------------------------------------------
/client/build/webpack.test.conf.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | // This is the webpack config used for unit tests.
3 |
4 | const utils = require('./utils')
5 | const webpack = require('webpack')
6 | const merge = require('webpack-merge')
7 | const baseWebpackConfig = require('./webpack.base.conf')
8 |
9 | const webpackConfig = merge(baseWebpackConfig, {
10 | // use inline sourcemap for karma-sourcemap-loader
11 | module: {
12 | rules: utils.styleLoaders()
13 | },
14 | devtool: '#inline-source-map',
15 | resolveLoader: {
16 | alias: {
17 | // necessary to to make lang="scss" work in test when using vue-loader's ?inject option
18 | // see discussion at https://github.com/vuejs/vue-loader/issues/724
19 | 'scss-loader': 'sass-loader'
20 | }
21 | },
22 | plugins: [
23 | new webpack.DefinePlugin({
24 | 'process.env': require('../config/test.env')
25 | })
26 | ]
27 | })
28 |
29 | // no need for app entry during tests
30 | delete webpackConfig.entry
31 |
32 | module.exports = webpackConfig
33 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM ruby:2.4-alpine
2 | MAINTAINER Marien Fressinaud
3 |
4 | EXPOSE 3000
5 |
6 | WORKDIR /app/
7 | COPY Gemfile Gemfile.lock /app/
8 | COPY client/package.json client/package-lock.json /app/client/
9 |
10 | ENV RAILS_ENV production
11 | ENV RAILS_SERVE_STATIC_FILES true
12 | ENV RAILS_LOG_TO_STDOUT true
13 |
14 | RUN apk add --no-cache \
15 | nodejs \
16 | nodejs-npm \
17 | postgresql-client \
18 | tzdata \
19 | && apk --update add --virtual build-dependencies \
20 | build-base \
21 | ruby-dev \
22 | postgresql-dev \
23 | libc-dev \
24 | linux-headers \
25 | cmake \
26 | gmp-dev \
27 | && gem install bundler \
28 | && bundle install --without test development \
29 | && apk del build-dependencies
30 |
31 | COPY . /app
32 |
33 | RUN cd /app/client \
34 | && npm install \
35 | && npm cache clean --force \
36 | && npm run build \
37 | && rm -rf /app/client/node_modules
38 |
39 | CMD ["bundle", "exec", "rails", "server", "-p", "3000"]
40 |
--------------------------------------------------------------------------------
/client/src/store/modules/global.js:
--------------------------------------------------------------------------------
1 | import rootApi from '@/api/root'
2 |
3 | const state = {
4 | resourcesReady: false,
5 | registrationDisabled: true,
6 | tosVersion: null,
7 | }
8 |
9 | const getters = {
10 | registrationDisabled (state) {
11 | return state.registrationDisabled
12 | },
13 |
14 | tosVersion (state) {
15 | return state.tosVersion
16 | },
17 |
18 | resourcesReady (state) {
19 | return state.resourcesReady
20 | },
21 | }
22 |
23 | const actions = {
24 | listInfo ({ commit }, email) {
25 | return rootApi.listInfo()
26 | .then((res) => {
27 | commit('setInfo', res)
28 | })
29 | },
30 | }
31 |
32 | const mutations = {
33 | setInfo (state, data) {
34 | state.registrationDisabled = data.registrationDisabled
35 | state.tosVersion = data.tosVersion
36 | },
37 |
38 | setResourcesReady (state, value) {
39 | state.resourcesReady = value
40 | },
41 | }
42 |
43 | export default {
44 | namespaced: true,
45 | state,
46 | getters,
47 | actions,
48 | mutations,
49 | }
50 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require 'pathname'
3 | require 'fileutils'
4 | include FileUtils
5 |
6 | # path to your application root.
7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
8 |
9 | def system!(*args)
10 | system(*args) || abort("\n== Command #{args} failed ==")
11 | end
12 |
13 | chdir APP_ROOT do
14 | # This script is a starting point to setup your application.
15 | # Add necessary setup steps to this file.
16 |
17 | puts '== Installing dependencies =='
18 | system! 'gem install bundler --conservative'
19 | system('bundle check') || system!('bundle install')
20 |
21 | # puts "\n== Copying sample files =="
22 | # unless File.exist?('config/database.yml')
23 | # cp 'config/database.yml.sample', 'config/database.yml'
24 | # end
25 |
26 | puts "\n== Preparing database =="
27 | system! 'bin/rails db:setup'
28 |
29 | puts "\n== Removing old logs and tempfiles =="
30 | system! 'bin/rails log:clear tmp:clear'
31 |
32 | puts "\n== Restarting application server =="
33 | system! 'bin/rails restart'
34 | end
35 |
--------------------------------------------------------------------------------
/client/src/components/mixins/ErrorsHandler.js:
--------------------------------------------------------------------------------
1 | export default {
2 | data () {
3 | return {
4 | errors: {},
5 | }
6 | },
7 |
8 | methods: {
9 | setFailureErrors (failure) {
10 | const { data: { errors } } = failure
11 | let newErrors = {}
12 |
13 | errors.forEach(error => {
14 | const pointer = error.source == null ? '/_' : error.source.pointer
15 | if (newErrors[pointer] == null) {
16 | newErrors[pointer] = []
17 | }
18 | newErrors[pointer] = [
19 | ...newErrors[pointer],
20 | error.code,
21 | ]
22 | })
23 |
24 | this.errors = newErrors
25 | },
26 |
27 | getErrors (pointer = '/_') {
28 | const i18nPointer = pointer.replace(/\//g, '.')
29 | const i18nBase = `errors${i18nPointer}`
30 | return this.errors[pointer] && this.errors[pointer].map((errorCode) =>
31 | this.$t(i18nBase + '.' + errorCode)
32 | ).join(' ')
33 | },
34 |
35 | cleanErrors () {
36 | this.errors = {}
37 | },
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/client/src/components/tasks/TaskIndicators.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
11 |
12 | {{ $t('tasks.indicators.week', { count: task.startedSinceWeeks }) }}
13 |
14 |
18 | {{ task.replannedCount }}
19 |
20 |
21 |
22 |
23 |
30 |
--------------------------------------------------------------------------------
/docs/tests.md:
--------------------------------------------------------------------------------
1 | # Running tests
2 |
3 | In this section, we assume you already have installed development environment
4 | and you are running it on Docker.
5 |
6 | Tests are using [RSpec](http://rspec.info/) (backend) and [Jest](http://facebook.github.io/jest/) +
7 | [vue-test-utils](https://vue-test-utils.vuejs.org/en/).
8 |
9 | To execute them, just run:
10 |
11 | ```console
12 | $ make test
13 | $ # or
14 | $ make test-back
15 | $ make test-front
16 | ```
17 |
18 | This command calls `rspec` and `jest` commands with docker-compose and it
19 | passes `docker-compose-test.yml` in argument which is an adapted and simpler
20 | version of `docker-compose-dev.yml`.
21 |
22 | Tests' suite is run against [TravisCI](https://travis-ci.org/lessy-community/lessy).
23 | Pull requests must pass tests to be merged so please make sure it's all green
24 | before asking for a review (but you still can ask for help!)
25 |
26 | If you want to know more about how to write tests, please have a look to the
27 | dedicated document [for backend](backend/writing_tests.md) and [for
28 | frontend](frontend/writing_tests.md).
29 |
--------------------------------------------------------------------------------
/app/views/admin/user_sessions/new.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for(:title) do %>
2 | Admin login
3 | <% end %>
4 |
5 |
6 |
7 | Admin login
8 |
9 |
10 |
11 |
12 | <%= form_tag admin_user_sessions_path, method: :post, class: 'form' do %>
13 |
14 |
15 | <%= label_tag :username %>
16 |
17 |
18 | <%= text_field_tag :username %>
19 |
20 |
21 |
22 |
23 |
24 | <%= label_tag :password %>
25 |
26 |
27 | <%= password_field_tag :password %>
28 |
29 |
30 |
31 |
32 | <%= submit_tag 'Login' %>
33 | <%= link_to 'Back to Lessy', root_path %>
34 |
35 | <% end %>
36 |
37 |
--------------------------------------------------------------------------------
/client/src/components/projects/ProjectItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ project.name }}
6 |
7 |
8 |
9 | {{ $t('projects.item.pausedOn', { date: $d(project.pausedAt, 'short') }) }}
10 |
11 |
12 | {{ $tc('projects.item.tasksCount', tasks.length, {
13 | finishedCount: finishedTasks.length,
14 | totalCount: tasks.length,
15 | }) }}
16 |
17 |
18 |
19 |
20 |
37 |
--------------------------------------------------------------------------------
/spec/factories/tasks.rb:
--------------------------------------------------------------------------------
1 | FactoryBot.define do
2 | factory :task, traits: [:newed] do
3 | label { 'My task' }
4 | user
5 | sequence :order
6 |
7 | trait :newed do
8 | state { 'newed' }
9 | started_at { nil }
10 | planned_at { nil }
11 | finished_at { nil }
12 | abandoned_at { nil }
13 | end
14 |
15 | trait :started do
16 | state { 'started' }
17 | started_at { 15.days.ago }
18 | planned_at { nil }
19 | finished_at { nil }
20 | abandoned_at { nil }
21 | end
22 |
23 | trait :planned do
24 | state { 'planned' }
25 | started_at { 15.days.ago }
26 | planned_at { 15.days.from_now }
27 | finished_at { nil }
28 | abandoned_at { nil }
29 | end
30 |
31 | trait :finished do
32 | state { 'finished' }
33 | planned_at { 15.days.ago }
34 | finished_at { 5.days.ago }
35 | abandoned_at { nil }
36 | end
37 |
38 | trait :abandoned do
39 | state { 'abandoned' }
40 | finished_at { nil }
41 | abandoned_at { 5.days.ago }
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/client/src/components/mixins/ResourcesLoader.js:
--------------------------------------------------------------------------------
1 | import auth from '@/auth'
2 |
3 | export default {
4 | computed: {
5 | resourcesReady () {
6 | return this.$store.getters['global/resourcesReady']
7 | },
8 | },
9 |
10 | mounted () {
11 | this.$store
12 | .dispatch('users/getCurrent')
13 | .then(() => {
14 | return Promise.all([
15 | this.$store.dispatch('features/list'),
16 | this.$store.dispatch('tasks/list'),
17 | this.$store.dispatch('projects/list'),
18 | ])
19 | })
20 | .then(() => {
21 | this.$store.commit('global/setResourcesReady', true)
22 | })
23 | .catch((e) => {
24 | const isUnauthorized = e.data.errors.some(error => error.code === 'unauthorized')
25 | if (isUnauthorized) {
26 | // having troubles to fetch current user? It probably means token
27 | // expired or user does not exist. Logout and return to home page.
28 | auth.logout()
29 | window.location = '/'
30 | }
31 | })
32 | },
33 | }
34 |
--------------------------------------------------------------------------------
/client/src/components/projects/ProjectItemFinished.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ project.name }}
6 |
7 |
8 |
9 | {{ $t('projects.itemFinished.finishedLabel', { date: $d(project.finishedAt, 'short') }) }}
10 |
11 |
12 | {{ $tc('projects.itemFinished.tasksCount', tasks.length, {
13 | finishedCount: finishedTasks.length,
14 | totalCount: tasks.length,
15 | }) }}
16 |
17 |
18 |
19 |
20 |
37 |
--------------------------------------------------------------------------------
/client/src/components/Ly/LyForm/LyForm.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
23 |
24 |
51 |
--------------------------------------------------------------------------------
/client/src/components/tasks/TaskTransformInProjectModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
13 |
14 |
15 |
16 |
42 |
--------------------------------------------------------------------------------
/client/src/store/plugins/cable.js:
--------------------------------------------------------------------------------
1 | import ActionCable from 'actioncable'
2 | import auth from '@/auth'
3 |
4 | function cablePlugin (store) {
5 | if (!auth.isLoggedIn()) {
6 | return
7 | }
8 |
9 | document.cookie = `Authorization=${auth.getToken()};path=/`
10 | const cable = ActionCable.createConsumer()
11 | cable.subscriptions.create(
12 | { channel: 'NotificationsChannel' },
13 | {
14 | received: data => {
15 | const [actionType, resourceType] = data.action.split('#', 2)
16 | if (actionType === 'create') {
17 | const resource = store.getters[`${resourceType}/findById`](data.id)
18 | if (!resource) {
19 | store.dispatch(`${resourceType}/get`, { id: data.id })
20 | }
21 | } else if (actionType === 'update') {
22 | const resource = store.getters[`${resourceType}/findById`](data.id)
23 | if (!resource || resource.updatedAt < data.updatedAt) {
24 | store.dispatch(`${resourceType}/get`, { id: data.id })
25 | }
26 | }
27 | },
28 | }
29 | )
30 | }
31 |
32 | export default cablePlugin
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Marien Fressinaud
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/client/src/api/tasks.js:
--------------------------------------------------------------------------------
1 | import { get, post, patch, put } from './http'
2 |
3 | export default {
4 | list () {
5 | return get('/api/users/me/tasks?page=1')
6 | },
7 |
8 | get (id) {
9 | return get(`/api/tasks/${id}`)
10 | },
11 |
12 | create (payload) {
13 | return post('/api/users/me/tasks', payload)
14 | },
15 |
16 | update (task, payload) {
17 | return patch(`/api/tasks/${task.id}`, payload)
18 | },
19 |
20 | finish (task) {
21 | return put(`/api/tasks/${task.id}/state`, {
22 | state: 'finished',
23 | })
24 | },
25 |
26 | start (task) {
27 | return put(`/api/tasks/${task.id}/state`, {
28 | state: 'planned',
29 | })
30 | },
31 |
32 | unplan (task) {
33 | return put(`/api/tasks/${task.id}/state`, {
34 | state: 'started',
35 | })
36 | },
37 |
38 | abandon (task) {
39 | return put(`/api/tasks/${task.id}/state`, {
40 | state: 'abandoned',
41 | })
42 | },
43 |
44 | updateOrder (task, order) {
45 | const payload = {
46 | task: {
47 | order,
48 | },
49 | }
50 | return put(`/api/tasks/${task.id}/order`, payload)
51 | },
52 | }
53 |
--------------------------------------------------------------------------------
/client/src/api/projects.js:
--------------------------------------------------------------------------------
1 | import { get, post, patch, put, destroy } from './http'
2 |
3 | export default {
4 | list () {
5 | return get('/api/users/me/projects?page=1')
6 | },
7 |
8 | get (id) {
9 | return get(`/api/projects/${id}`)
10 | },
11 |
12 | create (name) {
13 | return post('/api/users/me/projects', { name })
14 | },
15 |
16 | update (project, payload) {
17 | return patch(`/api/projects/${project.id}`, payload)
18 | },
19 |
20 | start (project, dueAt) {
21 | return put(`/api/projects/${project.id}/state`, {
22 | project: {
23 | state: 'started',
24 | due_at: dueAt,
25 | },
26 | })
27 | },
28 |
29 | finish (project, finishedAt) {
30 | return put(`/api/projects/${project.id}/state`, {
31 | project: {
32 | state: 'finished',
33 | finished_at: finishedAt,
34 | },
35 | })
36 | },
37 |
38 | pause (project) {
39 | return put(`/api/projects/${project.id}/state`, {
40 | project: {
41 | state: 'paused',
42 | },
43 | })
44 | },
45 |
46 | delete (project) {
47 | return destroy(`/api/projects/${project.id}`)
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/client/src/styles/variables/colors.json:
--------------------------------------------------------------------------------
1 | {
2 | "ly-color-white": "#ffffff",
3 |
4 | "ly-color-grey-10": "#f9f9fa",
5 | "ly-color-grey-20": "#ededf0",
6 | "ly-color-grey-30": "#d7d7db",
7 | "ly-color-grey-40": "#b1b1b3",
8 | "ly-color-grey-50": "#737373",
9 | "ly-color-grey-60": "#4a4a4f",
10 | "ly-color-grey-70": "#38383d",
11 | "ly-color-grey-80": "#2a2a2e",
12 | "ly-color-grey-90": "#0c0c0d",
13 |
14 | "ly-color-pine-50": "#00aba8",
15 | "ly-color-pine-60": "#008c8a",
16 | "ly-color-pine-70": "#007876",
17 | "ly-color-pine-80": "#005958",
18 | "ly-color-pine-90": "#004544",
19 |
20 | "ly-color-green-50": "#00a86a",
21 | "ly-color-green-60": "#00945d",
22 | "ly-color-green-70": "#00754a",
23 | "ly-color-green-80": "#005c3a",
24 | "ly-color-green-90": "#00422a",
25 |
26 | "ly-color-gold-50": "#d1b200",
27 | "ly-color-gold-60": "#b29800",
28 | "ly-color-gold-70": "#857100",
29 | "ly-color-gold-80": "#665700",
30 | "ly-color-gold-90": "#524500",
31 |
32 | "ly-color-red-50": "#d40e00",
33 | "ly-color-red-60": "#ab0b00",
34 | "ly-color-red-70": "#880900",
35 | "ly-color-red-80": "#550600",
36 | "ly-color-red-90": "#3b0400"
37 | }
38 |
--------------------------------------------------------------------------------
/client/src/locales/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import VueI18n from 'vue-i18n'
3 |
4 | import en from './en'
5 | import fr from './fr'
6 |
7 | Vue.use(VueI18n)
8 |
9 | export const SUPPORTED_LANGUAGES = ['en', 'fr']
10 |
11 | const i18n = new VueI18n({
12 | locale: getPreferedLanguage(),
13 | fallbackLocale: 'en',
14 | messages: {
15 | en: en.messages,
16 | fr: fr.messages,
17 | },
18 | dateTimeFormats: {
19 | en: en.dateTimeFormats,
20 | fr: fr.dateTimeFormats,
21 | },
22 | })
23 |
24 | export function getPreferedLanguage () {
25 | const localStorageLanguage = window.localStorage.getItem('language')
26 | if (SUPPORTED_LANGUAGES.includes(localStorageLanguage)) {
27 | return localStorageLanguage
28 | }
29 | const browserLanguage = window.navigator.language
30 | if (SUPPORTED_LANGUAGES.includes(browserLanguage)) {
31 | return browserLanguage
32 | }
33 | return 'en'
34 | }
35 |
36 | export function savePreferedLanguage (language) {
37 | if (SUPPORTED_LANGUAGES.includes(language)) {
38 | window.localStorage.setItem('language', language)
39 | i18n.locale = language
40 | return language
41 | }
42 | }
43 |
44 | export default i18n
45 |
--------------------------------------------------------------------------------
/config/application.rb:
--------------------------------------------------------------------------------
1 | require_relative 'boot'
2 |
3 | require "rails"
4 | # Pick the frameworks you want:
5 | require "active_model/railtie"
6 | require "active_job/railtie"
7 | require "active_record/railtie"
8 | require "action_controller/railtie"
9 | require "action_mailer/railtie"
10 | require "action_view/railtie"
11 | require "action_cable/engine"
12 | # require "sprockets/railtie"
13 | require "rails/test_unit/railtie"
14 |
15 | # Require the gems listed in Gemfile, including any gems
16 | # you've limited to :test, :development, or :production.
17 | Bundler.require(*Rails.groups)
18 |
19 | module Lessy
20 | class Application < Rails::Application
21 | # Settings in config/environments/* take precedence over those specified here.
22 | # Application configuration should go into files in config/initializers
23 | # -- all .rb files in that directory are automatically loaded.
24 |
25 | config.time_zone = 'UTC'
26 |
27 | # Only loads a smaller set of middleware suitable for API only apps.
28 | # Middleware like session, flash, cookies can be added back manually.
29 | # Skip views, helpers and assets when generating a new resource.
30 | config.api_only = false
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/docs/frontend/writing_tests.md:
--------------------------------------------------------------------------------
1 | # Writing tests (frontend)
2 |
3 | We use [Jest](http://facebook.github.io/jest/) + [vue-test-utils](https://vue-test-utils.vuejs.org/en/)
4 | for our frontend tests. For the moment, the frontend suite test is very basic
5 | but we strongly encourage you to write tests when adding new features.
6 |
7 | Tests are written under `client/spec` directory.
8 |
9 | And, yes, we know: testing the frontend is never easy nor pleasant. But let's
10 | try!
11 |
12 | ## What to test?
13 |
14 | Actually, we only test presentational components: there are very basic (few
15 | JavaScript interactions) and they do not require complex mocking. These tests
16 | are not very interesting since they are already taking more than 10 seconds for
17 | less than 20 tests and the code which is tested is extremely simple! However,
18 | they gave insight of what is possible to test.
19 |
20 | Later, we would like to test container components wrapping these components
21 | since they handle more business logic. Store might be tested as well.
22 |
23 | However, we lack of examples for the moment. If you have any question or
24 | suggestion, don't hesitate to [open a ticket on GitHub](https://github.com/lessy-community/lessy/issues).
25 |
--------------------------------------------------------------------------------
/client/src/components/tasks/TasksPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
42 |
--------------------------------------------------------------------------------
/client/src/components/layouts/LayoutProfile.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ $t('layouts.profile.profile') }}
6 |
7 |
8 | {{ $t('layouts.profile.backToApp') }}
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
46 |
--------------------------------------------------------------------------------
/client/src/utils/color.js:
--------------------------------------------------------------------------------
1 | function hexToRGB (hexColor) {
2 | if (hexColor.charAt(0) === '#') {
3 | hexColor = hexColor.substring(1)
4 | }
5 |
6 | return [
7 | parseInt(hexColor.substring(0, 2), 16),
8 | parseInt(hexColor.substring(2, 4), 16),
9 | parseInt(hexColor.substring(4, 6), 16),
10 | ]
11 | }
12 |
13 | function hexLuminance (hexColor) {
14 | return rgbLuminance(hexToRGB(hexColor))
15 | }
16 |
17 | function rgbLuminance (rgbColor) {
18 | // Reference: https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
19 | const [red, green, blue] = rgbColor.map((color8bit) => {
20 | const colorSRGB = color8bit / 255
21 | if (colorSRGB <= 0.03928) {
22 | return colorSRGB / 12.92
23 | } else {
24 | return Math.pow(((colorSRGB + 0.055) / 1.055), 2.4)
25 | }
26 | })
27 | return 0.2126 * red + 0.7152 * green + 0.0722 * blue
28 | }
29 |
30 | function luminanceRatio (luminance1, luminance2) {
31 | // Reference: https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
32 | if (luminance1 > luminance2) {
33 | return (luminance1 + 0.05) / (luminance2 + 0.05)
34 | } else {
35 | return (luminance2 + 0.05) / (luminance1 + 0.05)
36 | }
37 | }
38 |
39 | export {
40 | hexToRGB,
41 | hexLuminance,
42 | rgbLuminance,
43 | luminanceRatio,
44 | }
45 |
--------------------------------------------------------------------------------
/client/src/components/projects/ProjectCardDeck.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 | {{ $t('projects.cardDeck.empty') }}
10 |
14 | {{ $t('projects.cardDeck.emptyLink') }}
15 |
16 |
17 |
18 |
22 |
23 |
24 |
25 |
46 |
47 |
52 |
--------------------------------------------------------------------------------
/client/src/components/tasks/TaskList.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
18 |
19 |
20 |
21 |
50 |
--------------------------------------------------------------------------------
/CONTRIBUTORS.md:
--------------------------------------------------------------------------------
1 | # Lessy's contributors
2 |
3 | This document is here to thank all people who helped Lessy to be what it is. We
4 | didn't set any rules yet in order to appear here but it will definitely not be
5 | bound to code contributions. A user making feedbacks and/or answering to other
6 | users' questions is as valuable as someone making code. Don't hesitate to ask
7 | if you think you deserve to appear in this document! Even smallest
8 | contributions are important.
9 |
10 | If you're not sure how to contribute, please have a look to [our dedicated
11 | document](CONTRIBUTING.md).
12 |
13 | For the moment the list is quite short since Lessy opened its community quite
14 | recently. […] Well, yes, I'm almost alone to work on this project for the
15 | moment. One more good reason to join me :).
16 |
17 | Please keep the list sorted by name.
18 |
19 | ---
20 |
21 | **Marien Fressinaud**
22 |
23 | - [GitHub profile](https://github.com/marienfressinaud)
24 | - website: [marienfressinaud.fr](https://marienfressinaud.fr/)
25 | - email: [lessy@marienfressinaud.fr](mailto:lessy@marienfressinaud.fr)
26 | - Twitter: [@berumuron](https://twitter.com/berumuron)
27 |
28 | Note I'm available to have a drink while discussing of the project. I also can
29 | move to different cities than Grenoble (where I stay). It would be good
30 | occasions to visit! :)
31 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .DEFAULT_GOAL := help
2 |
3 | run: ## Run development environment
4 | docker-compose -f docker-compose-dev.yml up
5 |
6 | build: ## Build Docker image
7 | docker-compose -f docker-compose-dev.yml build
8 |
9 | db-setup: ## Initialize development database
10 | docker-compose -f docker-compose-dev.yml run --rm lessy bundle exec rails db:setup
11 |
12 | install: ## Install dependencies
13 | docker-compose -f docker-compose-dev.yml run --rm --no-deps lessy bundle install
14 | docker-compose -f docker-compose-dev.yml run --rm --no-deps -w /app/client lessy npm install
15 |
16 | stop: ## Stop development environment
17 | docker-compose -f docker-compose-dev.yml stop
18 |
19 | clean: ## Clean development environment
20 | docker-compose -f docker-compose-dev.yml down
21 |
22 | test-back: ## Run tests (backend)
23 | docker-compose -f docker-compose-dev.yml run -e RAILS_ENV=test --rm lessy bundle exec rspec
24 |
25 | test-front: ## Run tests (frontend)
26 | docker-compose -f docker-compose-dev.yml run -e RAILS_ENV=test --rm -w /app/client lessy npm run test
27 |
28 | test: test-back test-front
29 |
30 | help:
31 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
32 |
33 | .PHONY: run build install update db-setup stop clean test test-back test-front help
34 |
--------------------------------------------------------------------------------
/client/src/components/design/DesignWordingPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Wording
5 |
6 | Wording in Lessy is extremely important. It is the way we interact
7 | with users and help them to accomplish their tasks. Words must be chosen
8 | carefully and always be supportive and inclusive.
9 |
10 | Our voice must be authentic: we do not sell a product but we build it
11 | to help people. It must support our values .
12 |
13 |
We must adapt our tone to context and user's mood. For instance, there
14 | are situations where user can be stressed and we have to be caring.
15 |
16 | Always try to suggest instead of telling how to do a thing. Interface
17 | limitations must be explained so user understands why he or she cannot
18 | achieve action he or she performs.
19 |
20 | Words are also important to dedramatize situations. If a user didn't
21 | finish any task during the day, we should not blame her or him. Instead,
22 | we can explain that is not dramatic and sometimes things don't happen as
23 | expected. Tomorrow is a new day!
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/client/build/build.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | require('./check-versions')()
3 |
4 | process.env.NODE_ENV = 'production'
5 |
6 | const ora = require('ora')
7 | const rm = require('rimraf')
8 | const path = require('path')
9 | const chalk = require('chalk')
10 | const webpack = require('webpack')
11 | const config = require('../config')
12 | const webpackConfig = require('./webpack.prod.conf')
13 |
14 | const spinner = ora('building for production...')
15 | spinner.start()
16 |
17 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
18 | if (err) throw err
19 | webpack(webpackConfig, (err, stats) => {
20 | spinner.stop()
21 | if (err) throw err
22 | process.stdout.write(stats.toString({
23 | colors: true,
24 | modules: false,
25 | children: false, // if you are using ts-loader, setting this to true will make typescript errors show up during build
26 | chunks: false,
27 | chunkModules: false
28 | }) + '\n\n')
29 |
30 | if (stats.hasErrors()) {
31 | console.log(chalk.red(' Build failed with errors.\n'))
32 | process.exit(1)
33 | }
34 |
35 | console.log(chalk.cyan(' Build complete.\n'))
36 | console.log(chalk.yellow(
37 | ' Tip: built files are meant to be served over an HTTP server.\n' +
38 | ' Opening index.html over file:// won\'t work.\n'
39 | ))
40 | })
41 | })
42 |
--------------------------------------------------------------------------------
/spec/json_web_token_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rails_helper'
2 | require 'base64'
3 |
4 | RSpec.describe JsonWebToken do
5 | describe '.encode' do
6 | let(:token) { described_class.encode({ foo: 'bar' }, 1.day.from_now) }
7 |
8 | it 'returns a token containing periods' do
9 | expect(token).to include('.')
10 | end
11 |
12 | it 'returns a token where each part is a valid base64-encoded value' do
13 | token.split('.').each do |part|
14 | expect { Base64.urlsafe_decode64(part) }.not_to raise_error
15 | end
16 | end
17 | end
18 |
19 | describe '.decode' do
20 | let(:decoded_token) { described_class.decode(token) }
21 |
22 | context 'when expiration is in the future' do
23 | let(:token) { described_class.encode({ foo: 'bar' }, 1.day.from_now) }
24 |
25 | it 'returns a decoded token with passed data' do
26 | expect(decoded_token[:data][:foo]).to eq('bar')
27 | end
28 | end
29 |
30 | context 'when expiration is in the past' do
31 | let(:token) { described_class.encode({ foo: 'bar' }, 1.day.ago) }
32 |
33 | it 'returns nil' do
34 | expect(decoded_token).to be_nil
35 | end
36 | end
37 |
38 | context 'when token is nil' do
39 | let(:token) { nil }
40 |
41 | it 'returns nil' do
42 | expect(decoded_token).to be_nil
43 | end
44 | end
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/client/src/components/projects/ProjectContainer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
52 |
--------------------------------------------------------------------------------
/client/src/components/users/UserPasswordNewForm.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
13 |
14 |
15 |
16 | {{ $t('users.passwordNewForm.submit') }}
17 |
18 |
19 |
20 |
21 |
22 |
51 |
--------------------------------------------------------------------------------
/spec/mailers/user_mailer_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rails_helper'
2 |
3 | describe UserMailer do
4 | describe 'activation_needed_email' do
5 | let(:user) { create :user }
6 | let(:mail) { described_class.activation_needed_email(user).deliver_now }
7 |
8 | it 'renders the subject' do
9 | expect(mail.subject).to eq('[Lessy] Welcome to Lessy!')
10 | end
11 |
12 | it 'renders the receiver email' do
13 | expect(mail.to).to eq([user.email])
14 | end
15 |
16 | it 'renders the sender email' do
17 | expect(mail.from).to eq(['noreply@lessy.io'])
18 | end
19 |
20 | it 'renders the user activation link' do
21 | expect(mail.body.encoded).to match(user.activation_token)
22 | end
23 | end
24 |
25 | describe 'activation_success_email' do
26 | let(:user) { create :user, username: 'john' }
27 | let(:mail) { described_class.activation_success_email(user).deliver_now }
28 |
29 | it 'renders the subject' do
30 | expect(mail.subject).to eq('[Lessy] Your account is now activated')
31 | end
32 |
33 | it 'renders the receiver email' do
34 | expect(mail.to).to eq([user.email])
35 | end
36 |
37 | it 'renders the sender email' do
38 | expect(mail.from).to eq(['noreply@lessy.io'])
39 | end
40 |
41 | it 'renders the sender username' do
42 | expect(mail.body.encoded).to match(user.username)
43 | end
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/client/src/styles/_forms.scss:
--------------------------------------------------------------------------------
1 | label {
2 | display: block;
3 | margin-bottom: .25rem;
4 | padding-left: .5rem;
5 | }
6 |
7 | input,
8 | textarea {
9 | width: 100%;
10 | height: 2.5rem;
11 | padding: .5rem;
12 |
13 | color: $ly-color-grey-90;
14 | font-size: 1rem;
15 | line-height: 1.5rem;
16 | font-family: inherit;
17 |
18 | background-color: $ly-color-white;
19 | border: 1px solid $ly-color-grey-40;
20 | box-shadow: 1px 1px 2px $ly-color-grey-20 inset;
21 | border-radius: .25rem;
22 |
23 | transition: border .2s ease-in-out;
24 |
25 | &:hover:not(:disabled) {
26 | box-shadow: 0 0 2px $ly-color-grey-30,
27 | 1px 1px 2px $ly-color-grey-20 inset;
28 | }
29 | &:focus {
30 | border: 1px solid $ly-color-pine-50;
31 | box-shadow: 0 0 1px $ly-color-pine-50,
32 | 1px 1px 2px $ly-color-grey-20 inset;
33 | }
34 | &.invalid {
35 | border: 1px solid $ly-color-red-50;
36 | }
37 | &.invalid:focus {
38 | box-shadow: 0 0 1px $ly-color-red-50,
39 | 1px 1px 2px $ly-color-grey-20 inset;
40 | }
41 | &:disabled {
42 | color: $ly-color-grey-50;
43 |
44 | background-color: $ly-color-grey-20;
45 | border-color: $ly-color-grey-30;
46 | }
47 |
48 | &::placeholder {
49 | color: $ly-color-grey-60;
50 | }
51 | }
52 |
53 | input[type="date"] {
54 | max-width: 10rem;
55 | }
56 |
57 | textarea {
58 | min-height: 7rem;
59 | }
60 |
--------------------------------------------------------------------------------
/client/src/components/Ly/LyForm/LyFormTextarea.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ label }}
5 |
6 | ({{ $t('ly.form.input.optional') }})
7 |
8 |
9 |
10 |
{{ caption }}
11 |
12 |
22 |
23 |
{{ error }}
24 |
25 |
26 |
27 |
55 |
--------------------------------------------------------------------------------
/config/secrets.yml:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Your secret key is used for verifying the integrity of signed cookies.
4 | # If you change this key, all old signed cookies will become invalid!
5 |
6 | # Make sure the secret is at least 30 characters and all random,
7 | # no regular words or you'll be exposed to dictionary attacks.
8 | # You can use `rails secret` to generate a secure secret key.
9 |
10 | # Make sure the secrets in this file are kept private
11 | # if you're sharing your code publicly.
12 |
13 | development:
14 | secret_key_base: adec725dac2890c35ab0f4d5d6898282689d7422885848639caf47c657e7fef763846b9ce4f331e865d0bcd732fb679d25c9cb5a503be02e68b041cb1af1da25
15 |
16 | test:
17 | secret_key_base: c985473748adea3e604603a62f3a494b3c486545674f242f64d07779de3e926512c3d475cf30625c1b462530a0a6d446e69d5ac570bd0f9767a6fc848ba76dac
18 |
19 | # Do not keep production secrets in the repository,
20 | # instead read values from the environment.
21 | production:
22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
23 | smtp_username: <%= ENV.fetch("SMTP_USERNAME") { nil } %>
24 | smtp_password: <%= ENV.fetch("SMTP_PASSWORD") { nil } %>
25 | smtp_address: <%= ENV.fetch("SMTP_ADDRESS") { 'localhost' } %>
26 | smtp_domain: <%= ENV.fetch("SMTP_DOMAIN") { 'localhost.localdomain' } %>
27 | smtp_port: <%= ENV.fetch("SMTP_PORT") { 25 } %>
28 | smtp_authentication: <%= ENV.fetch("SMTP_AUTHENTICATION") { nil } %>
29 |
--------------------------------------------------------------------------------
/client/src/components/projects/ProjectEditDueDateForm.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
13 |
14 |
15 |
16 | {{ $t('projects.editForm.submit') }}
17 |
18 |
19 | {{ $t('projects.editForm.cancel') }}
20 |
21 |
22 |
23 |
24 |
25 |
54 |
--------------------------------------------------------------------------------
/client/src/components/Ly/LySection.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
25 |
26 |
68 |
--------------------------------------------------------------------------------
/client/src/components/tasks/TasksPlanModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 | {{ intro }}
9 |
10 |
11 |
17 |
18 |
19 |
23 |
24 |
25 |
26 |
27 |
63 |
--------------------------------------------------------------------------------
/app/controllers/api/users/tasks_controller.rb:
--------------------------------------------------------------------------------
1 | class Api::Users::TasksController < ApiController
2 | skip_before_action :require_tos_accepted, only: [:index]
3 |
4 | def index
5 | @tasks = current_user
6 | .tasks
7 | .not_abandoned
8 | .not_finished_before(2.weeks.ago)
9 | .order(:id)
10 | .page(params[:page])
11 | end
12 |
13 | def create
14 | @task = current_user.tasks.new(create_task_params)
15 | @task.sync_state_with_project
16 | lock_name = "sync_order_for_user_#{current_user.id}"
17 | Task.with_advisory_lock(lock_name) do
18 | @task.sync_order
19 | @task.save!
20 | end
21 |
22 | NotificationsChannel.broadcast_to(
23 | current_user,
24 | action: 'create#tasks',
25 | id: @task.id,
26 | )
27 |
28 | render status: :created
29 | end
30 |
31 | private
32 |
33 | def create_task_params
34 | parameters = fetch_resource_params(:task, [:label], %i[planned_at finished_at project_id])
35 |
36 | if parameters.key?(:planned_at)
37 | parameters[:state] = 'planned'
38 | parameters[:started_at] = Time.current
39 | end
40 |
41 | if parameters.key?(:finished_at)
42 | parameters[:state] = 'finished'
43 | parameters[:planned_at] = Time.current unless parameters.key?(:planned_at)
44 | parameters[:started_at] = Time.current
45 | end
46 |
47 | parameters[:state] = 'newed' unless parameters.key?(:state)
48 |
49 | parameters
50 | end
51 |
52 | end
53 |
--------------------------------------------------------------------------------
/client/src/components/design/DesignVisualsPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Visuals
5 |
6 | Iconography
7 |
8 | We use FontAwesome 4 as our
9 | default set of icons, please never use another set so interface stays
10 | consistent.
11 |
12 | Icon color should adapt according to the text it accompanies. In other
13 | words, same rules apply .
14 |
15 |
Please always provide a text-based equivalent of icon for screen
16 | readers.
17 |
18 | Illustrations, mascot and logo
19 |
20 | We do not have any illustrations, mascot or logo yet. It is something
21 | we definitely want to see appear in the future though.
22 |
23 | Illustrations are a very powerful tool to encourage users when they
24 | are frustrated or lost. They also help to introduce a new feature. Mascot
25 | could bring a touch of humour and be supportive. Logo reflects identity
26 | of Lessy. In short, these tools are an essential part of what Lessy is
27 | (or could be).
28 |
29 | If you have any illustration skill, we would love
30 | to hear about you !
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/spec/requests/api/terms_of_services_request_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rails_helper'
4 |
5 | RSpec.describe Api::TermsOfServicesController, type: :request do
6 | describe 'GET #current' do
7 | subject { get api_terms_of_services_current_path }
8 |
9 | context 'with no terms of service' do
10 | before do
11 | TermsOfService.destroy_all
12 | subject
13 | end
14 |
15 | it 'succeeds with no content' do
16 | expect(response).to have_http_status(:no_content)
17 | end
18 | end
19 |
20 | context 'with actual terms of service' do
21 | let!(:actual_tos) { create :terms_of_service, :in_the_past }
22 | let!(:next_tos) { create :terms_of_service, :in_the_future }
23 |
24 | before { subject }
25 |
26 | it 'succeeds' do
27 | expect(response).to have_http_status(:ok)
28 | end
29 |
30 | it 'matches the terms_of_services/current schema' do
31 | expect(response).to match_response_schema('terms_of_services/current')
32 | end
33 |
34 | it 'returns the actual terms of service' do
35 | tos_json = JSON.parse(response.body)['data']
36 | expect(tos_json['id']).to eq(actual_tos.id)
37 | expect(tos_json['attributes']['content']).to eq(actual_tos.content)
38 | expect(tos_json['attributes']['version']).to eq(actual_tos.version)
39 | expect(tos_json['attributes']['effectiveAt']).to eq(actual_tos.effective_at.to_i)
40 | end
41 | end
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/app/controllers/api/users_controller.rb:
--------------------------------------------------------------------------------
1 | class Api::UsersController < ApiController
2 | skip_before_action :require_login, only: [:create]
3 | skip_before_action :require_tos_accepted, only: %i[create show destroy accept_tos]
4 |
5 | before_action :require_registration_enabled, only: :create
6 | before_action :require_active_user, only: :update
7 | before_action only: :destroy do
8 | require_sudo if current_user.active?
9 | end
10 |
11 | def create
12 | @user = User.create!(create_user_params)
13 | @token = @user.token(expiration: 1.day.from_now)
14 | render status: :created
15 | end
16 |
17 | def show
18 | @user = current_user
19 | end
20 |
21 | def update
22 | @user = current_user
23 | @user.update! update_user_params
24 | end
25 |
26 | def destroy
27 | current_user.destroy!
28 | end
29 |
30 | def accept_tos
31 | @user = current_user
32 | @user.update! terms_of_service: TermsOfService.current
33 | end
34 |
35 | private
36 |
37 | def require_registration_enabled
38 | registration_disabled = !Flipper.enabled?(:feature_registration)
39 | render_error ApiErrors::RegistrationDisabled.new, :forbidden if registration_disabled
40 | end
41 |
42 | def create_user_params
43 | fetch_resource_params(:user, [:email])
44 | .merge(terms_of_service: TermsOfService.current)
45 | end
46 |
47 | def update_user_params
48 | fetch_resource_params(:user, [], %i[email username time_zone])
49 | .delete_if { |k, v| v.nil? }
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/client/build/check-versions.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const chalk = require('chalk')
3 | const semver = require('semver')
4 | const packageConfig = require('../package.json')
5 | const shell = require('shelljs')
6 |
7 | function exec (cmd) {
8 | return require('child_process').execSync(cmd).toString().trim()
9 | }
10 |
11 | const versionRequirements = [
12 | {
13 | name: 'node',
14 | currentVersion: semver.clean(process.version),
15 | versionRequirement: packageConfig.engines.node
16 | }
17 | ]
18 |
19 | if (shell.which('npm')) {
20 | versionRequirements.push({
21 | name: 'npm',
22 | currentVersion: exec('npm --version'),
23 | versionRequirement: packageConfig.engines.npm
24 | })
25 | }
26 |
27 | module.exports = function () {
28 | const warnings = []
29 |
30 | for (let i = 0; i < versionRequirements.length; i++) {
31 | const mod = versionRequirements[i]
32 |
33 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
34 | warnings.push(mod.name + ': ' +
35 | chalk.red(mod.currentVersion) + ' should be ' +
36 | chalk.green(mod.versionRequirement)
37 | )
38 | }
39 | }
40 |
41 | if (warnings.length) {
42 | console.log('')
43 | console.log(chalk.yellow('To use this template, you must update following to modules:'))
44 | console.log()
45 |
46 | for (let i = 0; i < warnings.length; i++) {
47 | const warning = warnings[i]
48 | console.log(' ' + warning)
49 | }
50 |
51 | console.log()
52 | process.exit(1)
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/client/src/components/projects/ProjectFinishForm.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
13 |
14 |
15 |
16 |
17 | {{ $t('projects.finishForm.submit') }}
18 |
19 |
20 | {{ $t('projects.finishForm.cancel') }}
21 |
22 |
23 |
24 |
25 |
26 |
55 |
--------------------------------------------------------------------------------
/app/controllers/api/tasks_controller.rb:
--------------------------------------------------------------------------------
1 | class Api::TasksController < ApiController
2 |
3 | def show
4 | @task = current_task
5 | end
6 |
7 | def update
8 | @task = current_task
9 | @task.assign_attributes update_task_params
10 | @task.sync_state_with_project
11 | @task.save!
12 |
13 | NotificationsChannel.broadcast_to(
14 | current_user,
15 | action: 'update#tasks',
16 | id: @task.id,
17 | updatedAt: @task.updated_at.to_i,
18 | )
19 | end
20 |
21 | def update_state
22 | @task = current_task
23 | @task.update_with_transition! update_task_state_params
24 |
25 | NotificationsChannel.broadcast_to(
26 | current_user,
27 | action: 'update#tasks',
28 | id: @task.id,
29 | updatedAt: @task.updated_at.to_i,
30 | )
31 | end
32 |
33 | def update_order
34 | task = current_task
35 | order = update_task_order_params[:order]
36 | @impacted_tasks = []
37 | @impacted_tasks = task.order_incremental!(order) if order < task.order
38 | @impacted_tasks = task.order_decremental!(order) if order > task.order
39 | end
40 |
41 | private
42 |
43 | def current_task
44 | @current_task ||= current_user.tasks.find(params[:id])
45 | end
46 |
47 | def update_task_params
48 | fetch_resource_params(:task, [], [:label, :project_id])
49 | end
50 |
51 | def update_task_order_params
52 | fetch_resource_params(:task, [:order])
53 | end
54 |
55 | def update_task_state_params
56 | fetch_resource_params(:task, [:state])
57 | end
58 |
59 | end
60 |
--------------------------------------------------------------------------------
/client/src/components/projects/ProjectStartForm.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
13 |
14 |
15 |
16 |
17 | {{ $t('projects.startForm.submit') }}
18 |
19 |
20 | {{ $t('projects.startForm.cancel') }}
21 |
22 |
23 |
24 |
25 |
26 |
56 |
--------------------------------------------------------------------------------
/app/controllers/api/projects_controller.rb:
--------------------------------------------------------------------------------
1 | class Api::ProjectsController < ApiController
2 |
3 | def show
4 | @project = current_project
5 | end
6 |
7 | def update
8 | @project = current_project
9 | @project.update! update_project_params
10 |
11 | NotificationsChannel.broadcast_to(
12 | current_user,
13 | action: 'update#projects',
14 | id: @project.id,
15 | updatedAt: @project.updated_at.to_i,
16 | )
17 | end
18 |
19 | def destroy
20 | current_project.destroy!
21 | end
22 |
23 | def update_state
24 | @project = current_project
25 | @project.update_with_transition! update_project_state_params
26 |
27 | NotificationsChannel.broadcast_to(
28 | current_user,
29 | action: 'update#projects',
30 | id: @project.id,
31 | updatedAt: @project.updated_at.to_i,
32 | )
33 | end
34 |
35 | private
36 |
37 | def current_project
38 | @current_project ||= current_user.projects.find(params[:id])
39 | end
40 |
41 | def update_project_params
42 | permitted_params = %i[name description]
43 | permitted_params << :due_at if current_project.started?
44 | params.require(:project).permit(*permitted_params)
45 | end
46 |
47 | def update_project_state_params
48 | state = params[:project][:state]
49 | if state == 'started'
50 | fetch_resource_params(:project, %i[state due_at])
51 | elsif state == 'finished'
52 | fetch_resource_params(:project, %i[state finished_at])
53 | else
54 | fetch_resource_params(:project, %i[state])
55 | end
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/client/src/components/users/UserPopover.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 | {{ user.displayedName }}
9 |
10 |
11 |
12 |
13 | {{ $t('users.popover.profile') }}
14 |
15 |
16 |
17 |
18 |
19 | {{ $t('users.popover.administration') }}
20 |
21 |
22 |
23 |
24 | {{ $t('users.popover.logout') }}
25 |
26 |
27 |
28 |
29 |
30 |
31 |
57 |
58 |
63 |
--------------------------------------------------------------------------------
/client/src/components/users/UserPasswordResetForm.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
13 |
14 |
15 |
16 | {{ $t('users.passwordResetForm.submit') }}
17 |
18 | {{ $t('users.passwordResetForm.login') }}
19 |
20 | {{ $t('users.passwordResetForm.register') }}
21 |
22 |
23 |
24 |
25 |
53 |
--------------------------------------------------------------------------------
/client/src/components/App/AppLogo.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
Lessy
7 |
8 |
9 |
10 |
19 |
20 |
66 |
--------------------------------------------------------------------------------
/docs/api/terms_of_service.md:
--------------------------------------------------------------------------------
1 | # Terms of service (API)
2 |
3 | ## `GET /api/terms_of_services/current`
4 |
5 | Return the current terms applicable to the service.
6 |
7 | **This endpoint doesn't require an `Authorization` header.**
8 |
9 | Parameters: none.
10 |
11 | Result format:
12 |
13 | | Name | Type | Description | Optional |
14 | |-----------------------------|--------|---------------------------------------------------|----------|
15 | | data | object | | yes |
16 | | data.type | string | Type of returned data (always `terms_of_service`) | |
17 | | data.id | number | Terms of service's identifier | |
18 | | data.attributes | object | | |
19 | | data.attributes.content | string | Terms of service's content (HTML) | |
20 | | data.attributes.version | string | Terms of service's version (unique) | |
21 | | data.attributes.effectiveAt | number | Since when the terms apply to the service | |
22 |
23 | Note: the endpoint can return a `no_content` response if there is no terms of
24 | service.
25 |
26 | Example:
27 |
28 | ```console
29 | $ curl https://lessy.io/api/terms_of_services/current
30 | ```
31 |
32 | ```json
33 | {
34 | "data": {
35 | "type": "terms_of_service",
36 | "id": 1,
37 | "attributes": {
38 | "content": "[...]",
39 | "version": "2018-06",
40 | "effectiveAt": 1529855818
41 | }
42 | }
43 | }
44 | ```
45 |
--------------------------------------------------------------------------------
/app/dashboards/terms_of_service_dashboard.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'administrate/base_dashboard'
4 |
5 | class TermsOfServiceDashboard < Administrate::BaseDashboard
6 | # ATTRIBUTE_TYPES
7 | # a hash that describes the type of each of the model's fields.
8 | #
9 | # Each different type represents an Administrate::Field object,
10 | # which determines how the attribute is displayed
11 | # on pages throughout the dashboard.
12 | ATTRIBUTE_TYPES = {
13 | id: Field::Number,
14 | content: Field::Text,
15 | version: Field::String,
16 | effective_at: Field::DateTime,
17 | created_at: Field::DateTime,
18 | }.freeze
19 |
20 | # COLLECTION_ATTRIBUTES
21 | # an array of attributes that will be displayed on the model's index page.
22 | #
23 | # By default, it's limited to four items to reduce clutter on index pages.
24 | # Feel free to add, remove, or rearrange items.
25 | COLLECTION_ATTRIBUTES = %i[
26 | id
27 | version
28 | effective_at
29 | ].freeze
30 |
31 | # SHOW_PAGE_ATTRIBUTES
32 | # an array of attributes that will be displayed on the model's show page.
33 | SHOW_PAGE_ATTRIBUTES = %i[
34 | content
35 | version
36 | effective_at
37 | created_at
38 | ].freeze
39 |
40 | # FORM_ATTRIBUTES
41 | # an array of attributes that will be displayed
42 | # on the model's form (`new` and `edit`) pages.
43 | FORM_ATTRIBUTES = %i[
44 | content
45 | version
46 | effective_at
47 | ].freeze
48 |
49 | # Overwrite this method to customize how users are displayed
50 | # across all pages of the admin dashboard.
51 | def display_resource(terms_of_service)
52 | "ToS #{terms_of_service.version}"
53 | end
54 | end
55 |
--------------------------------------------------------------------------------
/client/src/components/users/UserRegisterForm.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
13 |
14 |
15 |
16 | {{ $t('users.registerForm.tosMustAccept') }}
17 | {{ $t('users.registerForm.tosLink') }} .
18 |
19 |
20 |
21 |
22 | {{ $t('users.registerForm.submit') }}
23 |
24 |
25 | {{ $t('users.registerForm.login') }}
26 |
27 |
28 |
29 |
30 |
31 |
57 |
--------------------------------------------------------------------------------
/client/src/styles/app.scss:
--------------------------------------------------------------------------------
1 | @import 'variables/index';
2 |
3 | @import 'fonts';
4 | @import 'forms';
5 | @import 'grid';
6 | @import 'links';
7 | @import 'typography';
8 |
9 | @import 'components/vue_tooltip';
10 |
11 | * {
12 | box-sizing: border-box;
13 | }
14 |
15 | html,
16 | body {
17 | margin: 0;
18 | padding: 0;
19 | }
20 |
21 | body {
22 | background: url("/static/logo-text-inverse.png") center 25vh no-repeat $ly-color-pine-70;
23 | }
24 |
25 | @media(max-width: $medium-screen-width) {
26 | body {
27 | background: url("/static/logo-inverse.svg") center 25vh no-repeat $ly-color-pine-70;
28 | }
29 | }
30 |
31 | body.modal-opened {
32 | overflow: hidden;
33 | }
34 |
35 | img {
36 | height: auto;
37 | max-width: 100%;
38 | }
39 |
40 | hr {
41 | display: block;
42 | margin-top: 1rem;
43 | margin-bottom: 2rem;
44 |
45 | border: 0;
46 | border-radius: .25rem;
47 | box-shadow: 0 0 0 1px $ly-color-grey-30;
48 | }
49 |
50 | code {
51 | padding: .1rem .25rem;
52 |
53 | font-size: .9em;
54 |
55 | background-color: $ly-color-grey-30;
56 | border-radius: .25rem;
57 | }
58 | .text-on-dark code {
59 | background-color: $ly-color-grey-70;
60 | }
61 |
62 | table {
63 | width: 100%;
64 | margin-bottom: 1rem;
65 |
66 | border-collapse: collapse;
67 |
68 | th {
69 | padding: .25rem .5rem;
70 |
71 | font-size: .9rem;
72 | text-align: left;
73 | font-weight: bold;
74 |
75 | background-color: $ly-color-grey-30;
76 | }
77 |
78 | tr:nth-child(odd) {
79 | background-color: $ly-color-grey-20;
80 | }
81 |
82 | td {
83 | padding: .5rem;
84 | }
85 | }
86 |
87 | @media(max-width: $small-screen-width) {
88 | .no-mobile {
89 | display: none;
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/client/src/styles/_typography.scss:
--------------------------------------------------------------------------------
1 | html {
2 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
3 | font-size: 20px;
4 | line-height: 1.5rem;
5 | color: $ly-color-grey-90;
6 | }
7 |
8 | @media(max-width: $small-screen-width) {
9 | html {
10 | font-size: 16px;
11 | }
12 | }
13 |
14 | h1, h2, h3 {
15 | margin-top: 0;
16 | margin-bottom: 0.5rem;
17 |
18 | word-wrap: break-word;
19 | }
20 |
21 | h1 {
22 | margin-bottom: 1.75rem;
23 | }
24 |
25 | h1 { font-size: 2rem; line-height: 2.5rem; }
26 | h2 { font-size: 1.5rem; line-height: 2rem; }
27 | h3 { font-size: 1rem; line-height: 1.5rem; }
28 |
29 | * + h1,
30 | * + h2,
31 | * + h3 {
32 | margin-top: 1.5rem;
33 | }
34 |
35 | p {
36 | margin-top: 0;
37 | margin-bottom: 1rem;
38 | }
39 | p:last-child {
40 | margin-bottom: 0;
41 | }
42 |
43 | ul, ol, dl {
44 | margin-top: 0;
45 | margin-bottom: 1rem;
46 | }
47 | ul:last-child,
48 | ol:last-child,
49 | dl:last-child {
50 | margin-bottom: 0;
51 | }
52 | p + ul,
53 | p + ol,
54 | p + dl,
55 | p + table {
56 | margin-top: -.5rem;
57 | }
58 |
59 | small,
60 | .text-addition {
61 | font-size: .8em;
62 | line-height: 1em;
63 | }
64 |
65 | .text-secondary {
66 | color: $ly-color-grey-60;
67 | }
68 | .text-primary {
69 | color: $ly-color-pine-70;
70 | }
71 | .text-success {
72 | color: $ly-color-green-70;
73 | }
74 | .text-warning {
75 | color: $ly-color-gold-80;
76 | }
77 | .text-alert {
78 | color: $ly-color-red-60;
79 | }
80 |
81 | .text-on-dark {
82 | color: $ly-color-white;
83 | }
84 | .text-on-dark.text-secondary {
85 | color: $ly-color-grey-30;
86 | }
87 |
88 | strong,
89 | .text-important {
90 | font-weight: bold;
91 | }
92 |
--------------------------------------------------------------------------------
/client/src/components/profile/ProfileDeleteAccount.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
17 |
18 |
19 |
23 | {{ $t('profile.deleteAccount.submit') }}
24 |
25 |
26 |
27 |
32 |
33 |
34 |
35 |
67 |
--------------------------------------------------------------------------------
/client/src/components/general/LoadingPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ $t('general.loadingPage.title') }}
4 |
5 |
6 |
7 |
8 | {{ $t('general.loadingPage.wait') }}
9 |
10 |
11 |
12 | {{ $t('general.loadingPage.tooLong') }}
13 |
14 |
15 | {{ $t('general.loadingPage.reset') }}
16 |
17 |
18 |
19 |
20 |
21 |
43 |
44 |
65 |
--------------------------------------------------------------------------------
/docs/api/authorizations.md:
--------------------------------------------------------------------------------
1 | # Authorizations (API)
2 |
3 | Almost all API requests require authorization which is based on a JsonWebToken
4 | system. We will discuss how to get user token later in this document.
5 |
6 | Important note: unless it is specified, the endpoints requiring authorization
7 | also require that user accepted current terms of service.
8 |
9 | To authorize requests to the Lessy API, you have to send an `Authorization`
10 | HTTP header.
11 |
12 | Example:
13 |
14 | ```console
15 | $ curl -H 'Authorization: ' https://lessy.io/api/users/me
16 | ```
17 |
18 | Or in JavaScript:
19 |
20 | ```js
21 | window.fetch('https://lessy.io/api/users/me', {
22 | method: 'GET',
23 | headers: {
24 | 'Content-Type': 'application/json',
25 | 'Authorization': '',
26 | },
27 | });
28 | ```
29 |
30 | To obtain a token you must authorize yourself:
31 |
32 | ```console
33 | $ curl -H "Content-Type: application/json" \
34 | -X POST \
35 | -d '{"username": "dalecooper", "password": "secret"}' \
36 | https://lessy.io/api/users/authorizations
37 | ```
38 |
39 | If everything is OK, it should return:
40 |
41 | ```json
42 | {
43 | "data": {
44 | "type": "user",
45 | "id": 1,
46 | "attributes": {
47 | "username": "dalecooper",
48 | "email": "dale.cooper@lessy.io"
49 | }
50 | },
51 | "meta": {
52 | "token": ""
53 | }
54 | }
55 | ```
56 |
57 | The returned token must be saved and is valid for the next month only. Then it
58 | will be invalidated.
59 |
60 | You can also ask for a "sudo" token which has greater permissions but is valid
61 | for only 15 minutes. Endpoints requiring a sudo token are clearly identified in
62 | the documentation.
63 |
64 | You can learn more about `users` API in [the dedicated section](users.md).
65 |
--------------------------------------------------------------------------------
/client/src/components/projects/ProjectCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ project.name }}
6 |
7 |
8 | {{ project.name }}
9 |
10 |
11 |
12 |
13 |
14 |
15 |
19 | {{ $tc('projects.card.tasksCount', tasks.length, {
20 | finishedCount: finishedTasks.length,
21 | totalCount: tasks.length,
22 | }) }}
23 |
24 |
25 |
26 | {{ $t('projects.card.shouldAddTasks') }}
27 |
28 |
29 |
30 |
31 |
32 |
33 |
64 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | # Generate administration
4 | gem 'administrate', '~> 0.8'
5 |
6 | # Support for feature flags
7 | gem 'flipper', '~> 0.12'
8 | gem 'flipper-active_record', '~> 0.12'
9 |
10 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
11 | gem 'jbuilder', '~> 2.7'
12 |
13 | # Manage pagination for us
14 | gem 'kaminari', '~> 1.1'
15 |
16 | # Use sqlite3 as the database for Active Record
17 | gem 'pg', '~> 1.0'
18 |
19 | # Use Puma as the app server
20 | gem 'puma', '~> 3.0'
21 |
22 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
23 | gem 'rails', '~> 5.1'
24 |
25 | # Use Redis adapter to run Action Cable in production
26 | gem 'redis', '~> 4.0'
27 |
28 | # Add authentication methods
29 | gem 'sorcery', '~> 0.12'
30 |
31 | # Provide a lock system for DB
32 | gem 'with_advisory_lock', '~> 3.2'
33 |
34 | group :development, :test do
35 | gem 'action-cable-testing', '~> 0.2'
36 | # Call 'ap' anywhere in the code to pretty print your Ruby objects with style
37 | gem 'awesome_print', '~> 1.8', require: 'ap'
38 | # Call 'byebug' anywhere in the code to stop execution and get a debugger console
39 | gem 'byebug', '~> 10.0', platform: :mri
40 | gem 'factory_bot_rails', '~> 4.8'
41 | gem 'faker', '~> 1.8'
42 | gem 'json_matchers', '= 0.9'
43 | gem 'rspec-rails', '~> 3.6'
44 | gem 'rubocop', '~> 0.52', require: false
45 | gem 'shoulda-matchers', '~> 3.1'
46 | gem 'timecop', '~> 0.8'
47 | end
48 |
49 | group :development do
50 | gem 'foreman', '~> 0.84'
51 | gem 'letter_opener', '~> 1.4'
52 | gem 'listen', '~> 3.1'
53 | # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
54 | gem 'spring', '~> 2.0'
55 | gem 'spring-watcher-listen', '~> 2.0'
56 | end
57 |
--------------------------------------------------------------------------------